snow-flow 10.0.1-dev.463 → 10.0.1-dev.464

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.463",
3
+ "version": "10.0.1-dev.464",
4
4
  "name": "snow-flow",
5
5
  "description": "Snow-Flow - ServiceNow Multi-Agent Development Framework powered by AI",
6
6
  "license": "Elastic-2.0",
@@ -1040,6 +1040,252 @@ async function addActionViaGraphQL(
1040
1040
  }
1041
1041
  }
1042
1042
 
1043
+ // ── DATA PILL CONDITION HELPERS ────────────────────────────────────────
1044
+
1045
+ /**
1046
+ * Get trigger info from a flow for constructing data pill references.
1047
+ * Reads the flow version payload to find the trigger name, table, and outputs.
1048
+ *
1049
+ * Returns the data pill base (e.g., "Created or Updated_1") and table (e.g., "incident").
1050
+ * The data pill base is used as: {{dataPillBase.fieldName}} in condition values.
1051
+ */
1052
+ async function getFlowTriggerInfo(
1053
+ client: any,
1054
+ flowId: string
1055
+ ): Promise<{ dataPillBase: string; triggerName: string; table: string; tableLabel: string; tableRef: string; error?: string }> {
1056
+ var triggerName = '';
1057
+ var table = '';
1058
+ var tableLabel = '';
1059
+
1060
+ try {
1061
+ // Read the flow version payload which contains all flow elements
1062
+ var resp = await client.get('/api/now/table/sys_hub_flow_version', {
1063
+ params: {
1064
+ sysparm_query: 'flow=' + flowId + '^ORDERBYDESCsys_created_on',
1065
+ sysparm_fields: 'sys_id,payload',
1066
+ sysparm_limit: 1
1067
+ }
1068
+ });
1069
+ var payload = resp.data.result?.[0]?.payload;
1070
+ if (payload) {
1071
+ var parsed = typeof payload === 'string' ? JSON.parse(payload) : payload;
1072
+ // The payload contains triggerInstances array with trigger data
1073
+ var triggerInstances = parsed.triggerInstances || parsed.trigger_instances || [];
1074
+ if (!Array.isArray(triggerInstances)) {
1075
+ // Some payloads nest elements differently
1076
+ if (parsed.elements) {
1077
+ triggerInstances = (Array.isArray(parsed.elements) ? parsed.elements : []).filter(
1078
+ (e: any) => e.type === 'trigger' || e.elementType === 'trigger'
1079
+ );
1080
+ }
1081
+ }
1082
+ if (triggerInstances.length > 0) {
1083
+ var trigger = triggerInstances[0];
1084
+ triggerName = trigger.name || trigger.triggerName || '';
1085
+ // Look for table in trigger inputs
1086
+ if (trigger.inputs && Array.isArray(trigger.inputs)) {
1087
+ for (var ti = 0; ti < trigger.inputs.length; ti++) {
1088
+ if (trigger.inputs[ti].name === 'table') {
1089
+ table = trigger.inputs[ti].value?.value || trigger.inputs[ti].value || '';
1090
+ break;
1091
+ }
1092
+ }
1093
+ }
1094
+ }
1095
+ }
1096
+ } catch (_) {}
1097
+
1098
+ // Look up table label for display in label cache
1099
+ if (table) {
1100
+ try {
1101
+ var labelResp = await client.get('/api/now/table/sys_db_object', {
1102
+ params: {
1103
+ sysparm_query: 'name=' + table,
1104
+ sysparm_fields: 'label',
1105
+ sysparm_display_value: 'true',
1106
+ sysparm_limit: 1
1107
+ }
1108
+ });
1109
+ tableLabel = str(labelResp.data.result?.[0]?.label) || '';
1110
+ } catch (_) {}
1111
+ if (!tableLabel) {
1112
+ // Fallback: capitalize the table name
1113
+ tableLabel = table.charAt(0).toUpperCase() + table.slice(1).replace(/_/g, ' ');
1114
+ }
1115
+ }
1116
+
1117
+ if (!triggerName) {
1118
+ return { dataPillBase: '', triggerName: '', table: table, tableLabel: tableLabel, tableRef: table, error: 'Could not determine trigger name from flow version payload' };
1119
+ }
1120
+
1121
+ var dataPillBase = triggerName + '_1.current';
1122
+ return { dataPillBase, triggerName, table, tableLabel, tableRef: table };
1123
+ }
1124
+
1125
+ /**
1126
+ * Parse a ServiceNow encoded query into individual condition clauses.
1127
+ * Each clause has: prefix (^ or ^OR), field, operator, value.
1128
+ *
1129
+ * Example: "category=inquiry^priority!=1^ORshort_descriptionLIKEtest"
1130
+ * → [
1131
+ * { prefix: '', field: 'category', operator: '=', value: 'inquiry' },
1132
+ * { prefix: '^', field: 'priority', operator: '!=', value: '1' },
1133
+ * { prefix: '^OR', field: 'short_description', operator: 'LIKE', value: 'test' }
1134
+ * ]
1135
+ */
1136
+ function parseEncodedQuery(query: string): { prefix: string; field: string; operator: string; value: string }[] {
1137
+ if (!query || query === '^EQ') return [];
1138
+
1139
+ // Remove trailing ^EQ
1140
+ var q = query.replace(/\^EQ$/, '');
1141
+ if (!q) return [];
1142
+
1143
+ // Split on ^OR and ^ while keeping the separators
1144
+ var clauses: { prefix: string; raw: string }[] = [];
1145
+ var parts = q.split(/(\^OR|\^NQ|\^)/);
1146
+ var currentPrefix = '';
1147
+
1148
+ for (var i = 0; i < parts.length; i++) {
1149
+ var part = parts[i];
1150
+ if (part === '^' || part === '^OR' || part === '^NQ') {
1151
+ currentPrefix = part;
1152
+ } else if (part.length > 0) {
1153
+ clauses.push({ prefix: currentPrefix, raw: part });
1154
+ currentPrefix = '';
1155
+ }
1156
+ }
1157
+
1158
+ // Operators sorted by length descending to match longest first
1159
+ var operators = [
1160
+ 'VALCHANGES', 'CHANGESFROM', 'CHANGESTO',
1161
+ 'ISNOTEMPTY', 'ISEMPTY', 'EMPTYSTRING', 'ANYTHING',
1162
+ 'NOT LIKE', 'NOT IN', 'NSAMEAS',
1163
+ 'STARTSWITH', 'ENDSWITH', 'BETWEEN', 'INSTANCEOF',
1164
+ 'DYNAMIC', 'SAMEAS',
1165
+ 'LIKE', 'IN',
1166
+ '!=', '>=', '<=', '>', '<', '='
1167
+ ];
1168
+
1169
+ var result: { prefix: string; field: string; operator: string; value: string }[] = [];
1170
+ for (var j = 0; j < clauses.length; j++) {
1171
+ var clause = clauses[j];
1172
+ var raw = clause.raw;
1173
+ var matched = false;
1174
+
1175
+ for (var k = 0; k < operators.length; k++) {
1176
+ var op = operators[k];
1177
+ var opIdx = raw.indexOf(op);
1178
+ if (opIdx > 0) {
1179
+ result.push({
1180
+ prefix: clause.prefix,
1181
+ field: raw.substring(0, opIdx),
1182
+ operator: op,
1183
+ value: raw.substring(opIdx + op.length)
1184
+ });
1185
+ matched = true;
1186
+ break;
1187
+ }
1188
+ }
1189
+ if (!matched) {
1190
+ // Unrecognized format — keep as-is
1191
+ result.push({ prefix: clause.prefix, field: raw, operator: '', value: '' });
1192
+ }
1193
+ }
1194
+
1195
+ return result;
1196
+ }
1197
+
1198
+ /**
1199
+ * Transform an encoded query condition value to use data pill references.
1200
+ * Replaces field names with {{dataPillBase.fieldName}} syntax.
1201
+ *
1202
+ * Example: "category=inquiry" → "{{Created or Updated_1.current.category}}=inquiry"
1203
+ */
1204
+ function transformConditionToDataPills(conditionValue: string, dataPillBase: string): string {
1205
+ if (!conditionValue || !dataPillBase) return conditionValue;
1206
+
1207
+ var clauses = parseEncodedQuery(conditionValue);
1208
+ if (clauses.length === 0) return conditionValue;
1209
+
1210
+ var result = '';
1211
+ for (var i = 0; i < clauses.length; i++) {
1212
+ var clause = clauses[i];
1213
+ result += clause.prefix;
1214
+ result += '{{' + dataPillBase + '.' + clause.field + '}}';
1215
+ result += clause.operator;
1216
+ result += clause.value;
1217
+ }
1218
+
1219
+ return result;
1220
+ }
1221
+
1222
+ /**
1223
+ * Build labelCache entries for data pills used in flow logic conditions.
1224
+ * Each unique data pill reference needs a labelCache entry so the Flow Designer UI
1225
+ * can display the data pill label correctly.
1226
+ *
1227
+ * The entry format matches the captured UI mutation format.
1228
+ */
1229
+ function buildConditionLabelCache(
1230
+ conditionValue: string,
1231
+ dataPillBase: string,
1232
+ triggerName: string,
1233
+ table: string,
1234
+ tableLabel: string,
1235
+ logicUiId: string
1236
+ ): any[] {
1237
+ if (!conditionValue || !dataPillBase) return [];
1238
+
1239
+ var clauses = parseEncodedQuery(conditionValue);
1240
+ if (clauses.length === 0) return [];
1241
+
1242
+ // Build a label cache entry for each unique field-level data pill
1243
+ var seen: Record<string, boolean> = {};
1244
+ var entries: any[] = [];
1245
+
1246
+ // Also register the record-level data pill (the base: TriggerName_1.current)
1247
+ // This is needed for the condition builder to understand the context
1248
+ if (!seen[dataPillBase]) {
1249
+ seen[dataPillBase] = true;
1250
+ entries.push({
1251
+ name: dataPillBase,
1252
+ label: 'Trigger - Record ' + triggerName + '\u27a1' + tableLabel + ' Record',
1253
+ reference: table,
1254
+ reference_display: tableLabel,
1255
+ type: 'reference',
1256
+ base_type: 'reference',
1257
+ attributes: '',
1258
+ usedInstances: [{ uiUniqueIdentifier: logicUiId, inputName: 'condition' }],
1259
+ choices: {}
1260
+ });
1261
+ }
1262
+
1263
+ for (var i = 0; i < clauses.length; i++) {
1264
+ var field = clauses[i].field;
1265
+ if (!field) continue;
1266
+ var pillName = dataPillBase + '.' + field;
1267
+ if (seen[pillName]) continue;
1268
+ seen[pillName] = true;
1269
+
1270
+ // Capitalize field name for label
1271
+ var fieldLabel = field.replace(/_/g, ' ').replace(/\b\w/g, function (c: string) { return c.toUpperCase(); });
1272
+
1273
+ entries.push({
1274
+ name: pillName,
1275
+ label: 'Trigger - Record ' + triggerName + '\u27a1' + tableLabel + ' Record\u27a1' + fieldLabel,
1276
+ reference: '',
1277
+ reference_display: '',
1278
+ type: 'string',
1279
+ base_type: 'string',
1280
+ attributes: '',
1281
+ usedInstances: [{ uiUniqueIdentifier: logicUiId, inputName: 'condition' }],
1282
+ choices: {}
1283
+ });
1284
+ }
1285
+
1286
+ return entries;
1287
+ }
1288
+
1043
1289
  // ── FLOW LOGIC (If/Else, For Each, etc.) ─────────────────────────────
