@servicenow/sdk-build-plugins 4.7.2 → 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.
Files changed (114) hide show
  1. package/dist/alias/alias-plugin.d.ts +2 -0
  2. package/dist/alias/alias-plugin.js +183 -0
  3. package/dist/alias/alias-plugin.js.map +1 -0
  4. package/dist/alias/alias-template-plugin.d.ts +2 -0
  5. package/dist/alias/alias-template-plugin.js +232 -0
  6. package/dist/alias/alias-template-plugin.js.map +1 -0
  7. package/dist/alias/index.d.ts +3 -0
  8. package/dist/alias/index.js +20 -0
  9. package/dist/alias/index.js.map +1 -0
  10. package/dist/alias/retry-policy-plugin.d.ts +2 -0
  11. package/dist/alias/retry-policy-plugin.js +119 -0
  12. package/dist/alias/retry-policy-plugin.js.map +1 -0
  13. package/dist/arrow-function-plugin.d.ts +1 -0
  14. package/dist/arrow-function-plugin.js +60 -21
  15. package/dist/arrow-function-plugin.js.map +1 -1
  16. package/dist/atf/test-plugin.js +1 -1
  17. package/dist/atf/test-plugin.js.map +1 -1
  18. package/dist/basic-syntax-plugin.js +7 -7
  19. package/dist/basic-syntax-plugin.js.map +1 -1
  20. package/dist/column/index.d.ts +2 -0
  21. package/dist/column/index.js +13 -0
  22. package/dist/column/index.js.map +1 -0
  23. package/dist/dashboard/dashboard-plugin.js +4 -0
  24. package/dist/dashboard/dashboard-plugin.js.map +1 -1
  25. package/dist/data-lookup-plugin.d.ts +2 -0
  26. package/dist/data-lookup-plugin.js +159 -0
  27. package/dist/data-lookup-plugin.js.map +1 -0
  28. package/dist/flow/flow-logic/flow-logic-diagnostics.js +2 -1
  29. package/dist/flow/flow-logic/flow-logic-diagnostics.js.map +1 -1
  30. package/dist/flow/flow-logic/flow-logic-plugin.js.map +1 -1
  31. package/dist/flow/plugins/flow-action-definition-plugin.js +81 -16
  32. package/dist/flow/plugins/flow-action-definition-plugin.js.map +1 -1
  33. package/dist/flow/plugins/flow-definition-plugin.js +70 -7
  34. package/dist/flow/plugins/flow-definition-plugin.js.map +1 -1
  35. package/dist/flow/plugins/flow-instance-plugin.d.ts +35 -1
  36. package/dist/flow/plugins/flow-instance-plugin.js +241 -7
  37. package/dist/flow/plugins/flow-instance-plugin.js.map +1 -1
  38. package/dist/flow/plugins/step-instance-plugin.js +61 -1
  39. package/dist/flow/plugins/step-instance-plugin.js.map +1 -1
  40. package/dist/flow/post-install.d.ts +2 -1
  41. package/dist/flow/post-install.js +31 -4
  42. package/dist/flow/post-install.js.map +1 -1
  43. package/dist/flow/utils/complex-object-resolver.js +4 -2
  44. package/dist/flow/utils/complex-object-resolver.js.map +1 -1
  45. package/dist/flow/utils/datapill-transformer.d.ts +5 -72
  46. package/dist/flow/utils/datapill-transformer.js +199 -28
  47. package/dist/flow/utils/datapill-transformer.js.map +1 -1
  48. package/dist/flow/utils/flow-constants.d.ts +7 -0
  49. package/dist/flow/utils/flow-constants.js +6 -1
  50. package/dist/flow/utils/flow-constants.js.map +1 -1
  51. package/dist/flow/utils/flow-io-to-record.js +24 -15
  52. package/dist/flow/utils/flow-io-to-record.js.map +1 -1
  53. package/dist/flow/utils/flow-shapes.d.ts +8 -2
  54. package/dist/flow/utils/flow-shapes.js +19 -0
  55. package/dist/flow/utils/flow-shapes.js.map +1 -1
  56. package/dist/flow/utils/flow-variable-processor.d.ts +6 -6
  57. package/dist/flow/utils/flow-variable-processor.js +8 -8
  58. package/dist/flow/utils/flow-variable-processor.js.map +1 -1
  59. package/dist/form-plugin.js +35 -24
  60. package/dist/form-plugin.js.map +1 -1
  61. package/dist/index.d.ts +5 -1
  62. package/dist/index.js +6 -1
  63. package/dist/index.js.map +1 -1
  64. package/dist/now-attach-plugin.d.ts +1 -1
  65. package/dist/now-config-plugin.js +2 -1
  66. package/dist/now-config-plugin.js.map +1 -1
  67. package/dist/now-delete-plugin.d.ts +2 -0
  68. package/dist/now-delete-plugin.js +64 -0
  69. package/dist/now-delete-plugin.js.map +1 -0
  70. package/dist/record-plugin.d.ts +10 -0
  71. package/dist/record-plugin.js +15 -1
  72. package/dist/record-plugin.js.map +1 -1
  73. package/dist/repack/lint/Rules.js +17 -7
  74. package/dist/repack/lint/Rules.js.map +1 -1
  75. package/dist/rest-message-plugin.d.ts +2 -0
  76. package/dist/rest-message-plugin.js +331 -0
  77. package/dist/rest-message-plugin.js.map +1 -0
  78. package/dist/script-include-plugin.js +1 -1
  79. package/dist/script-include-plugin.js.map +1 -1
  80. package/dist/server-module-plugin/sbom-builder.js +17 -7
  81. package/dist/server-module-plugin/sbom-builder.js.map +1 -1
  82. package/dist/static-content-plugin.js +17 -7
  83. package/dist/static-content-plugin.js.map +1 -1
  84. package/package.json +7 -6
  85. package/src/alias/alias-plugin.ts +221 -0
  86. package/src/alias/alias-template-plugin.ts +271 -0
  87. package/src/alias/index.ts +3 -0
  88. package/src/alias/retry-policy-plugin.ts +138 -0
  89. package/src/arrow-function-plugin.ts +67 -23
  90. package/src/atf/test-plugin.ts +1 -1
  91. package/src/basic-syntax-plugin.ts +7 -7
  92. package/src/column/index.ts +7 -0
  93. package/src/dashboard/dashboard-plugin.ts +4 -0
  94. package/src/data-lookup-plugin.ts +191 -0
  95. package/src/flow/flow-logic/flow-logic-diagnostics.ts +2 -1
  96. package/src/flow/flow-logic/flow-logic-plugin.ts +0 -1
  97. package/src/flow/plugins/flow-action-definition-plugin.ts +92 -25
  98. package/src/flow/plugins/flow-definition-plugin.ts +114 -8
  99. package/src/flow/plugins/flow-instance-plugin.ts +264 -7
  100. package/src/flow/plugins/step-instance-plugin.ts +74 -2
  101. package/src/flow/post-install.ts +36 -5
  102. package/src/flow/utils/complex-object-resolver.ts +4 -2
  103. package/src/flow/utils/datapill-transformer.ts +248 -36
  104. package/src/flow/utils/flow-constants.ts +8 -0
  105. package/src/flow/utils/flow-io-to-record.ts +28 -14
  106. package/src/flow/utils/flow-shapes.ts +19 -0
  107. package/src/flow/utils/flow-variable-processor.ts +21 -10
  108. package/src/form-plugin.ts +47 -26
  109. package/src/index.ts +5 -1
  110. package/src/now-config-plugin.ts +2 -1
  111. package/src/now-delete-plugin.ts +82 -0
  112. package/src/record-plugin.ts +17 -2
  113. package/src/rest-message-plugin.ts +391 -0
  114. package/src/script-include-plugin.ts +4 -1
