@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
@@ -0,0 +1,138 @@
1
+ import { CallExpressionShape, Plugin } from '@servicenow/sdk-build-core'
2
+ import { NowIdShape } from '../now-id-plugin'
3
+
4
+ const SYS_RETRY_POLICY = 'sys_retry_policy'
5
+
6
+ const DEFAULT_CONNECTION_TYPE = 'http_retry_conditions'
7
+ const DEFAULT_RETRY_STRATEGY = 'fixed_time_interval'
8
+ const MAX_ELAPSED_TIME_SECONDS = 86400
9
+ const DEFAULT_RESTRICT_TO = 'http_method,status_code,error,response_body,response_headers'
10
+
11
+ export const RetryPolicyPlugin = Plugin.create({
12
+ name: 'RetryPolicyPlugin',
13
+
14
+ records: {
15
+ [SYS_RETRY_POLICY]: {
16
+ async toShape(record) {
17
+ const restrictToRaw = record.get('restrict_to')?.ifString()?.getValue()
18
+ const restrictToArray =
19
+ restrictToRaw === undefined || restrictToRaw === DEFAULT_RESTRICT_TO
20
+ ? undefined
21
+ : restrictToRaw === ''
22
+ ? []
23
+ : restrictToRaw
24
+ .split(',')
25
+ .map((s) => s.trim())
26
+ .filter(Boolean)
27
+
28
+ return {
29
+ success: true,
30
+ value: new CallExpressionShape({
31
+ source: record,
32
+ callee: 'RetryPolicy',
33
+ args: [
34
+ record.transform(({ $ }) => ({
35
+ $id: $.val(NowIdShape.from(record)),
36
+ name: $.def(''),
37
+ connectionType: $.from('connection_type').def(DEFAULT_CONNECTION_TYPE),
38
+ retryStrategy: $.from('retry_strategy').def(DEFAULT_RETRY_STRATEGY),
39
+ count: $.map((v) => v.ifString()?.ifNotEmpty()?.toNumber()),
40
+ interval: $.map((v) => v.ifString()?.ifNotEmpty()?.toNumber()),
41
+ maxElapsedTime: $.from('max_elapsed_time').map((v) =>
42
+ v.ifString()?.ifNotEmpty()?.toNumber()
43
+ ),
44
+ condition: $.def(''),
45
+ restrictTo: $.val(restrictToArray),
46
+ protectionPolicy: $.from('sys_policy').def(''),
47
+ })),
48
+ ],
49
+ }),
50
+ }
51
+ },
52
+ },
53
+ },
54
+
55
+ shapes: [
56
+ {
57
+ shape: CallExpressionShape,
58
+ fileTypes: ['fluent'],
59
+ async toRecord(callExpression, { factory, diagnostics }) {
60
+ if (callExpression.getCallee() !== 'RetryPolicy') {
61
+ return { success: false }
62
+ }
63
+
64
+ const arg = callExpression.getArgument(0).asObject()
65
+
66
+ // Runtime validation for maxElapsedTime upper bound (TypeScript cannot enforce numeric ranges)
67
+ const maxElapsedTimeShape = arg.get('maxElapsedTime')
68
+ const maxElapsedTime = maxElapsedTimeShape.ifNumber()?.getValue()
69
+ if (maxElapsedTime !== undefined && maxElapsedTime > MAX_ELAPSED_TIME_SECONDS) {
70
+ diagnostics.error(
71
+ maxElapsedTimeShape,
72
+ `maxElapsedTime must not exceed ${MAX_ELAPSED_TIME_SECONDS} seconds (24 hours). Received: ${maxElapsedTime}.`
73
+ )
74
+ }
75
+
76
+ const countShape = arg.get('count')
77
+ const count = countShape.ifNumber()?.getValue()
78
+ if (count !== undefined && !Number.isInteger(count)) {
79
+ diagnostics.error(countShape, `count must be an integer. Received: ${count}.`)
80
+ }
81
+
82
+ const intervalShape = arg.get('interval')
83
+ const interval = intervalShape.ifNumber()?.getValue()
84
+ if (interval !== undefined && !Number.isInteger(interval)) {
85
+ diagnostics.error(intervalShape, `interval must be an integer. Received: ${interval}.`)
86
+ }
87
+
88
+ // Convert restrictTo string[] → comma-separated string for the DB
89
+ const restrictToShape = arg.get('restrictTo')
90
+ const restrictToArrayShape = restrictToShape.ifArray()
91
+ const restrictToCsv = restrictToArrayShape
92
+ ? restrictToArrayShape
93
+ .getElements()
94
+ .map((el) => el.ifString()?.getValue() ?? '')
95
+ .filter(Boolean)
96
+ .join(',')
97
+ : DEFAULT_RESTRICT_TO
98
+
99
+ // Validate condition only references fields in restrictTo
100
+ const allowedFields = (restrictToCsv || DEFAULT_RESTRICT_TO).split(',')
101
+ const conditionShape = arg.get('condition')
102
+ const conditionValue = conditionShape.ifString()?.ifNotEmpty()?.getValue()
103
+ if (conditionValue) {
104
+ for (const part of conditionValue.split('^')) {
105
+ const fieldPart = part.startsWith('OR') ? part.slice(2) : part
106
+ if (!allowedFields.some((field) => fieldPart.startsWith(field))) {
107
+ const fieldName = fieldPart.match(/^[a-z_]+/)?.[0] ?? fieldPart
108
+ diagnostics.error(
109
+ conditionShape,
110
+ `Condition references field '${fieldName}' which is not in restrictTo. Allowed fields: ${allowedFields.join(', ')}.`
111
+ )
112
+ break
113
+ }
114
+ }
115
+ }
116
+
117
+ const record = await factory.createRecord({
118
+ source: callExpression,
119
+ table: SYS_RETRY_POLICY,
120
+ explicitId: arg.get('$id'),
121
+ properties: arg.transform(({ $ }) => ({
122
+ name: $.def(''),
123
+ connection_type: $.from('connectionType').def(DEFAULT_CONNECTION_TYPE),
124
+ retry_strategy: $.from('retryStrategy').def(DEFAULT_RETRY_STRATEGY),
125
+ count: $,
126
+ interval: $,
127
+ max_elapsed_time: $.from('maxElapsedTime'),
128
+ condition: $.def(''),
129
+ restrict_to: $.val(restrictToCsv),
130
+ sys_policy: $.from('protectionPolicy').def(''),
131
+ })),
132
+ })
133
+
134
+ return { success: true, value: record }
135
+ },
136
+ },
137
+ ],
138
+ })
@@ -71,6 +71,10 @@ export class ArrowFunctionShape extends Shape {
71
71
  return this.returnValue
72
72
  }
