@servicenow/sdk-build-plugins 4.5.0 → 4.6.0

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 (144) hide show
  1. package/dist/column-plugin.js +3 -7
  2. package/dist/column-plugin.js.map +1 -1
  3. package/dist/flow/flow-logic/flow-logic-diagnostics.js +5 -5
  4. package/dist/flow/flow-logic/flow-logic-diagnostics.js.map +1 -1
  5. package/dist/flow/plugins/flow-action-definition-plugin.js +1229 -54
  6. package/dist/flow/plugins/flow-action-definition-plugin.js.map +1 -1
  7. package/dist/flow/plugins/flow-data-pill-plugin.js +5 -2
  8. package/dist/flow/plugins/flow-data-pill-plugin.js.map +1 -1
  9. package/dist/flow/plugins/flow-definition-plugin.js +16 -42
  10. package/dist/flow/plugins/flow-definition-plugin.js.map +1 -1
  11. package/dist/flow/plugins/flow-diagnostics-plugin.d.ts +2 -2
  12. package/dist/flow/plugins/flow-diagnostics-plugin.js +2 -2
  13. package/dist/flow/plugins/flow-instance-plugin.js +68 -22
  14. package/dist/flow/plugins/flow-instance-plugin.js.map +1 -1
  15. package/dist/flow/plugins/step-definition-plugin.js +2 -1
  16. package/dist/flow/plugins/step-definition-plugin.js.map +1 -1
  17. package/dist/flow/plugins/step-instance-plugin.d.ts +9 -1
  18. package/dist/flow/plugins/step-instance-plugin.js +649 -136
  19. package/dist/flow/plugins/step-instance-plugin.js.map +1 -1
  20. package/dist/flow/plugins/wfa-datapill-plugin.js +20 -5
  21. package/dist/flow/plugins/wfa-datapill-plugin.js.map +1 -1
  22. package/dist/flow/post-install.js +1 -0
  23. package/dist/flow/post-install.js.map +1 -1
  24. package/dist/flow/utils/complex-object-resolver.js +4 -1
  25. package/dist/flow/utils/complex-object-resolver.js.map +1 -1
  26. package/dist/flow/utils/complex-objects.js +1 -1
  27. package/dist/flow/utils/complex-objects.js.map +1 -1
  28. package/dist/flow/utils/flow-constants.d.ts +66 -2
  29. package/dist/flow/utils/flow-constants.js +402 -6
  30. package/dist/flow/utils/flow-constants.js.map +1 -1
  31. package/dist/flow/utils/flow-io-to-record.d.ts +1 -1
  32. package/dist/flow/utils/flow-io-to-record.js +37 -16
  33. package/dist/flow/utils/flow-io-to-record.js.map +1 -1
  34. package/dist/flow/utils/flow-shapes.js +4 -0
  35. package/dist/flow/utils/flow-shapes.js.map +1 -1
  36. package/dist/flow/utils/label-cache-parser.d.ts +9 -2
  37. package/dist/flow/utils/label-cache-parser.js +32 -4
  38. package/dist/flow/utils/label-cache-parser.js.map +1 -1
  39. package/dist/flow/utils/pill-shape-helpers.d.ts +15 -0
  40. package/dist/flow/utils/pill-shape-helpers.js +35 -0
  41. package/dist/flow/utils/pill-shape-helpers.js.map +1 -0
  42. package/dist/flow/utils/pill-string-parser.js +1 -0
  43. package/dist/flow/utils/pill-string-parser.js.map +1 -1
  44. package/dist/flow/utils/schema-to-flow-object.d.ts +6 -1
  45. package/dist/flow/utils/schema-to-flow-object.js +131 -15
  46. package/dist/flow/utils/schema-to-flow-object.js.map +1 -1
  47. package/dist/flow/utils/utils.d.ts +1 -0
  48. package/dist/flow/utils/utils.js +6 -1
  49. package/dist/flow/utils/utils.js.map +1 -1
  50. package/dist/form-plugin.js +7 -9
  51. package/dist/form-plugin.js.map +1 -1
  52. package/dist/inbound-email-action-plugin.d.ts +10 -0
  53. package/dist/inbound-email-action-plugin.js +128 -0
  54. package/dist/inbound-email-action-plugin.js.map +1 -0
  55. package/dist/index.d.ts +4 -0
  56. package/dist/index.js +4 -0
  57. package/dist/index.js.map +1 -1
  58. package/dist/instance-scan-plugin.js +0 -5
  59. package/dist/instance-scan-plugin.js.map +1 -1
  60. package/dist/property-plugin.js +1 -1
  61. package/dist/property-plugin.js.map +1 -1
  62. package/dist/record-plugin.d.ts +7 -0
  63. package/dist/record-plugin.js +10 -2
  64. package/dist/record-plugin.js.map +1 -1
  65. package/dist/rest-api-plugin.js +8 -1
  66. package/dist/rest-api-plugin.js.map +1 -1
  67. package/dist/schedule-script/scheduled-script-plugin.js +8 -3
  68. package/dist/schedule-script/scheduled-script-plugin.js.map +1 -1
  69. package/dist/service-catalog/service-catalog-base.d.ts +18 -18
  70. package/dist/service-catalog/service-catalog-base.js +22 -22
  71. package/dist/service-catalog/service-catalog-base.js.map +1 -1
  72. package/dist/service-portal/header-footer-plugin.d.ts +2 -0
  73. package/dist/service-portal/header-footer-plugin.js +50 -0
  74. package/dist/service-portal/header-footer-plugin.js.map +1 -0
  75. package/dist/service-portal/menu-plugin.js +3 -22
  76. package/dist/service-portal/menu-plugin.js.map +1 -1
  77. package/dist/service-portal/page-plugin.js +3 -24
  78. package/dist/service-portal/page-plugin.js.map +1 -1
  79. package/dist/service-portal/page-route-map-plugin.d.ts +2 -0
  80. package/dist/service-portal/page-route-map-plugin.js +114 -0
  81. package/dist/service-portal/page-route-map-plugin.js.map +1 -0
  82. package/dist/service-portal/portal-plugin.js +21 -8
  83. package/dist/service-portal/portal-plugin.js.map +1 -1
  84. package/dist/service-portal/utils.d.ts +40 -2
  85. package/dist/service-portal/utils.js +283 -2
  86. package/dist/service-portal/utils.js.map +1 -1
  87. package/dist/service-portal/widget-plugin.js +9 -218
  88. package/dist/service-portal/widget-plugin.js.map +1 -1
  89. package/dist/static-content-plugin.js +4 -0
  90. package/dist/static-content-plugin.js.map +1 -1
  91. package/dist/table-plugin.js +190 -26
  92. package/dist/table-plugin.js.map +1 -1
  93. package/dist/ui-action-plugin.js +1 -4
  94. package/dist/ui-action-plugin.js.map +1 -1
  95. package/dist/ui-page-plugin.js +68 -13
  96. package/dist/ui-page-plugin.js.map +1 -1
  97. package/dist/view-plugin.js +8 -3
  98. package/dist/view-plugin.js.map +1 -1
  99. package/dist/workspace-plugin.js +39 -36
  100. package/dist/workspace-plugin.js.map +1 -1
  101. package/package.json +5 -4
  102. package/src/column-plugin.ts +3 -8
  103. package/src/flow/flow-logic/flow-logic-diagnostics.ts +5 -6
  104. package/src/flow/plugins/flow-action-definition-plugin.ts +1581 -61
  105. package/src/flow/plugins/flow-data-pill-plugin.ts +5 -2
  106. package/src/flow/plugins/flow-definition-plugin.ts +12 -47
  107. package/src/flow/plugins/flow-diagnostics-plugin.ts +2 -2
  108. package/src/flow/plugins/flow-instance-plugin.ts +98 -22
  109. package/src/flow/plugins/step-definition-plugin.ts +2 -1
  110. package/src/flow/plugins/step-instance-plugin.ts +772 -156
  111. package/src/flow/plugins/wfa-datapill-plugin.ts +25 -5
  112. package/src/flow/post-install.ts +1 -0
  113. package/src/flow/utils/complex-object-resolver.ts +4 -1
  114. package/src/flow/utils/complex-objects.ts +1 -1
  115. package/src/flow/utils/flow-constants.ts +421 -5
  116. package/src/flow/utils/flow-io-to-record.ts +43 -17
  117. package/src/flow/utils/flow-shapes.ts +4 -0
  118. package/src/flow/utils/label-cache-parser.ts +33 -4
  119. package/src/flow/utils/pill-shape-helpers.ts +42 -0
  120. package/src/flow/utils/pill-string-parser.ts +1 -0
  121. package/src/flow/utils/schema-to-flow-object.ts +183 -15
  122. package/src/flow/utils/utils.ts +12 -1
  123. package/src/form-plugin.ts +1 -3
  124. package/src/inbound-email-action-plugin.ts +145 -0
  125. package/src/index.ts +4 -0
  126. package/src/instance-scan-plugin.ts +0 -5
  127. package/src/property-plugin.ts +4 -1
  128. package/src/record-plugin.ts +14 -4
  129. package/src/rest-api-plugin.ts +7 -1
  130. package/src/schedule-script/scheduled-script-plugin.ts +14 -3
  131. package/src/service-catalog/service-catalog-base.ts +22 -22
  132. package/src/service-portal/header-footer-plugin.ts +57 -0
  133. package/src/service-portal/menu-plugin.ts +1 -23
  134. package/src/service-portal/page-plugin.ts +3 -28
  135. package/src/service-portal/page-route-map-plugin.ts +124 -0
  136. package/src/service-portal/portal-plugin.ts +33 -10
  137. package/src/service-portal/utils.ts +404 -3
  138. package/src/service-portal/widget-plugin.ts +14 -290
  139. package/src/static-content-plugin.ts +3 -0
  140. package/src/table-plugin.ts +226 -36
  141. package/src/ui-action-plugin.ts +1 -8
  142. package/src/ui-page-plugin.ts +76 -13
  143. package/src/view-plugin.ts +10 -4
  144. package/src/workspace-plugin.ts +43 -43
