@servicenow/sdk-build-plugins 4.8.0 → 4.8.1
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/flow/flow-logic/flow-logic-diagnostics.js +2 -1
- package/dist/flow/flow-logic/flow-logic-diagnostics.js.map +1 -1
- package/dist/flow/flow-logic/flow-logic-plugin.js.map +1 -1
- package/dist/flow/plugins/flow-action-definition-plugin.js +81 -16
- package/dist/flow/plugins/flow-action-definition-plugin.js.map +1 -1
- package/dist/flow/plugins/flow-definition-plugin.js +70 -7
- package/dist/flow/plugins/flow-definition-plugin.js.map +1 -1
- package/dist/flow/plugins/flow-instance-plugin.d.ts +35 -1
- package/dist/flow/plugins/flow-instance-plugin.js +240 -6
- package/dist/flow/plugins/flow-instance-plugin.js.map +1 -1
- package/dist/flow/plugins/step-instance-plugin.js +60 -0
- package/dist/flow/plugins/step-instance-plugin.js.map +1 -1
- package/dist/flow/post-install.d.ts +2 -1
- package/dist/flow/post-install.js +31 -4
- package/dist/flow/post-install.js.map +1 -1
- package/dist/flow/utils/complex-object-resolver.js +4 -2
- package/dist/flow/utils/complex-object-resolver.js.map +1 -1
- package/dist/flow/utils/datapill-transformer.d.ts +5 -72
- package/dist/flow/utils/datapill-transformer.js +199 -28
- package/dist/flow/utils/datapill-transformer.js.map +1 -1
- package/dist/flow/utils/flow-io-to-record.js +24 -15
- package/dist/flow/utils/flow-io-to-record.js.map +1 -1
- package/dist/flow/utils/flow-shapes.d.ts +7 -1
- package/dist/flow/utils/flow-shapes.js +19 -0
- package/dist/flow/utils/flow-shapes.js.map +1 -1
- package/dist/flow/utils/flow-variable-processor.d.ts +6 -6
- package/dist/flow/utils/flow-variable-processor.js +8 -8
- package/dist/flow/utils/flow-variable-processor.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/flow/flow-logic/flow-logic-diagnostics.ts +2 -1
- package/src/flow/flow-logic/flow-logic-plugin.ts +0 -1
- package/src/flow/plugins/flow-action-definition-plugin.ts +92 -25
- package/src/flow/plugins/flow-definition-plugin.ts +114 -8
- package/src/flow/plugins/flow-instance-plugin.ts +262 -6
- package/src/flow/plugins/step-instance-plugin.ts +73 -1
- package/src/flow/post-install.ts +36 -5
- package/src/flow/utils/complex-object-resolver.ts +4 -2
- package/src/flow/utils/datapill-transformer.ts +248 -36
- package/src/flow/utils/flow-io-to-record.ts +28 -14
- package/src/flow/utils/flow-shapes.ts +19 -0
- package/src/flow/utils/flow-variable-processor.ts +21 -10
- package/src/index.ts +1 -1
|
@@ -30,7 +30,8 @@ import {
|
|
|
30
30
|
FLOW_OBJECT_API_NAME,
|
|
31
31
|
FLOW_ARRAY_API_NAME,
|
|
32
32
|
} from '../utils/flow-constants'
|
|
33
|
-
import { normalizeInputValue } from './flow-instance-plugin'
|
|
33
|
+
import { normalizeInputValue, applyTemplateValueSubFieldScripts } from './flow-instance-plugin'
|
|
34
|
+
import { FDInlineScriptCallShape } from './inline-script-plugin'
|
|
34
35
|
import { stripPillType, collectPillTypes } from '../utils/flow-pill-utils'
|
|
35
36
|
import { ApprovalRulesShape, ApprovalDueDateShape } from '../utils/flow-shapes'
|
|
36
37
|
import {
|
|
@@ -374,6 +375,9 @@ export const StepInstancePlugin = Plugin.create({
|
|
|
374
375
|
sys_element_mapping: {
|
|
375
376
|
coalesce: ['field', 'table', 'id'],
|
|
376
377
|
},
|
|
378
|
+
sys_hub_input_scripts: {
|
|
379
|
+
coalesce: ['instance', 'input_name'],
|
|
380
|
+
},
|
|
377
381
|
sys_hub_step_instance: {
|
|
378
382
|
relationships: {
|
|
379
383
|
sys_variable_value: {
|
|
@@ -396,6 +400,10 @@ export const StepInstancePlugin = Plugin.create({
|
|
|
396
400
|
via: 'documentkey',
|
|
397
401
|
descendant: true,
|
|
398
402
|
},
|
|
403
|
+
sys_hub_input_scripts: {
|
|
404
|
+
via: 'instance',
|
|
405
|
+
descendant: true,
|
|
406
|
+
},
|
|
399
407
|
},
|
|
400
408
|
toShape(record, { descendants, logger }) {
|
|
401
409
|
// Skip step instances that belong to an action definition —
|
|
@@ -491,6 +499,32 @@ export const StepInstancePlugin = Plugin.create({
|
|
|
491
499
|
: builtInDef.name
|
|
492
500
|
}
|
|
493
501
|
|
|
502
|
+
// Recover wfa.inlineScript() sub-fields from sys_hub_input_scripts descendants.
|
|
503
|
+
// Without this, TemplateValue fields with fd-scripted placeholders would round-trip
|
|
504
|
+
// as TemplateValue({ assignment_group: 'fd-scripted' }) instead of
|
|
505
|
+
// TemplateValue({ assignment_group: wfa.inlineScript('...') }).
|
|
506
|
+
const inputScripts = descendants.query('sys_hub_input_scripts')
|
|
507
|
+
for (const scriptRecord of inputScripts) {
|
|
508
|
+
const inputName = scriptRecord.get('input_name')?.asString()?.getValue()
|
|
509
|
+
const scriptJson = scriptRecord.get('script')?.asString()?.getValue()
|
|
510
|
+
if (!inputName || !scriptJson || configObj[inputName] === undefined) {
|
|
511
|
+
continue
|
|
512
|
+
}
|
|
513
|
+
try {
|
|
514
|
+
const scriptedFields = JSON.parse(scriptJson) as globalThis.Record<
|
|
515
|
+
string,
|
|
516
|
+
{ scriptActive: boolean; script: string }
|
|
517
|
+
>
|
|
518
|
+
configObj[inputName] = applyTemplateValueSubFieldScripts(
|
|
519
|
+
configObj[inputName],
|
|
520
|
+
scriptedFields,
|
|
521
|
+
record
|
|
522
|
+
)
|
|
523
|
+
} catch {
|
|
524
|
+
// malformed script JSON — leave the existing value unchanged
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
494
528
|
// Build step instance config (2nd arg): $id, label, error_handling_type
|
|
495
529
|
const configProperties: globalThis.Record<string, unknown> = {
|
|
496
530
|
$id: NowIdShape.from(record),
|
|
@@ -1079,6 +1113,9 @@ async function createVariableRecords(
|
|
|
1079
1113
|
// Handle TemplateValueShape specially - serialize to ServiceNow format.
|
|
1080
1114
|
// When TemplateValue contains datapills, the platform stores the entire encoded string
|
|
1081
1115
|
// (with pills inline) in a single sys_element_mapping record with field=<parent input name>.
|
|
1116
|
+
// Scripted sub-fields (wfa.inlineScript()) use the 'fd-scripted' placeholder in the
|
|
1117
|
+
// encoded value and have their script content stored in a sys_hub_input_scripts record.
|
|
1118
|
+
const scriptedSubFields: globalThis.Record<string, { scriptActive: true; script: string }> = {}
|
|
1082
1119
|
let actualValue: unknown
|
|
1083
1120
|
if (valueShape.is(TemplateValueShape)) {
|
|
1084
1121
|
const templateObj = (valueShape as TemplateValueShape).getTemplateValue()
|
|
@@ -1091,6 +1128,10 @@ async function createVariableRecords(
|
|
|
1091
1128
|
collectPillTypes(pillString, pillTypeMap)
|
|
1092
1129
|
entries.push(`${field}=${stripPillType(pillString)}`)
|
|
1093
1130
|
hasPills = true
|
|
1131
|
+
} else if (fieldShape instanceof FDInlineScriptCallShape) {
|
|
1132
|
+
// Use fd-scripted placeholder; script content goes to sys_hub_input_scripts
|
|
1133
|
+
entries.push(`${field}=fd-scripted`)
|
|
1134
|
+
scriptedSubFields[field] = { scriptActive: true, script: fieldShape.getScriptContent() }
|
|
1094
1135
|
} else if (fieldShape.is(TemplateExpressionShape)) {
|
|
1095
1136
|
const resolved = resolveTemplateExpression(
|
|
1096
1137
|
fieldShape as TemplateExpressionShape,
|
|
@@ -1111,6 +1152,8 @@ async function createVariableRecords(
|
|
|
1111
1152
|
|
|
1112
1153
|
const encodedValue = entries.join('^')
|
|
1113
1154
|
|
|
1155
|
+
const hasScripts = Object.keys(scriptedSubFields).length > 0
|
|
1156
|
+
|
|
1114
1157
|
if (hasPills) {
|
|
1115
1158
|
// TemplateValue with datapills → single sys_element_mapping record
|
|
1116
1159
|
// No sys_variable_value for this field — platform reads from element_mapping only
|
|
@@ -1126,6 +1169,19 @@ async function createVariableRecords(
|
|
|
1126
1169
|
},
|
|
1127
1170
|
})
|
|
1128
1171
|
)
|
|
1172
|
+
if (hasScripts) {
|
|
1173
|
+
allRecords.push(
|
|
1174
|
+
await factory.createRecord({
|
|
1175
|
+
source: callExpression,
|
|
1176
|
+
table: 'sys_hub_input_scripts',
|
|
1177
|
+
properties: {
|
|
1178
|
+
instance: stepInstanceSysId,
|
|
1179
|
+
input_name: key,
|
|
1180
|
+
script: JSON.stringify(scriptedSubFields),
|
|
1181
|
+
},
|
|
1182
|
+
})
|
|
1183
|
+
)
|
|
1184
|
+
}
|
|
1129
1185
|
return
|
|
1130
1186
|
}
|
|
1131
1187
|
|
|
@@ -1236,6 +1292,22 @@ async function createVariableRecords(
|
|
|
1236
1292
|
},
|
|
1237
1293
|
})
|
|
1238
1294
|
allRecords.push(record)
|
|
1295
|
+
|
|
1296
|
+
// For TemplateValue with scripted sub-fields (no datapills path),
|
|
1297
|
+
// create sys_hub_input_scripts record alongside the sys_variable_value
|
|
1298
|
+
if (Object.keys(scriptedSubFields).length > 0) {
|
|
1299
|
+
allRecords.push(
|
|
1300
|
+
await factory.createRecord({
|
|
1301
|
+
source: callExpression,
|
|
1302
|
+
table: 'sys_hub_input_scripts',
|
|
1303
|
+
properties: {
|
|
1304
|
+
instance: stepInstanceSysId,
|
|
1305
|
+
input_name: key,
|
|
1306
|
+
script: JSON.stringify(scriptedSubFields),
|
|
1307
|
+
},
|
|
1308
|
+
})
|
|
1309
|
+
)
|
|
1310
|
+
}
|
|
1239
1311
|
})
|
|
1240
1312
|
)
|
|
1241
1313
|
return allRecords
|
package/src/flow/post-install.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PostInstallTask, PostInstallContext } from '@servicenow/sdk-build-core'
|
|
1
|
+
import type { PostInstallTask, PostInstallContext, RecordEntry } from '@servicenow/sdk-build-core'
|
|
2
2
|
|
|
3
3
|
type ActivateFlowsResult = {
|
|
4
4
|
status: string
|
|
@@ -16,8 +16,11 @@ type ActivateFlowsResult = {
|
|
|
16
16
|
}>
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
const FLOW_TABLE = 'sys_hub_flow'
|
|
20
|
+
const ACTION_TABLE = 'sys_hub_action_type_definition'
|
|
21
|
+
|
|
19
22
|
async function activateFlows(context: PostInstallContext): Promise<void> {
|
|
20
|
-
const { instanceClient, logger, config } = context
|
|
23
|
+
const { instanceClient, logger, config, recordIds } = context
|
|
21
24
|
const { scopeId } = config
|
|
22
25
|
|
|
23
26
|
if (!instanceClient) {
|
|
@@ -25,13 +28,28 @@ async function activateFlows(context: PostInstallContext): Promise<void> {
|
|
|
25
28
|
return
|
|
26
29
|
}
|
|
27
30
|
|
|
28
|
-
|
|
31
|
+
const flowAndSubflowIds = recordIds?.[FLOW_TABLE] ?? []
|
|
32
|
+
const actionIds = recordIds?.[ACTION_TABLE] ?? []
|
|
33
|
+
|
|
34
|
+
if (flowAndSubflowIds.length === 0 && actionIds.length === 0) {
|
|
35
|
+
logger.debug('No flows, subflows, or actions found in project, skipping flow activation')
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
logger.debug(`Activating flows... (${flowAndSubflowIds.length} flow/subflow(s), ${actionIds.length} action(s))`)
|
|
29
40
|
|
|
30
41
|
const response = await instanceClient.fetch(
|
|
31
42
|
'api/now/wfa_fluent/activate_flows',
|
|
32
43
|
{
|
|
33
44
|
method: 'POST',
|
|
34
|
-
headers: {
|
|
45
|
+
headers: {
|
|
46
|
+
Accept: 'application/json',
|
|
47
|
+
'Content-Type': 'application/json',
|
|
48
|
+
},
|
|
49
|
+
body: JSON.stringify({
|
|
50
|
+
flows: flowAndSubflowIds,
|
|
51
|
+
actions: actionIds,
|
|
52
|
+
}),
|
|
35
53
|
},
|
|
36
54
|
new URLSearchParams({ sysparm_transaction_scope: scopeId })
|
|
37
55
|
)
|
|
@@ -40,7 +58,7 @@ async function activateFlows(context: PostInstallContext): Promise<void> {
|
|
|
40
58
|
// Any other non-OK status (400, 401, 403, 500, etc.) is an error
|
|
41
59
|
if (!response.ok && response.status !== 422) {
|
|
42
60
|
const body = await response.json().catch(() => null)
|
|
43
|
-
const msg = body?.error?.message ?? body?.message ?? response.statusText
|
|
61
|
+
const msg = body?.result?.error?.message ?? body?.error?.message ?? body?.message ?? response.statusText
|
|
44
62
|
|
|
45
63
|
// Instances without the endpoint return 400 with "does not represent any resource"
|
|
46
64
|
if (msg.includes('does not represent any resource')) {
|
|
@@ -84,6 +102,19 @@ async function activateFlows(context: PostInstallContext): Promise<void> {
|
|
|
84
102
|
logger.warn(msg)
|
|
85
103
|
}
|
|
86
104
|
|
|
105
|
+
export function getRecordIdsByTable(keysRegistry: Now.Internal.KeysRegistry): Record<string, RecordEntry[]> {
|
|
106
|
+
const result: Record<string, RecordEntry[]> = {}
|
|
107
|
+
|
|
108
|
+
for (const entry of Object.values(keysRegistry.explicit)) {
|
|
109
|
+
if (entry.deleted) {
|
|
110
|
+
continue
|
|
111
|
+
}
|
|
112
|
+
const entries = (result[entry.table] ??= [])
|
|
113
|
+
entries.push({ sys_id: entry.id, active: '', state: '' })
|
|
114
|
+
}
|
|
115
|
+
return result
|
|
116
|
+
}
|
|
117
|
+
|
|
87
118
|
export const FlowActivationTask: PostInstallTask = {
|
|
88
119
|
name: 'flow-activation',
|
|
89
120
|
skipFlag: 'skipFlowActivation',
|
|
@@ -850,8 +850,10 @@ function buildCv(typeNode: unknown, valueNode: unknown): unknown {
|
|
|
850
850
|
// Special handling for primitive arrays (schema: ["Any"], value: ["item1", "item2", ...])
|
|
851
851
|
if (Array.isArray(typeNode) && Array.isArray(valueNode)) {
|
|
852
852
|
if (typeNode.length === 1 && typeNode[0] === 'Any') {
|
|
853
|
-
// Primitive array:
|
|
854
|
-
|
|
853
|
+
// Primitive array: preserve native types (string, number, boolean) without $cv wrapping.
|
|
854
|
+
// Do NOT force String(v) — that would convert [10,20,30] to ['10','20','30'] and
|
|
855
|
+
// [true,false] to ['true','false'], breaking the round-trip for IntegerColumn/BooleanColumn.
|
|
856
|
+
return valueNode
|
|
855
857
|
}
|
|
856
858
|
}
|
|
857
859
|
|
|
@@ -5,11 +5,18 @@ import {
|
|
|
5
5
|
Shape,
|
|
6
6
|
VariableStatementShape,
|
|
7
7
|
ObjectShape,
|
|
8
|
+
gunzipSync,
|
|
8
9
|
type Diagnostics,
|
|
9
10
|
type Logger,
|
|
10
11
|
} from '@servicenow/sdk-build-core'
|
|
11
12
|
import { ArrowFunctionShape } from '../../arrow-function-plugin'
|
|
12
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
DO_IN_PARALLEL_BLOCK_SYS_ID,
|
|
15
|
+
FLOW_LOGIC,
|
|
16
|
+
FLOW_LOGIC_PREFIX,
|
|
17
|
+
FLOW_LOGIC_SYS_ID_MAP,
|
|
18
|
+
TRY_CATCH_CATCH_SYS_ID,
|
|
19
|
+
} from '../flow-logic/flow-logic-constants'
|
|
13
20
|
|
|
14
21
|
/**
|
|
15
22
|
* Result of datapill transformation
|
|
@@ -22,7 +29,7 @@ export interface DatapillTransformResult {
|
|
|
22
29
|
/**
|
|
23
30
|
* Information about a flow instance shape for processing
|
|
24
31
|
*/
|
|
25
|
-
|
|
32
|
+
interface FlowInstanceShapeInfo {
|
|
26
33
|
callExpression: CallExpressionShape
|
|
27
34
|
variableStatement: VariableStatementShape | CallExpressionShape
|
|
28
35
|
}
|
|
@@ -57,7 +64,7 @@ export function buildUuidToIdentifierMap(recordToShapeMap: Map<Record, Shape>):
|
|
|
57
64
|
|
|
58
65
|
for (const [record, instanceShape] of recordToShapeMap.entries()) {
|
|
59
66
|
// Extract ui_id from the record (this is what datapills reference)
|
|
60
|
-
const uiId = record.get('ui_id')?.getValue()
|
|
67
|
+
const uiId = record.get('ui_id')?.ifString()?.getValue()
|
|
61
68
|
|
|
62
69
|
if (!uiId) {
|
|
63
70
|
continue
|
|
@@ -66,6 +73,14 @@ export function buildUuidToIdentifierMap(recordToShapeMap: Map<Record, Shape>):
|
|
|
66
73
|
// Handle VariableStatementShape (actions/subflows with variable assignments)
|
|
67
74
|
if (instanceShape instanceof VariableStatementShape) {
|
|
68
75
|
const variableStatement = instanceShape.as(VariableStatementShape)
|
|
76
|
+
const initCallee =
|
|
77
|
+
variableStatement.getInitializer() instanceof CallExpressionShape
|
|
78
|
+
? (variableStatement.getInitializer() as CallExpressionShape).getCallee()
|
|
79
|
+
: undefined
|
|
80
|
+
if (initCallee === FLOW_LOGIC.DO_IN_PARALLEL || initCallee === FLOW_LOGIC.TRY_CATCH) {
|
|
81
|
+
continue
|
|
82
|
+
}
|
|
83
|
+
|
|
69
84
|
const variableName = variableStatement.getVariableName()
|
|
70
85
|
const callExpression = variableStatement.getInitializer().as(CallExpressionShape)
|
|
71
86
|
|
|
@@ -119,6 +134,227 @@ export function buildUuidToIdentifierMap(recordToShapeMap: Map<Record, Shape>):
|
|
|
119
134
|
return uuidToIdentifierMap
|
|
120
135
|
}
|
|
121
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Build a ui_id → ancestor-chain map for the given instances. Each entry's value is the set of all `ui_id`s in the
|
|
139
|
+
* record's ancestor chain, including the record itself. Useful when resolving pills: a source record that lives
|
|
140
|
+
* "inside the same branch" as the target action shares the target's branch `ui_id` in this set, and the bare
|
|
141
|
+
* identifier can be used (no outer-binding dot-walk needed).
|
|
142
|
+
*/
|
|
143
|
+
function buildAncestorChainMap(allInstances: ReadonlyArray<Record>): Map<string, Set<string>> {
|
|
144
|
+
const recordByUiId = new Map<string, Record>()
|
|
145
|
+
|
|
146
|
+
for (const inst of allInstances) {
|
|
147
|
+
const ui = inst.get('ui_id')?.ifString()?.getValue()
|
|
148
|
+
if (ui) {
|
|
149
|
+
recordByUiId.set(ui, inst)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const chainByUiId = new Map<string, Set<string>>()
|
|
154
|
+
|
|
155
|
+
const computeChain = (uiId: string, seen: Set<string>): Set<string> => {
|
|
156
|
+
const cached = chainByUiId.get(uiId)
|
|
157
|
+
if (cached) {
|
|
158
|
+
return cached
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (seen.has(uiId)) {
|
|
162
|
+
return new Set([uiId]) // Cycle guard.
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
seen.add(uiId)
|
|
166
|
+
|
|
167
|
+
const chain = new Set<string>([uiId])
|
|
168
|
+
const rec = recordByUiId.get(uiId)
|
|
169
|
+
const parent = rec?.get('parent_ui_id')?.ifString()?.getValue()
|
|
170
|
+
if (parent) {
|
|
171
|
+
for (const ancestor of computeChain(parent, seen)) {
|
|
172
|
+
chain.add(ancestor)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
chainByUiId.set(uiId, chain)
|
|
177
|
+
return chain
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
for (const inst of allInstances) {
|
|
181
|
+
const ui = inst.get('ui_id')?.ifString()?.getValue()
|
|
182
|
+
if (ui) {
|
|
183
|
+
computeChain(ui, new Set())
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return chainByUiId
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Detect whether any action/subflow nested inside a DoInParallel branch or TryCatch arm has its output
|
|
192
|
+
* referenced from outside that branch/arm. Returns true if hoisting is detected (caller should fall back
|
|
193
|
+
* to Record() API), false otherwise.
|
|
194
|
+
*/
|
|
195
|
+
export function detectHoistedOutputBindings(allInstances: ReadonlyArray<Record>): boolean {
|
|
196
|
+
if (allInstances.length === 0) {
|
|
197
|
+
return false
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const tryCatchLogicId = FLOW_LOGIC_SYS_ID_MAP[FLOW_LOGIC.TRY_CATCH]
|
|
201
|
+
|
|
202
|
+
// Collect ui_ids of all DoInParallel branches and TryCatch arms.
|
|
203
|
+
const branchUiIds = new Set<string>()
|
|
204
|
+
for (const inst of allInstances) {
|
|
205
|
+
if (inst.getTable() !== 'sys_hub_flow_logic_instance_v2') {
|
|
206
|
+
continue
|
|
207
|
+
}
|
|
208
|
+
const logicDef = inst.get('logic_definition')?.ifString()?.getValue()
|
|
209
|
+
if (
|
|
210
|
+
logicDef === DO_IN_PARALLEL_BLOCK_SYS_ID ||
|
|
211
|
+
logicDef === TRY_CATCH_CATCH_SYS_ID ||
|
|
212
|
+
(tryCatchLogicId && logicDef === tryCatchLogicId)
|
|
213
|
+
) {
|
|
214
|
+
const uiId = inst.get('ui_id')?.ifString()?.getValue()
|
|
215
|
+
if (uiId) {
|
|
216
|
+
branchUiIds.add(uiId)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (branchUiIds.size === 0) {
|
|
222
|
+
return false
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Build ancestor chain map and reference index.
|
|
226
|
+
const ancestorChainMap = buildAncestorChainMap(allInstances)
|
|
227
|
+
const referenceIndex = buildReferenceIndex(allInstances)
|
|
228
|
+
|
|
229
|
+
// Map: action/subflow UUID → the branch/arm ui_id that contains it (directly or transitively).
|
|
230
|
+
// Uses the full ancestor chain so actions nested inside If/ForEach within a branch are also caught.
|
|
231
|
+
const innerActionBranch = new Map<string, string>()
|
|
232
|
+
for (const [uuid, record] of uuidToRecordMap.entries()) {
|
|
233
|
+
const uiId = record.get('ui_id')?.ifString()?.getValue()
|
|
234
|
+
if (!uiId) {
|
|
235
|
+
continue
|
|
236
|
+
}
|
|
237
|
+
const chain = ancestorChainMap.get(uiId)
|
|
238
|
+
if (!chain) {
|
|
239
|
+
continue
|
|
240
|
+
}
|
|
241
|
+
for (const branchUiId of branchUiIds) {
|
|
242
|
+
if (chain.has(branchUiId)) {
|
|
243
|
+
innerActionBranch.set(uuid, branchUiId)
|
|
244
|
+
break
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (innerActionBranch.size === 0) {
|
|
250
|
+
return false
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// For each inner action UUID, check if any source record referencing it lives outside its branch.
|
|
254
|
+
for (const [uuid, branchUiId] of innerActionBranch.entries()) {
|
|
255
|
+
const sources = referenceIndex.get(uuid)
|
|
256
|
+
if (!sources) {
|
|
257
|
+
continue
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
for (const sourceUiId of sources) {
|
|
261
|
+
if (!sourceUiId) {
|
|
262
|
+
continue
|
|
263
|
+
}
|
|
264
|
+
const ancestorChain = ancestorChainMap.get(sourceUiId)
|
|
265
|
+
const isInside = ancestorChain ? ancestorChain.has(branchUiId) : false
|
|
266
|
+
if (!isInside) {
|
|
267
|
+
return true
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return false
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function buildReferenceIndex(allInstances: ReadonlyArray<Record>): Map<string, Set<string | undefined>> {
|
|
276
|
+
const result = new Map<string, Set<string | undefined>>()
|
|
277
|
+
const uuidRe = /\{\{(?:step\[)?([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/g
|
|
278
|
+
for (const inst of allInstances) {
|
|
279
|
+
const valuesStr = readValuesForPillScan(inst)
|
|
280
|
+
if (!valuesStr) {
|
|
281
|
+
continue
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const sourceUiId = inst.get('ui_id')?.ifString()?.getValue()
|
|
285
|
+
for (const m of valuesStr.matchAll(uuidRe)) {
|
|
286
|
+
const uuid = m[1]
|
|
287
|
+
if (!uuid) {
|
|
288
|
+
continue
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
let sources = result.get(uuid)
|
|
292
|
+
if (!sources) {
|
|
293
|
+
sources = new Set<string | undefined>()
|
|
294
|
+
result.set(uuid, sources)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
sources.add(sourceUiId)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return result
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Read an instance record's `values` column for substring-search of datapill UUIDs. ServiceNow stores the values field
|
|
305
|
+
* as a gzipped+base64 string. Returns the decompressed JSON-stringified payload, or `undefined` if the column is
|
|
306
|
+
* missing / unreadable. Errors are swallowed because pill-scan failure is non-critical — the worst case is
|
|
307
|
+
* over-keeping a binding.
|
|
308
|
+
*/
|
|
309
|
+
// Memoizes the decoded `values` column per record. Decoding gzips (via `gunzipSync`) which is expensive, and the same
|
|
310
|
+
// records are scanned repeatedly — once by `buildReferenceIndex`, once per catch arm in `buildCatchParamMap`, etc.
|
|
311
|
+
// Keyed by Record identity (WeakMap), so entries are collected once a flow's records go out of scope.
|
|
312
|
+
const valuesScanCache = new WeakMap<Record, string | undefined>()
|
|
313
|
+
|
|
314
|
+
function readValuesForPillScan(record: Record): string | undefined {
|
|
315
|
+
const cached = valuesScanCache.get(record)
|
|
316
|
+
if (cached !== undefined || valuesScanCache.has(record)) {
|
|
317
|
+
return cached
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const decoded = decodeValuesForPillScan(record)
|
|
321
|
+
valuesScanCache.set(record, decoded)
|
|
322
|
+
|
|
323
|
+
return decoded
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function decodeValuesForPillScan(record: Record): string | undefined {
|
|
327
|
+
const raw = record.get('values')?.getValue()
|
|
328
|
+
if (raw === undefined || raw === null) {
|
|
329
|
+
return undefined
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (typeof raw !== 'string') {
|
|
333
|
+
// Already deserialized — stringify so callers can do substring search.
|
|
334
|
+
try {
|
|
335
|
+
return JSON.stringify(raw)
|
|
336
|
+
} catch {
|
|
337
|
+
return undefined
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (raw === '') {
|
|
342
|
+
return ''
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// gzip magic bytes are `H4sI` when base64-encoded. `gunzipSync` (fflate) takes/returns a Uint8Array, so decode the
|
|
346
|
+
// base64 to bytes on the way in and back to utf8 on the way out.
|
|
347
|
+
if (raw.startsWith('H4sI')) {
|
|
348
|
+
try {
|
|
349
|
+
return Buffer.from(gunzipSync(Buffer.from(raw, 'base64'))).toString('utf8')
|
|
350
|
+
} catch {
|
|
351
|
+
return undefined
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return raw
|
|
356
|
+
}
|
|
357
|
+
|
|
122
358
|
/**
|
|
123
359
|
* Extract CallExpression and VariableStatement from an instance shape
|
|
124
360
|
* Handles both VariableStatementShape (for Action/Subflow) and bare CallExpressionShape (for flow logic)
|
|
@@ -126,7 +362,7 @@ export function buildUuidToIdentifierMap(recordToShapeMap: Map<Record, Shape>):
|
|
|
126
362
|
* @param instanceShape - The shape to extract from
|
|
127
363
|
* @returns Object containing callExpression and variableStatement
|
|
128
364
|
*/
|
|
129
|
-
|
|
365
|
+
function extractInstanceShapeInfo(instanceShape: Shape): FlowInstanceShapeInfo {
|
|
130
366
|
let callExpression: CallExpressionShape
|
|
131
367
|
let variableStatement: VariableStatementShape | CallExpressionShape
|
|
132
368
|
|
|
@@ -141,32 +377,6 @@ export function extractInstanceShapeInfo(instanceShape: Shape): FlowInstanceShap
|
|
|
141
377
|
return { callExpression, variableStatement }
|
|
142
378
|
}
|
|
143
379
|
|
|
144
|
-
/**
|
|
145
|
-
* Rebuild a CallExpressionShape with transformed properties
|
|
146
|
-
* Creates a new CallExpression with the transformed props and cleaned config
|
|
147
|
-
*
|
|
148
|
-
* @param originalCallExpression - The original CallExpression
|
|
149
|
-
* @param transformedProps - The transformed properties object
|
|
150
|
-
* @param configShape - The config shape (possibly cleaned)
|
|
151
|
-
* @param record - The source record for shape creation
|
|
152
|
-
* @returns New CallExpressionShape with transformed properties
|
|
153
|
-
*/
|
|
154
|
-
export function rebuildCallExpressionWithDatapills(
|
|
155
|
-
originalCallExpression: CallExpressionShape,
|
|
156
|
-
transformedProps: { [key: string]: Shape },
|
|
157
|
-
configShape: Shape,
|
|
158
|
-
record: Record
|
|
159
|
-
): CallExpressionShape {
|
|
160
|
-
const originalActionDef = originalCallExpression.getArgument(0, false)
|
|
161
|
-
const newPropsArg = Shape.from(record, transformedProps)
|
|
162
|
-
|
|
163
|
-
return new CallExpressionShape({
|
|
164
|
-
source: originalCallExpression.getSource(),
|
|
165
|
-
callee: originalCallExpression.getCallee(),
|
|
166
|
-
args: [originalActionDef, configShape, newPropsArg],
|
|
167
|
-
})
|
|
168
|
-
}
|
|
169
|
-
|
|
170
380
|
/**
|
|
171
381
|
* Rebuild an instance shape (VariableStatement or CallExpression) with transformed datapills
|
|
172
382
|
*
|
|
@@ -175,7 +385,7 @@ export function rebuildCallExpressionWithDatapills(
|
|
|
175
385
|
* @param instanceShapeInfo - Information extracted from the instance shape
|
|
176
386
|
* @returns Rebuilt VariableStatementShape or the new CallExpression
|
|
177
387
|
*/
|
|
178
|
-
|
|
388
|
+
function rebuildInstanceWithDatapills(
|
|
179
389
|
instanceShape: Shape,
|
|
180
390
|
newCallExpression: CallExpressionShape,
|
|
181
391
|
instanceShapeInfo: FlowInstanceShapeInfo
|
|
@@ -203,7 +413,7 @@ export function rebuildInstanceWithDatapills(
|
|
|
203
413
|
* @param callee - The callee string (e.g., 'wfa.flowLogic.if', 'wfa.action')
|
|
204
414
|
* @returns true if the callee is a flow logic call
|
|
205
415
|
*/
|
|
206
|
-
|
|
416
|
+
function isFlowLogicCallee(callee: string): boolean {
|
|
207
417
|
return callee.startsWith(FLOW_LOGIC_PREFIX)
|
|
208
418
|
}
|
|
209
419
|
|
|
@@ -212,7 +422,7 @@ export function isFlowLogicCallee(callee: string): boolean {
|
|
|
212
422
|
* @param callee - The flow logic callee (e.g., 'wfa.flowLogic.if')
|
|
213
423
|
* @returns true if the flow logic has a body
|
|
214
424
|
*/
|
|
215
|
-
|
|
425
|
+
function flowLogicHasBody(callee: string): boolean {
|
|
216
426
|
// Flow logic with bodies: if, elseIf, else, forEach, tryCatch, doInParallel
|
|
217
427
|
// Flow logic without bodies: goBackTo, waitForADuration, exitLoop, endFlow, skipIteration
|
|
218
428
|
return [
|
|
@@ -237,7 +447,7 @@ export function flowLogicHasBody(callee: string): boolean {
|
|
|
237
447
|
* @param callee - The flow logic callee
|
|
238
448
|
* @returns Array of arrow function shapes
|
|
239
449
|
*/
|
|
240
|
-
|
|
450
|
+
function extractFlowLogicBodies(callExpression: CallExpressionShape, callee: string): ArrowFunctionShape[] {
|
|
241
451
|
const bodies: ArrowFunctionShape[] = []
|
|
242
452
|
|
|
243
453
|
if (callee === FLOW_LOGIC.TRY_CATCH) {
|
|
@@ -288,7 +498,7 @@ export function extractFlowLogicBodies(callExpression: CallExpressionShape, call
|
|
|
288
498
|
* @param record - The source record
|
|
289
499
|
* @returns Rebuilt CallExpressionShape
|
|
290
500
|
*/
|
|
291
|
-
|
|
501
|
+
function rebuildFlowLogicWithDatapills(
|
|
292
502
|
callExpression: CallExpressionShape,
|
|
293
503
|
transformedConfig: ObjectShape,
|
|
294
504
|
transformedBodies: ArrowFunctionShape[],
|
|
@@ -544,12 +754,14 @@ export function processInstanceForDatapills(
|
|
|
544
754
|
}
|
|
545
755
|
}
|
|
546
756
|
|
|
547
|
-
// Rebuild body if children changed
|
|
757
|
+
// Rebuild body if children changed. Preserve the arrow's `returnValue` if present.
|
|
758
|
+
const returnValue = body.getReturnValue()
|
|
548
759
|
const transformedBody = anyChildChanged
|
|
549
760
|
? new ArrowFunctionShape({
|
|
550
761
|
source: body.getSource(),
|
|
551
762
|
parameters: body.getParameters(),
|
|
552
763
|
statements: transformedChildren,
|
|
764
|
+
...(returnValue !== undefined && { returnValue }),
|
|
553
765
|
})
|
|
554
766
|
: body
|
|
555
767
|
|
|
@@ -194,6 +194,16 @@ export async function buildVariableRecords(options: {
|
|
|
194
194
|
additionalProperties['reference'] = referenceTable
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
|
+
|
|
198
|
+
// Handle field_list and template_value columns - extract dependent field.
|
|
199
|
+
// Always write all three fields so that removing 'dependent' from fluent
|
|
200
|
+
// explicitly clears the old value in ServiceNow rather than leaving it stale.
|
|
201
|
+
if (internal_type === 'field_list' || internal_type === 'template_value') {
|
|
202
|
+
const dependent = config.get('dependent')?.ifString()?.getValue() ?? ''
|
|
203
|
+
additionalProperties['dependent'] = dependent
|
|
204
|
+
additionalProperties['dependent_on_field'] = dependent
|
|
205
|
+
additionalProperties['use_dependent_field'] = dependent !== ''
|
|
206
|
+
}
|
|
197
207
|
additionalProperties['internal_type'] = internal_type
|
|
198
208
|
additionalProperties['max_length'] =
|
|
199
209
|
config.get('maxLength')?.ifNumber()?.getValue() ?? getDefaultMaxLength(internal_type)
|
|
@@ -400,7 +410,11 @@ function buildBasicVariableShape(
|
|
|
400
410
|
// Add dependent field for template_value and field_list column types
|
|
401
411
|
if (fieldType === 'template_value' || fieldType === 'field_list') {
|
|
402
412
|
const dependent = variable.get('dependent')?.ifString()?.getValue()
|
|
403
|
-
|
|
413
|
+
// For field_list, dependent is optional — only emit it when non-empty.
|
|
414
|
+
// For template_value, dependent is required by its type, so always include it.
|
|
415
|
+
if (fieldType === 'template_value' || dependent) {
|
|
416
|
+
additionalProps['dependent'] = dependent ?? ''
|
|
417
|
+
}
|
|
404
418
|
}
|
|
405
419
|
|
|
406
420
|
return new CallExpressionShape({
|
|
@@ -521,7 +535,7 @@ function validateAndPopulateChoiceProperties(
|
|
|
521
535
|
}
|
|
522
536
|
|
|
523
537
|
// Default to dropdown_with_none
|
|
524
|
-
let choiceValue = ChoiceDropdown.
|
|
538
|
+
let choiceValue = ChoiceDropdown.DROPDOWN_WITH_NONE
|
|
525
539
|
|
|
526
540
|
// Handle dropdown type configuration
|
|
527
541
|
const defaultValue = config.get('default')
|
|
@@ -531,23 +545,23 @@ function validateAndPopulateChoiceProperties(
|
|
|
531
545
|
const choiceTypeValue = choiceTypeNode.getValue() as choiceDropdownType
|
|
532
546
|
const choiceDropdownValue = choiceDropdown.indexOf(choiceTypeValue)
|
|
533
547
|
|
|
534
|
-
if (choiceDropdownValue ===
|
|
535
|
-
//
|
|
536
|
-
if (defaultValue.isUndefined()) {
|
|
537
|
-
diagnostics.error(config, 'Default value is required for dropdown type "dropdown_without_none"')
|
|
538
|
-
return
|
|
539
|
-
}
|
|
540
|
-
choiceValue = ChoiceDropdown.DROPDOWN_WITHOUT_NONE
|
|
541
|
-
} else if (choiceDropdownValue !== ChoiceDropdown.DROPDOWN_WITH_NONE) {
|
|
542
|
-
// Only dropdown_with_none and dropdown_without_none are valid
|
|
548
|
+
if (choiceDropdownValue === -1) {
|
|
549
|
+
// Not a recognized dropdown type
|
|
543
550
|
diagnostics.error(
|
|
544
551
|
choiceTypeNode,
|
|
545
|
-
|
|
552
|
+
`Invalid dropdown type "${choiceTypeValue}", expected one of: ${choiceDropdown.join(', ')}`
|
|
546
553
|
)
|
|
547
554
|
return
|
|
548
|
-
} else {
|
|
549
|
-
choiceValue = choiceDropdownValue
|
|
550
555
|
}
|
|
556
|
+
|
|
557
|
+
// dropdown_without_none has no "--None--" entry, so a default value is required
|
|
558
|
+
if (choiceDropdownValue === ChoiceDropdown.DROPDOWN_WITHOUT_NONE && defaultValue.isUndefined()) {
|
|
559
|
+
diagnostics.error(config, 'Default value is required for dropdown type "dropdown_without_none"')
|
|
560
|
+
return
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Preserve whatever valid dropdown type came through (none/with_none/suggestion/without_none)
|
|
564
|
+
choiceValue = choiceDropdownValue
|
|
551
565
|
}
|
|
552
566
|
|
|
553
567
|
// Set the choice value
|