73
73
 
74
+ isImplicitReturn(): boolean {
75
+ return this.implicitReturn
76
+ }
77
+
74
78
  override getCode(): string {
75
79
  const params = `(${this.getParameters()
76
80
  .map((p) => p.getCode())
@@ -192,48 +196,88 @@ export const ArrowFunctionPlugin = Plugin.create({
192
196
  {
193
197
  shape: ArrowFunctionShape,
194
198
  async commit(shape, target, { commit }) {
195
- const arrowFunction = ts.Node.isArrowFunction(target)
199
+ const originalTargetWasArrow = ts.Node.isArrowFunction(target)
200
+ const arrowTarget = originalTargetWasArrow
196
201
  ? target
197
202
  : target.replaceWithText('() => {}').asKindOrThrow(ts.SyntaxKind.ArrowFunction)
198
203
 
199
- const nodeStatements = arrowFunction.getStatements()
200
204
  const shapeStatements = shape.getStatements()
201
205
  const returnValue = shape.getReturnValue()
206
+ const body = arrowTarget.getBody()
202
207
 
203
- if (ts.Node.isArrowFunction(target)) {
204
- const body = target.getBody()
205
- if (!ts.Node.isBlock(body) && shapeStatements.length === 0 && returnValue) {
206
- // Already inline form - commit directly to the expression
208
+ // Preserve concise arrows when the shape is still an implicit return.
209
+ if (
210
+ originalTargetWasArrow &&
211
+ !ts.Node.isBlock(body) &&
212
+ shapeStatements.length === 0 &&
213
+ returnValue &&
214
+ shape.isImplicitReturn()
215
+ ) {
216
+ if (returnValue.is(ObjectShape)) {
217
+ body.replaceWithText(`(${returnValue.getCode()})`)
218
+ } else {
207
219
  await commit(returnValue, body)
208
- return { success: true }
209
220
  }
221
+ return { success: true }
222
+ }
223
+
224
+ if (!ts.Node.isBlock(body)) {
225
+ // ts-morph cannot grow expression-body arrows like `() => ({})` into
226
+ // block bodies with statement APIs, so convert the body to an empty block
227
+ // first and then continue through the normal commit flow below.
228
+ body.replaceWithText('{}')
210
229
  }
211
230
 
212
- const totalExpected = shapeStatements.length + (returnValue ? 1 : 0)
213
- const excess = nodeStatements.length - totalExpected
231
+ const getNonReturnStatements = () =>
232
+ arrowTarget.getStatements().filter((statement) => !ts.Node.isReturnStatement(statement))
233
+
234
+ const existingStatements = getNonReturnStatements()
235
+ const excess = existingStatements.length - shapeStatements.length
214
236
  for (let i = 1; i <= excess; i++) {
215
- arrowFunction.removeStatement(nodeStatements.length - i)
237
+ existingStatements[existingStatements.length - i]?.remove()
216
238
  }
217
239
 
218
240
  for (const [i, statement] of shapeStatements.entries()) {
219
- const targetStatement = nodeStatements[i] ?? arrowFunction.addStatements('undefined')[0]!
220
- await commit(statement, targetStatement)
241
+ const currentStatements = getNonReturnStatements()
242
+ const targetStatement = currentStatements[i]
243
+ if (targetStatement) {
244
+ await commit(statement, targetStatement)
245
+ } else {
246
+ // Insert before `return` so return-only bodies can grow safely.
247
+ const returnIndex = arrowTarget
248
+ .getStatements()
249
+ .findIndex((node) => ts.Node.isReturnStatement(node))
250
+ const inserted =
251
+ returnIndex >= 0
252
+ ? arrowTarget.insertStatements(returnIndex, statement.getCode())[0]!
253
+ : arrowTarget.addStatements(statement.getCode())[0]!
254
+ await commit(statement, inserted)
255
+ }
221
256
  }
222
257
 
223
- if (returnValue) {
224
- const returnIndex = shapeStatements.length
225
- const targetReturn =
226
- nodeStatements[returnIndex] ?? arrowFunction.addStatements('return undefined')[0]!
227
- if (ts.Node.isReturnStatement(targetReturn)) {
228
- const expr = targetReturn.getExpression()
229
- if (expr) {
230
- await commit(returnValue, expr)
231
- }
258
+ let existingReturn = arrowTarget
259
+ .getStatements()
260
+ .find((statement) => ts.Node.isReturnStatement(statement))
261
+ if (!returnValue) {
262
+ existingReturn?.remove()
263
+ return { success: true }
264
+ }
265
+
266
+ if (!existingReturn) {
267
+ const inserted = arrowTarget.addStatements(`return ${returnValue.getCode()}`)[0]!
268
+ existingReturn = ts.Node.isReturnStatement(inserted)
269
+ ? inserted
270
+ : arrowTarget.getStatements().find((statement) => ts.Node.isReturnStatement(statement))
271
+ }
272
+
273
+ if (existingReturn) {
274
+ const expr = existingReturn.getExpression()
275
+ if (expr) {
276
+ await commit(returnValue, expr)
232
277
  } else {
233
- targetReturn.replaceWithText(`return ${returnValue.getCode()}`)
278
+ existingReturn.replaceWithText(`return ${returnValue.getCode()}`)
234
279
  }
235
280
  }
236
-
237
281
  return { success: true }
238
282
  },
239
283
  },
@@ -242,7 +242,7 @@ export const TestPlugin = Plugin.create({
242
242
  },
243
243
  },
244
244
  sys_element_mapping: {
245
- coalesce: ['field', 'id'],
245
+ coalesce: ['field', 'table', 'id'],
246
246
  },
247
247
  },
248
248
  shapes: [
@@ -85,13 +85,13 @@ export const BasicSyntaxPlugin = Plugin.create({
85
85
  const conditionalDir = NowConfig.getConditionalDirectory(config, source.path)
86
86
  const taxonomySubdir = tableName && taxonomy.mapping[tableName] ? taxonomy.mapping[tableName] : ''
87
87
 
88
- const dir = hostedDir
89
- ? pathModule.join(generatedDir, hostedDir)
90
- : conditionalDir
91
- ? pathModule.join(generatedDir, conditionalDir)
92
- : taxonomySubdir
93
- ? pathModule.join(generatedDir, taxonomySubdir)
94
- : generatedDir
88
+ // The hosted/conditional plugin prefix (e.g. `if/com.<plugin>`) is orthogonal to
89
+ // the taxonomy folder (e.g. `data/table`). Compose them so records keep their
90
+ // taxonomy organization even when they live under a conditional/hosted plugin
91
+ // directory, instead of the plugin prefix overriding the taxonomy (DEF0844422).
92
+ // `pathModule.join` drops empty segments, so any of these may be absent.
93
+ const prefixDir = hostedDir ?? conditionalDir ?? ''
94
+ const dir = pathModule.join(generatedDir, prefixDir, taxonomySubdir)
95
95
 
96
96
  let name = pathModule.basename(source.path).replace(RegExp(`${pathModule.extname(source.path)}$`), '')
97
97
  if (!name) {
@@ -0,0 +1,7 @@
1
+ export {
2
+ addFieldsToColumn,
3
+ COLUMN_API_TO_TYPE,
4
+ COLUMN_TYPE_TO_API,
5
+ getDefaultMaxLength,
6
+ } from './column-helper'
7
+ export { getChoiceRecords, getDocumentationRecords, getLabelForDefaultLanguage } from './column-to-record'
@@ -211,6 +211,8 @@ export const DashboardPlugin = Plugin.create({
211
211
  $id: $.val(NowIdShape.from(record)),
212
212
  name: $,
213
213
  active: $.toBoolean().def(true),
214
+ description: $.def(''),
215
+ certified: $.toBoolean().def(false),
214
216
  tabs: $.val(tabs).def([]),
215
217
  visibilities: $.val(visibilities).def([]),
216
218
  permissions: $.val(permissions).def([]),
@@ -272,6 +274,8 @@ export const DashboardPlugin = Plugin.create({
272
274
  properties: arg.transform(({ $ }) => ({
273
275
  name: $,
274
276
  active: $.def(true),
277
+ description: $.def(''),
278
+ certified: $.def(false),
275
279
  })),
276
280
  })
277
281
 
@@ -0,0 +1,191 @@
1
+ import { CallExpressionShape, Plugin, isSNScope } from '@servicenow/sdk-build-core'
2
+ import { NowIdShape } from './now-id-plugin'
3
+
4
+ const DL_DEFINITION = 'dl_definition'
5
+ const DL_DEFINITION_REL_MATCH = 'dl_definition_rel_match'
6
+ const DL_DEFINITION_REL_SET = 'dl_definition_rel_set'
7
+
8
+ export const DataLookupPlugin = Plugin.create({
9
+ name: 'DataLookupPlugin',
10
+
11
+ records: {
12
+ [DL_DEFINITION]: {
13
+ relationships: {
14
+ [DL_DEFINITION_REL_MATCH]: {
15
+ via: 'dl_definition',
16
+ descendant: true,
17
+ },
18
+ [DL_DEFINITION_REL_SET]: {
19
+ via: 'dl_definition',
20
+ descendant: true,
21
+ },
22
+ },
23
+
24
+ async toShape(record, { descendants }) {
25
+ const matchRecords = descendants
26
+ .query(DL_DEFINITION_REL_MATCH)
27
+ .filter((r) => r.get('dl_definition').equals(record.getId()))
28
+
29
+ const setRecords = descendants
30
+ .query(DL_DEFINITION_REL_SET)
31
+ .filter((r) => r.get('dl_definition').equals(record.getId()))
32
+
33
+ const matchRules = matchRecords.map((r) =>
34
+ r.transform(({ $ }) => ({
35
+ $id: $.val(NowIdShape.from(r)),
36
+ sourceField: $.from('source_table_field'),
37
+ matcherField: $.from('matcher_table_field'),
38
+ exactMatch: $.from('exact_match').toBoolean().def(false),
39
+ }))
40
+ )
41
+
42
+ const setRules = setRecords.map((r) =>
43
+ r.transform(({ $ }) => ({
44
+ $id: $.val(NowIdShape.from(r)),
45
+ targetField: $.from('source_table_field'),
46
+ matcherField: $.from('matcher_table_field'),
47
+ alwaysReplace: $.from('always_replace').toBoolean().def(false),
48
+ }))
49
+ )
50
+
51
+ return {
52
+ success: true,
53
+ value: new CallExpressionShape({
54
+ source: record,
55
+ callee: 'DataLookup',
56
+ args: [
57
+ record.transform(({ $ }) => ({
58
+ $id: $.val(NowIdShape.from(record)),
59
+ name: $,
60
+ sourceTable: $.from('source_table'),
61
+ matcherTable: $.from('matcher_table'),
62
+ active: $.from('active').toBoolean().def(true),
63
+ runOnInsert: $.from('run_on_insert').toBoolean().def(true),
64
+ runOnUpdate: $.from('run_on_update').toBoolean().def(false),
65
+ runOnFormChange: $.from('run_on_form_change').toBoolean().def(true),
66
+ matchRules: $.val(matchRules.length ? matchRules : undefined),
67
+ setRules: $.val(setRules.length ? setRules : undefined),
68
+ protectionPolicy: $.from('sys_policy').def(''),
69
+ })),
70
+ ],
71
+ }),
72
+ }
73
+ },
74
+ },
75
+
76
+ [DL_DEFINITION_REL_MATCH]: {
77
+ // Child records are pulled by the parent dl_definition toShape via descendants.query(),
78
+ // so they must not be transformed independently.
79
+ toShape() {
80
+ return { success: false }
81
+ },
82
+ },
83
+
84
+ [DL_DEFINITION_REL_SET]: {
85
+ // Same as dl_definition_rel_match — rendered inline by the parent's toShape.
86
+ toShape() {
87
+ return { success: false }
88
+ },
89
+ },
90
+ },
91
+
92
+ shapes: [
93
+ {
94
+ shape: CallExpressionShape,
95
+ fileTypes: ['fluent'],
96
+ async toRecord(callExpression, { factory, diagnostics, config }) {
97
+ if (callExpression.getCallee() !== 'DataLookup') {
98
+ return { success: false }
99
+ }
100
+
101
+ const arg = callExpression.getArgument(0).asObject()
102
+
103
+ const name = arg.get('name').ifString()?.getValue()
104
+ if (name && name.length > 40) {
105
+ diagnostics.error(
106
+ arg.get('name'),
107
+ `Data Lookup Definition name must be 40 characters or fewer (got ${name.length}).`
108
+ )
109
+ }
110
+
111
+ const sourceTable = arg.get('sourceTable').ifString()?.getValue() ?? ''
112
+ const matcherTable = arg.get('matcherTable').ifString()?.getValue() ?? ''
113
+
114
+ const scope = config.scope
115
+ if (sourceTable && !sourceTable.startsWith(`${scope}_`) && !isSNScope(scope) && scope !== 'global') {
116
+ diagnostics.error(
117
+ arg.get('sourceTable'),
118
+ `sourceTable '${sourceTable}' must be in the same scope as the data lookup definition. Expected a table starting with '${scope}_'.`
119
+ )
120
+ }
121
+ if (matcherTable && !matcherTable.startsWith(`${scope}_`) && !isSNScope(scope) && scope !== 'global') {
122
+ diagnostics.error(
123
+ arg.get('matcherTable'),
124
+ `matcherTable '${matcherTable}' must be in the same scope as the data lookup definition. Expected a table starting with '${scope}_'.`
125
+ )
126
+ }
127
+
128
+ const mainRecord = await factory.createRecord({
129
+ source: callExpression,
130
+ table: DL_DEFINITION,
131
+ explicitId: arg.get('$id'),
132
+ properties: arg.transform(({ $ }) => ({
133
+ name: $,
134
+ source_table: $.from('sourceTable'),
135
+ matcher_table: $.from('matcherTable'),
136
+ active: $.from('active').def(true),
137
+ run_on_insert: $.from('runOnInsert').def(true),
138
+ run_on_update: $.from('runOnUpdate').def(false),
139
+ run_on_form_change: $.from('runOnFormChange').def(true),
140
+ sys_policy: $.from('protectionPolicy'),
141
+ })),
142
+ })
143
+
144
+ const matchElements = arg.get('matchRules').ifArray()?.getElements() ?? []
145
+ const matchRecords = await Promise.all(
146
+ matchElements.map(async (el) => {
147
+ const rule = el.asObject()
148
+ return factory.createRecord({
149
+ source: el,
150
+ table: DL_DEFINITION_REL_MATCH,
151
+ explicitId: rule.get('$id'),
152
+ properties: rule.transform(({ $ }) => ({
153
+ dl_definition: $.val(mainRecord.getId()),
154
+ source_table_field: $.from('sourceField'),
155
+ matcher_table_field: $.from('matcherField'),
156
+ exact_match: $.from('exactMatch').def(false),
157
+ source_table: $.val(sourceTable),
158
+ matcher_table: $.val(matcherTable),
159
+ })),
160
+ })
161
+ })
162
+ )
163
+
164
+ const setElements = arg.get('setRules').ifArray()?.getElements() ?? []
165
+ const setRecords = await Promise.all(
166
+ setElements.map(async (el) => {
167
+ const rule = el.asObject()
168
+ return factory.createRecord({
169
+ source: el,
170
+ table: DL_DEFINITION_REL_SET,
171
+ explicitId: rule.get('$id'),
172
+ properties: rule.transform(({ $ }) => ({
173
+ dl_definition: $.val(mainRecord.getId()),
174
+ source_table_field: $.from('targetField'),
175
+ matcher_table_field: $.from('matcherField'),
176
+ always_replace: $.from('alwaysReplace').def(false),
177
+ source_table: $.val(sourceTable),
178
+ matcher_table: $.val(matcherTable),
179
+ })),
180
+ })
181
+ })
182
+ )
183
+
184
+ return {
185
+ success: true,
186
+ value: mainRecord.with(...matchRecords, ...setRecords),
187
+ }
188
+ },
189
+ },
190
+ ],
191
+ })
@@ -756,12 +756,13 @@ function validateEndFlowPlacement(expr: FlowLogicInstanceShape): string | undefi
756
756
  FLOW_LOGIC.IF,
757
757
  FLOW_LOGIC.ELSEIF,
758
758
  FLOW_LOGIC.ELSE,
759
+ FLOW_LOGIC.DO_IN_PARALLEL,
759
760
  // TODO: Add FLOW_LOGIC.DO_UNTIL when implemented
760
761
  ]
761
762
 
762
763
  const parentBlock = findAncestorByCalleeName(node, ...END_FLOW_BLOCKS)
763
764
  if (!parentBlock) {
764
- return 'End Flow must be used within a Do Until, For Each, or If block'
765
+ return 'End Flow must be used within a Do In Parallel, Do Until, For Each, or If block'
765
766
  }
766
767
 
767
768
  return undefined
@@ -292,7 +292,6 @@ export const FlowLogicPlugin = Plugin.create({
292
292
  parameters: [],
293
293
  statements: catchStatements,
294
294
  })
295
-
296
295
  const handlersArg = new ObjectShape({
297
296
  source: record,
298
297
  properties: {