1044
1290
 
1045
1291
  async function addFlowLogicViaGraphQL(
@@ -1112,11 +1358,37 @@ async function addFlowLogicViaGraphQL(
1112
1358
  steps.resolved_inputs = inputResult.resolvedInputs;
1113
1359
  steps.input_query_stats = { defParamsFound: inputResult.defParamsCount, inputsBuilt: inputResult.inputs.length, error: inputResult.inputQueryError };
1114
1360
 
1361
+ // ── Data pill transformation for condition inputs ───────────────────
1362
+ // Flow Designer conditions require data pill references ({{TriggerName_1.current.field}})
1363
+ // instead of bare field names. Transform encoded query conditions and build labelCache.
1364
+ const uuid = generateUUID();
1365
+ var labelCacheEntries: any[] = [];
1366
+
1367
+ var conditionInput = inputResult.inputs.find(function (inp: any) { return inp.name === 'condition'; });
1368
+ var rawCondition = conditionInput?.value?.value || '';
1369
+ if (rawCondition && rawCondition !== '^EQ') {
1370
+ var triggerInfo = await getFlowTriggerInfo(client, flowId);
1371
+ steps.trigger_info = { dataPillBase: triggerInfo.dataPillBase, triggerName: triggerInfo.triggerName, table: triggerInfo.table, tableLabel: triggerInfo.tableLabel, error: triggerInfo.error };
1372
+
1373
+ if (triggerInfo.dataPillBase) {
1374
+ // Transform condition value: "category=inquiry" → "{{Created or Updated_1.current.category}}=inquiry"
1375
+ var transformedCondition = transformConditionToDataPills(rawCondition, triggerInfo.dataPillBase);
1376
+ conditionInput.value = { schemaless: false, schemalessValue: '', value: transformedCondition };
1377
+ steps.condition_transform = { original: rawCondition, transformed: transformedCondition };
1378
+
1379
+ // Build labelCache entries for each data pill used in the condition
1380
+ labelCacheEntries = buildConditionLabelCache(
1381
+ rawCondition, triggerInfo.dataPillBase, triggerInfo.triggerName,
1382
+ triggerInfo.tableRef, triggerInfo.tableLabel, uuid
1383
+ );
1384
+ steps.label_cache = { count: labelCacheEntries.length, pills: labelCacheEntries.map(function (e: any) { return e.name; }) };
1385
+ }
1386
+ }
1387
+
1115
1388
  // Calculate insertion order
1116
1389
  const resolvedOrder = await calculateInsertOrder(client, flowId, parentUiId, order);
1117
1390
  steps.insert_order = resolvedOrder;
1118
1391
 
1119
- const uuid = generateUUID();
1120
1392
  const logicResponseFields = 'flowLogics { inserts { sysId uiUniqueIdentifier __typename } updates deletes __typename }' +
1121
1393
  ' actions { inserts { sysId uiUniqueIdentifier __typename } updates deletes __typename }' +
1122
1394
  ' subflows { inserts { sysId uiUniqueIdentifier __typename } updates deletes __typename }';
@@ -1147,6 +1419,11 @@ async function addFlowLogicViaGraphQL(
1147
1419
  }
1148
1420
  };
1149
1421
 
1422
+ // Add labelCache entries for data pill references in conditions
1423
+ if (labelCacheEntries.length > 0) {
1424
+ flowPatch.labelCache = { insert: labelCacheEntries };
1425
+ }
1426
+
1150
1427
  // Add parent flow logic update signal (tells GraphQL the parent was modified)
1151
1428
  if (parentUiId) {
1152
1429
  flowPatch.flowLogics.update = [{ uiUniqueIdentifier: parentUiId, type: 'flowlogic' }];