@@ -1,5 +1,163 @@
1
1
  import { NowIncludeShape } from '../now-include-plugin'
2
- import { type Record, Shape, type RecordId, type Diagnostics, type Factory } from '@servicenow/sdk-build-core'
2
+ import {
3
+ type Record,
4
+ Shape,
5
+ type RecordId,
6
+ type Diagnostics,
7
+ type Factory,
8
+ CallExpressionShape,
9
+ isGUID,
10
+ } from '@servicenow/sdk-build-core'
11
+ import { noThrow, reverseObject } from '../utils'
12
+ import { WidgetCategories } from '@servicenow/sdk-core/runtime/service-portal'
13
+ import { NowIdShape } from '../now-id-plugin'
14
+
15
+ function convertOptionSchemaKeys(optionSchema: unknown, convertKey: (key: string) => string): unknown {
16
+ if (!Array.isArray(optionSchema)) {
17
+ return optionSchema
18
+ }
19
+ return optionSchema.map((option: globalThis.Record<string, unknown>) =>
20
+ Object.fromEntries(Object.entries(option).map(([k, v]) => [convertKey(k), v]))
21
+ )
22
+ }
23
+
24
+ const toCamelCase = (key: string) => key.replace(/_([a-z])/g, (_, c: string) => c.toUpperCase())
25
+
26
+ /**
27
+ * Shared toShape implementation for SPWidget and SPHeaderFooter
28
+ */
29
+ export async function createWidgetToShape(
30
+ record: Record,
31
+ descendants: { query: (table: string) => Record[] },
32
+ callee: 'SPWidget' | 'SPHeaderFooter',
33
+ tablePrefix: string = 'sp_widget',
34
+ includeStaticField: boolean = false
35
+ ) {
36
+ const folderName =
37
+ record.get('id')?.asString()?.getValue()?.replace(/[\s-]/g, '_') ||
38
+ record.get('name')?.asString()?.getValue()?.toLowerCase().replace(/[\s-]/g, '_')
39
+ const dependencies = descendants.query('m2m_sp_widget_dependency')
40
+ const angularProviders = descendants.query('m2m_sp_ng_pro_sp_widget')
41
+ const templates = descendants.query('sp_ng_template').map((template) => {
42
+ return {
43
+ $id: template.getId(),
44
+ id: template.get('id'),
45
+ htmlTemplate: template.get('template'),
46
+ }
47
+ })
48
+
49
+ const category = reverseObject(WidgetCategories)[record.get('category').ifString()?.getValue() || '']
50
+ const serverScript = await createIncludeShape(
51
+ record,
52
+ record.get('script'),
53
+ 'server_script',
54
+ 'js',
55
+ folderName,
56
+ tablePrefix
57
+ )
58
+ const clientScript = await createIncludeShape(
59
+ record,
60
+ record.get('client_script'),
61
+ 'client_script',
62
+ 'js',
63
+ folderName,
64
+ tablePrefix
65
+ )
66
+ const css = await createIncludeShape(record, record.get('css'), 'style', 'scss', folderName, tablePrefix)
67
+ const htmlTemplate = await createIncludeShape(
68
+ record,
69
+ record.get('template'),
70
+ 'template',
71
+ 'html',
72
+ folderName,
73
+ tablePrefix
74
+ )
75
+ const linkScript = await createIncludeShape(
76
+ record,
77
+ record.get('link'),
78
+ 'link-script',
79
+ 'js',
80
+ folderName,
81
+ tablePrefix
82
+ )
83
+
84
+ return {
85
+ success: true,
86
+ value: new CallExpressionShape({
87
+ source: record,
88
+ callee,
89
+ args: [
90
+ record.transform(({ $ }) => {
91
+ const baseTransform = {
92
+ $id: $.val(NowIdShape.from(record)),
93
+ name: $,
94
+ category: $.val(category).def('custom'),
95
+ clientScript: $.val(clientScript).def(''),
96
+ serverScript: $.val(serverScript).def(''),
97
+ controllerAs: $.from('controller_as').def('c'),
98
+ htmlTemplate: $.val(htmlTemplate).def(''),
99
+ customCss: $.val(css).def(''),
100
+ dataTable: $.from('data_table')
101
+ .map((v: Shape) => {
102
+ const val = v.ifString()?.getValue()
103
+ return val && val.trim() !== '' ? val : 'sp_instance'
104
+ })
105
+ .def('sp_instance'),
106
+ demoData: $.from('demo_data')
107
+ .map((d: Shape) => {
108
+ const json = d.ifString()?.getValue()
109
+
110
+ const parsed = noThrow(() => json && JSON.parse(json))
111
+ return parsed instanceof Error ? json : parsed
112
+ })
113
+ .def(''),
114
+ description: $.def(''),
115
+ docs: $.def(''),
116
+ fields: $.from('field_list')
117
+ .map((fields: Shape) => {
118
+ const fieldStr = fields.ifString()?.getValue()
119
+ const fieldArray = fieldStr?.split(',').filter((f: string) => f.trim())
120
+ return fieldArray && fieldArray.length > 0 ? fieldArray : undefined
121
+ })
122
+ .def(undefined),
123
+ hasPreview: $.from('has_preview').toBoolean().def(false),
124
+ id: $.def(''),
125
+ internal: $.toBoolean().def(false),
126
+ linkScript: $.val(linkScript).def(''),
127
+ roles: $.map((role: Shape) => {
128
+ const roleStr = role.ifString()?.getValue()
129
+ const roleArray = roleStr?.split(',').filter((r: string) => r.trim())
130
+ return roleArray && roleArray.length > 0 ? roleArray : undefined
131
+ }).def(undefined),
132
+ servicenow: $.toBoolean().def(false),
133
+ optionSchema: $.from('option_schema')
134
+ .map((v: Shape) => {
135
+ const json = v.ifString()?.getValue()
136
+ const parsed = noThrow(() => json && JSON.parse(json))
137
+ const result = parsed instanceof Error ? json : parsed
138
+ return convertOptionSchemaKeys(result, toCamelCase)
139
+ })
140
+ .def(''),
141
+ public: $.toBoolean().def(false),
142
+ dependencies: $.val(
143
+ dependencies.length > 0 ? dependencies.map((dep) => dep.get('sp_dependency')) : undefined
144
+ ),
145
+ angularProviders: $.val(
146
+ angularProviders.length > 0
147
+ ? angularProviders.map((ap) => ap.get('sp_angular_provider'))
148
+ : undefined
149
+ ),
150
+ templates: $.val(templates.length > 0 ? templates : undefined),
151
+ }
152
+
153
+ return includeStaticField
154
+ ? { ...baseTransform, static: $.from('static').toBoolean().def(false) }
155
+ : baseTransform
156
+ }),
157
+ ],
158
+ }),
159
+ }
160
+ }
3
161
 