@@ -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 { FLOW_LOGIC, FLOW_LOGIC_PREFIX } from '../flow-logic/flow-logic-constants'
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
- export interface FlowInstanceShapeInfo {
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() as string | undefined
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
- export function extractInstanceShapeInfo(instanceShape: Shape): FlowInstanceShapeInfo {
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
- export function rebuildInstanceWithDatapills(
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
- export function isFlowLogicCallee(callee: string): boolean {
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
- export function flowLogicHasBody(callee: string): boolean {
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
- export function extractFlowLogicBodies(callExpression: CallExpressionShape, callee: string): ArrowFunctionShape[] {
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
- export function rebuildFlowLogicWithDatapills(
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
 
@@ -545,6 +545,14 @@ export const ELEMENT_MAPPING_FIELD_ALIASES: { [platformName: string]: string } =
545
545
  __snc_dont_fail_on_error: 'dont_fail_flow_on_error',
546
546
  }
547
547
 
548
+ /**
549
+ * Reverse of ELEMENT_MAPPING_FIELD_ALIASES — maps Fluent API names back to their
550
+ * platform-internal names for serialization into sys_hub_action_instance_v2.
551
+ */
552
+ export const ELEMENT_MAPPING_FIELD_ALIASES_REVERSE: { [fluentName: string]: string } = Object.fromEntries(
553
+ Object.entries(ELEMENT_MAPPING_FIELD_ALIASES).map(([platform, fluent]) => [fluent, platform])
554
+ )
555
+
548
556
  export enum ChoiceDropdown {
549
557
  DROPDOWN_WITH_NONE = 1,
550
558
  DROPDOWN_WITHOUT_NONE = 3,
@@ -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
- additionalProps['dependent'] = dependent ?? ''
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.DROPDOWN_WITHOUT_NONE
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 === ChoiceDropdown.DROPDOWN_WITHOUT_NONE) {
535
- // Validate default value exists for dropdown_without_none
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
- 'Invalid dropdown type, expected "dropdown_with_none" or "dropdown_without_none"'
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
@@ -12,6 +12,25 @@ import type { ApprovalRulesType, ApprovalDueDateType } from '@servicenow/sdk-cor
12
12
  import { approvalRulesJsonToString, approvalRulesStringToJson } from './approval-rules-processor'
13
13
  import { APPROVAL_RULES_API_NAME, APPROVAL_DUE_DATE_API_NAME } from './flow-constants'
14
14
 
15
+ /**
16
+ * Safely extracts a sysId string from a shape that may be either a plain StringShape
17
+ * or a FK-resolved Record. After build+install the SDK resolves FK fields to Record
18
+ * objects, so calling .asString() directly would throw.
19
+ */
20
+ export function getRefSysId(shape: Shape | undefined): string | undefined {
21
+ if (!shape) {
22
+ return undefined
23
+ }
24
+ const str = shape.ifString()?.getValue()
25
+ if (str !== undefined) {
26
+ return str
27
+ }
28
+ if (shape instanceof Record) {
29
+ return shape.getId().getValue()
30
+ }
31
+ return undefined
32
+ }
33
+
15
34
  /**
16
35
  * Abstract base class for all flow definition instances.
17
36
  * Extends CallExpressionShape to provide common functionality for flow constructs.
@@ -7,9 +7,9 @@ import { buildVariableRecords } from './flow-io-to-record'
7
7
  export type VariableTableType = 'sys_hub_flow_input' | 'sys_hub_flow_output' | 'sys_hub_flow_variable'
8
8
 
9
9
  /**
10
- * Parent table type (always sys_hub_flow for flows)
10
+ * Parent table type for flow variable/IO records
11
11
  */
12
- export type ParentTableType = 'sys_hub_flow'
12
+ export type ParentTableType = 'sys_hub_flow' | 'sys_hub_flow_snapshot'
13
13
 
14
14
  /**
15
15
  * Process a flow variable configuration (inputs, outputs, or flow variables)
@@ -27,7 +27,8 @@ export async function processVariableConfig(
27
27
  varTable: VariableTableType,
28
28
  parentRecord: Record,
29
29
  factory: Factory,
30
- diagnostics: Diagnostics
30
+ diagnostics: Diagnostics,
31
+ parentTable: ParentTableType = 'sys_hub_flow'
31
32
  ): Promise<Record[]> {
32
33
  if (!config) {
33
34
  return []
@@ -38,7 +39,7 @@ export async function processVariableConfig(
38
39
  parentRecord,
39
40
  factory,
40
41
  diagnostics,
41
- parentTable: 'sys_hub_flow' as ParentTableType,
42
+ parentTable,
42
43
  varTable,
43
44
  })
44
45
 
@@ -58,9 +59,10 @@ export async function processFlowInputs(
58
59
  inputsConfig: ObjectShape | undefined,
59
60
  parentRecord: Record,
60
61
  factory: Factory,
61
- diagnostics: Diagnostics
62
+ diagnostics: Diagnostics,
63
+ parentTable: ParentTableType = 'sys_hub_flow'
62
64
  ): Promise<Record[]> {
63
- return processVariableConfig(inputsConfig, 'sys_hub_flow_input', parentRecord, factory, diagnostics)
65
+ return processVariableConfig(inputsConfig, 'sys_hub_flow_input', parentRecord, factory, diagnostics, parentTable)
64
66
  }
65
67
 
66
68
  /**
@@ -76,9 +78,10 @@ export async function processFlowOutputs(
76
78
  outputsConfig: ObjectShape | undefined,
77
79
  parentRecord: Record,
78
80
  factory: Factory,
79
- diagnostics: Diagnostics
81
+ diagnostics: Diagnostics,
82
+ parentTable: ParentTableType = 'sys_hub_flow'
80
83
  ): Promise<Record[]> {
81
- return processVariableConfig(outputsConfig, 'sys_hub_flow_output', parentRecord, factory, diagnostics)
84
+ return processVariableConfig(outputsConfig, 'sys_hub_flow_output', parentRecord, factory, diagnostics, parentTable)
82
85
  }
83
86
 
84
87
  /**
@@ -94,7 +97,15 @@ export async function processFlowVariables(
94
97
  flowVariablesConfig: ObjectShape | undefined,
95
98
  parentRecord: Record,
96
99
  factory: Factory,
97
- diagnostics: Diagnostics
100
+ diagnostics: Diagnostics,
101
+ parentTable: ParentTableType = 'sys_hub_flow'
98
102
  ): Promise<Record[]> {
99
- return processVariableConfig(flowVariablesConfig, 'sys_hub_flow_variable', parentRecord, factory, diagnostics)
103
+ return processVariableConfig(
104
+ flowVariablesConfig,
105
+ 'sys_hub_flow_variable',
106
+ parentRecord,
107
+ factory,
108
+ diagnostics,
109
+ parentTable
110
+ )
100
111
  }