4
162
  /**
5
163
  * Creates a NowIncludeShape with a custom file path suffix and extension
@@ -9,9 +167,10 @@ export async function createIncludeShape(
9
167
  content: string | Shape,
10
168
  suffix: string,
11
169
  extension: 'js' | 'html' | 'scss',
12
- folderName: string
170
+ folderName: string,
171
+ tablePrefix: string = 'sp_widget'
13
172
  ): Promise<NowIncludeShape> {
14
- const baseName = `sp_widget_${folderName}`
173
+ const baseName = `${tablePrefix}_${folderName}`
15
174
  const includedText = content instanceof Shape ? content.toString().getValue() : content
16
175
 
17
176
  return new NowIncludeShape({
@@ -23,6 +182,248 @@ export async function createIncludeShape(
23
182
 
24
183
  export const DEFAULT_ORDER = 100
25
184
 
185
+ /**
186
+ * Shared toRecord implementation for SPWidget and SPHeaderFooter
187
+ */
188
+ export async function createWidgetToRecord(
189
+ callExpression: CallExpressionShape,
190
+ {
191
+ diagnostics,
192
+ factory,
193
+ config,
194
+ }: {
195
+ diagnostics: Diagnostics
196
+ factory: Factory
197
+ config: { scope: string }
198
+ },
199
+ options: {
200
+ callee: 'SPWidget' | 'SPHeaderFooter'
201
+ table: 'sp_widget' | 'sp_header_footer'
202
+ includeStaticField?: boolean
203
+ getDefaultClientScript: (controller: string) => string
204
+ defaultServerScript: string
205
+ defaultLinkScript: string
206
+ defaultHtmlTemplate: string
207
+ }
208
+ ) {
209
+ const { table, includeStaticField = false } = options
210
+ const widget = callExpression.getArgument(0).asObject()
211
+ const dependencies =
212
+ widget
213
+ .get('dependencies')
214
+ .ifArray()
215
+ ?.map((dep) => (dep.isString() ? dep.getValue() : dep.ifRecord()?.getId()))
216
+ .filter((dep) => dep) ?? []
217
+ const angularProviders =
218
+ widget
219
+ .get('angularProviders')
220
+ .ifArray()
221
+ ?.map((ap) => (ap.isString() ? ap.getValue() : ap.ifRecord()?.getId()))
222
+ .filter((ap) => ap) ?? []
223
+
224
+ const templates: Array<{ $id: string; id: unknown; htmlTemplate: unknown }> =
225
+ widget
226
+ .get('templates')
227
+ .ifArray()
228
+ ?.map((shp) => shp.getValue() as { $id: string; id: unknown; htmlTemplate: unknown }) ?? []
229
+ const widgetId = widget.get('id').ifString()
230
+ if (widgetId && !/^[a-zA-Z0-9_-]+$/g.test(widgetId.getValue())) {
231
+ diagnostics.error(
232
+ widgetId.getOriginalNode(),
233
+ `Invalid value: must contain only alphanumeric, -, or _ characters`
234
+ )
235
+ }
236
+
237
+ const clientScript = widget.get('clientScript')
238
+ const clientScriptValue =
239
+ clientScript instanceof NowIncludeShape ? clientScript.getValue() : clientScript.ifString()?.getValue()
240
+ if (clientScriptValue && clientScriptValue.trim().length > 0) {
241
+ const clientScriptPattern =
242
+ /^(function|api\.controller\s?=\s?function)\s?([$a-z_][$0-9a-z_]*)?\s?\(.*\)\s?\n?{/i
243
+ if (!clientScriptPattern.test(clientScriptValue.trim())) {
244
+ diagnostics.error(
245
+ clientScript.getOriginalNode(),
246
+ `Client controller must contain a JavaScript function. Example: api.controller = function($scope) { ... }`
247
+ )
248
+ }
249
+ }
250
+
251
+ const htmlTemplate = widget.get('htmlTemplate')
252
+ const htmlTemplateValue =
253
+ htmlTemplate instanceof NowIncludeShape ? htmlTemplate.getValue() : htmlTemplate.ifString()?.getValue()
254
+ if (htmlTemplateValue && htmlTemplateValue.length > 0 && htmlTemplateValue.indexOf('href="#"') > 0) {
255
+ diagnostics.error(
256
+ htmlTemplate.getOriginalNode(),
257
+ `Do not use href="#" in the Service Portal, use href="javascript:void(0)" instead`
258
+ )
259
+ }
260
+
261
+ const roles = widget
262
+ .get('roles')
263
+ .ifArray()
264
+ ?.map((role) => {
265
+ if (role.isString()) {
266
+ return role
267
+ }
268
+ if (role.isRecord()) {
269
+ return role.get('name')
270
+ }
271
+
272
+ return undefined
273
+ })
274
+ .filter((role) => role) as Shape[] | undefined
275
+
276
+ if (roles) {
277
+ roles
278
+ .filter((role) => isGUID(role.getValue() as string))
279
+ .forEach((role) =>
280
+ diagnostics.error(
281
+ role!.getOriginalNode(),
282
+ `expecting role names or role records created by the Role or Record plugins, not sys_ids`
283
+ )
284
+ )
285
+ }
286
+
287
+ const controller = widget.get('controllerAs').ifString()?.getValue() || 'c'
288
+ const servicenow =
289
+ (widget.get('servicenow').ifBoolean()?.getValue() &&
290
+ (config.scope.startsWith('sn_') || config.scope.startsWith('snc_'))) ||
291
+ false
292
+
293
+ const widgetRecord = await factory.createRecord({
294
+ source: callExpression,
295
+ table,
296
+ explicitId: widget.get('$id'),
297
+ properties: widget.transform(({ $ }) => {
298
+ const baseTransform = {
299
+ name: $,
300
+ category: $.map((v) => {
301
+ const catKey = v.ifString()?.getValue() || ''
302
+ return WidgetCategories[catKey as keyof typeof WidgetCategories]
303
+ }).def('custom'),
304
+ client_script: $.from('clientScript').def(options.getDefaultClientScript(controller)),
305
+ script: $.from('serverScript').def(options.defaultServerScript),
306
+ controller_as: $.from('controllerAs').def('c'),
307
+ template: $.from('htmlTemplate').def(options.defaultHtmlTemplate),
308
+ css: $.from('customCss').def(''),
309
+ data_table: $.from('dataTable').def('sp_instance'),
310
+ demo_data: $.from('demoData')
311
+ .map((v) => (v.ifString() || v instanceof NowIncludeShape ? v : JSON.stringify(v.getValue())))
312
+ .def(''),
313
+ description: $.def(''),
314
+ docs: $.map((v) => (v.isString() ? v : v.ifRecord()?.getId())),
315
+ field_list: $.from('fields')
316
+ .map((v) => v.getValue()?.toString())
317
+ .def(''),
318
+ has_preview: $.from('hasPreview').toBoolean().def(false),
319
+ id: $.def(''),
320
+ internal: $.def(false),
321
+ servicenow: $.val(servicenow),
322
+ link: $.from('linkScript').def(options.defaultLinkScript),
323
+ roles: $.val(roles?.map((r) => r.getValue()).toString()).def(''),
324
+ option_schema: $.from('optionSchema')
325
+ .map((v) => {
326
+ const json = v.getValue()
327
+ const converted = convertOptionSchemaKeys(json, (key: string) =>
328
+ key.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`)
329
+ )
330
+ return converted && JSON.stringify(converted)
331
+ })
332
+ .def(''),
333
+ public: $.def(false),
334
+ }
335
+
336
+ return includeStaticField
337
+ ? { ...baseTransform, static: $.from('static').toBoolean().def(false) }
338
+ : baseTransform
339
+ }),
340
+ })
341
+
342
+ return {
343
+ success: true,
344
+ value: widgetRecord.with(
345
+ ...(await Promise.all(
346
+ templates.map(async (template) => {
347
+ return await factory.createRecord({
348
+ source: callExpression,
349
+ table: 'sp_ng_template',
350
+ explicitId: template.$id,
351
+ properties: {
352
+ id: template.id,
353
+ sp_widget: widgetRecord.getId(),
354
+ template: template.htmlTemplate,
355
+ },
356
+ })
357
+ })
358
+ )),
359
+ ...(await Promise.all(
360
+ dependencies.map(async (dep) => {
361
+ return await factory.createRecord({
362
+ source: callExpression,
363
+ table: 'm2m_sp_widget_dependency',
364
+ properties: {
365
+ sp_dependency: dep!,
366
+ sp_widget: widgetRecord.getId(),
367
+ },
368
+ })
369
+ })
370
+ )),
371
+ ...(await Promise.all(
372
+ angularProviders.map(async (ap) => {
373
+ return await factory.createRecord({
374
+ source: callExpression,
375
+ table: 'm2m_sp_ng_pro_sp_widget',
376
+ properties: {
377
+ sp_angular_provider: ap!,
378
+ sp_widget: widgetRecord.getId(),
379
+ },
380
+ })
381
+ })
382
+ ))
383
+ ),
384
+ }
385
+ }
386
+
387
+ /**
388
+ * Converts a roles Shape (array of strings, Role() expressions, or Record references)
389
+ * to a deduplicated comma-separated string of role names.
390
+ * @param rolesShape - Shape containing the roles array
391
+ * @param diagnostics - Optional diagnostics for reporting invalid role entries
392
+ * @returns Comma-separated string of unique role names, or empty string if no roles
393
+ */
394
+ export function getRolesString(rolesShape: Shape, diagnostics?: Diagnostics): string {
395
+ const roles = rolesShape
396
+ .ifArray()
397
+ ?.getElements()
398
+ .map((role) => {
399
+ if (role.isString()) {
400
+ return role.getValue()
401
+ }
402
+ if (role instanceof CallExpressionShape) {
403
+ const name = role.getArgument(0).asObject().get('name')
404
+ if (name.isString()) {
405
+ return name.getValue()
406
+ }
407
+ }
408
+ if (role.isRecord()) {
409
+ const name = role.get('name')
410
+ if (name?.isString()) {
411
+ return name.getValue()
412
+ }
413
+ }
414
+ if (diagnostics) {
415
+ diagnostics.error(role, 'roles must be strings or role records')
416
+ }
417
+ return undefined
418
+ })
419
+ .filter((r): r is string => r !== undefined && r.trim() !== '')
420
+
421
+ if (!roles || roles.length === 0) {
422
+ return ''
423
+ }
424
+ return [...new Set(roles)].join(',')
425
+ }
426
+
26
427
  export async function getIncludeRecords(
27
428
  shape: Shape,
28
429
  parentId: Shape,