@servicenow/sdk-build-plugins 4.4.1 → 4.5.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 (287) hide show
  1. package/dist/acl-plugin.js +54 -4
  2. package/dist/acl-plugin.js.map +1 -1
  3. package/dist/applicability-plugin.js +2 -0
  4. package/dist/applicability-plugin.js.map +1 -1
  5. package/dist/application-menu-plugin.js +2 -0
  6. package/dist/application-menu-plugin.js.map +1 -1
  7. package/dist/arrow-function-plugin.d.ts +6 -1
  8. package/dist/arrow-function-plugin.js +105 -12
  9. package/dist/arrow-function-plugin.js.map +1 -1
  10. package/dist/atf/test-plugin.js +2 -0
  11. package/dist/atf/test-plugin.js.map +1 -1
  12. package/dist/basic-syntax-plugin.js +20 -0
  13. package/dist/basic-syntax-plugin.js.map +1 -1
  14. package/dist/call-expression-plugin.js +1 -0
  15. package/dist/call-expression-plugin.js.map +1 -1
  16. package/dist/claims-plugin.js +1 -0
  17. package/dist/claims-plugin.js.map +1 -1
  18. package/dist/client-script-plugin.js +1 -0
  19. package/dist/client-script-plugin.js.map +1 -1
  20. package/dist/column-plugin.js +1 -0
  21. package/dist/column-plugin.js.map +1 -1
  22. package/dist/cross-scope-privilege-plugin.js +1 -0
  23. package/dist/cross-scope-privilege-plugin.js.map +1 -1
  24. package/dist/dashboard/dashboard-plugin.js +2 -0
  25. package/dist/dashboard/dashboard-plugin.js.map +1 -1
  26. package/dist/data-plugin.js +1 -0
  27. package/dist/data-plugin.js.map +1 -1
  28. package/dist/email-notification-plugin.js +9 -13
  29. package/dist/email-notification-plugin.js.map +1 -1
  30. package/dist/flow/constants/flow-plugin-constants.d.ts +1 -1
  31. package/dist/flow/constants/flow-plugin-constants.js +1 -1
  32. package/dist/flow/constants/flow-plugin-constants.js.map +1 -1
  33. package/dist/flow/flow-logic/flow-logic-plugin-helpers.d.ts +82 -2
  34. package/dist/flow/flow-logic/flow-logic-plugin-helpers.js +48 -40
  35. package/dist/flow/flow-logic/flow-logic-plugin-helpers.js.map +1 -1
  36. package/dist/flow/flow-logic/flow-logic-plugin.js +1 -0
  37. package/dist/flow/flow-logic/flow-logic-plugin.js.map +1 -1
  38. package/dist/flow/plugins/approval-rules-plugin.js +1 -0
  39. package/dist/flow/plugins/approval-rules-plugin.js.map +1 -1
  40. package/dist/flow/plugins/flow-action-definition-plugin.js +4 -2
  41. package/dist/flow/plugins/flow-action-definition-plugin.js.map +1 -1
  42. package/dist/flow/plugins/flow-data-pill-plugin.js +1 -0
  43. package/dist/flow/plugins/flow-data-pill-plugin.js.map +1 -1
  44. package/dist/flow/plugins/flow-definition-plugin.js +8 -3
  45. package/dist/flow/plugins/flow-definition-plugin.js.map +1 -1
  46. package/dist/flow/plugins/flow-diagnostics-plugin.js +1 -0
  47. package/dist/flow/plugins/flow-diagnostics-plugin.js.map +1 -1
  48. package/dist/flow/plugins/flow-instance-plugin.js +68 -12
  49. package/dist/flow/plugins/flow-instance-plugin.js.map +1 -1
  50. package/dist/flow/plugins/flow-trigger-instance-plugin.js +1 -0
  51. package/dist/flow/plugins/flow-trigger-instance-plugin.js.map +1 -1
  52. package/dist/flow/plugins/inline-script-plugin.js +1 -0
  53. package/dist/flow/plugins/inline-script-plugin.js.map +1 -1
  54. package/dist/flow/plugins/step-definition-plugin.js +3 -2
  55. package/dist/flow/plugins/step-definition-plugin.js.map +1 -1
  56. package/dist/flow/plugins/step-instance-plugin.js +1 -0
  57. package/dist/flow/plugins/step-instance-plugin.js.map +1 -1
  58. package/dist/flow/plugins/trigger-plugin.js +2 -0
  59. package/dist/flow/plugins/trigger-plugin.js.map +1 -1
  60. package/dist/flow/plugins/wfa-datapill-plugin.js +1 -0
  61. package/dist/flow/plugins/wfa-datapill-plugin.js.map +1 -1
  62. package/dist/flow/post-install.d.ts +2 -0
  63. package/dist/flow/post-install.js +58 -0
  64. package/dist/flow/post-install.js.map +1 -0
  65. package/dist/flow/utils/complex-objects.js +4 -2
  66. package/dist/flow/utils/complex-objects.js.map +1 -1
  67. package/dist/flow/utils/flow-constants.d.ts +24 -0
  68. package/dist/flow/utils/flow-constants.js +29 -2
  69. package/dist/flow/utils/flow-constants.js.map +1 -1
  70. package/dist/flow/utils/flow-to-xml.d.ts +3 -2
  71. package/dist/flow/utils/flow-to-xml.js +3 -4
  72. package/dist/flow/utils/flow-to-xml.js.map +1 -1
  73. package/dist/flow/utils/label-cache-processor.d.ts +5 -0
  74. package/dist/flow/utils/label-cache-processor.js +14 -2
  75. package/dist/flow/utils/label-cache-processor.js.map +1 -1
  76. package/dist/flow/utils/service-catalog.js +5 -1
  77. package/dist/flow/utils/service-catalog.js.map +1 -1
  78. package/dist/form-plugin.d.ts +2 -0
  79. package/dist/form-plugin.js +1134 -0
  80. package/dist/form-plugin.js.map +1 -0
  81. package/dist/html-import-plugin.js +1 -0
  82. package/dist/html-import-plugin.js.map +1 -1
  83. package/dist/import-sets-plugin.js +2 -0
  84. package/dist/import-sets-plugin.js.map +1 -1
  85. package/dist/index.d.ts +9 -0
  86. package/dist/index.js +13 -1
  87. package/dist/index.js.map +1 -1
  88. package/dist/instance-scan-plugin.d.ts +2 -0
  89. package/dist/instance-scan-plugin.js +298 -0
  90. package/dist/instance-scan-plugin.js.map +1 -0
  91. package/dist/json-plugin.js +1 -0
  92. package/dist/json-plugin.js.map +1 -1
  93. package/dist/list-plugin.js +1 -0
  94. package/dist/list-plugin.js.map +1 -1
  95. package/dist/now-attach-plugin.js +1 -0
  96. package/dist/now-attach-plugin.js.map +1 -1
  97. package/dist/now-config-plugin.js +659 -51
  98. package/dist/now-config-plugin.js.map +1 -1
  99. package/dist/now-id-plugin.js +1 -0
  100. package/dist/now-id-plugin.js.map +1 -1
  101. package/dist/now-include-plugin.js +1 -0
  102. package/dist/now-include-plugin.js.map +1 -1
  103. package/dist/now-ref-plugin.js +1 -0
  104. package/dist/now-ref-plugin.js.map +1 -1
  105. package/dist/now-unresolved-plugin.js +1 -0
  106. package/dist/now-unresolved-plugin.js.map +1 -1
  107. package/dist/package-json-plugin.js +1 -0
  108. package/dist/package-json-plugin.js.map +1 -1
  109. package/dist/property-plugin.js +3 -1
  110. package/dist/property-plugin.js.map +1 -1
  111. package/dist/record-plugin.d.ts +30 -0
  112. package/dist/record-plugin.js +37 -1
  113. package/dist/record-plugin.js.map +1 -1
  114. package/dist/repack/lint/Rules.d.ts +11 -2
  115. package/dist/repack/lint/Rules.js +160 -16
  116. package/dist/repack/lint/Rules.js.map +1 -1
  117. package/dist/repack/lint/index.d.ts +10 -5
  118. package/dist/repack/lint/index.js +76 -50
  119. package/dist/repack/lint/index.js.map +1 -1
  120. package/dist/rest-api-plugin.js +14 -0
  121. package/dist/rest-api-plugin.js.map +1 -1
  122. package/dist/role-plugin.js +1 -0
  123. package/dist/role-plugin.js.map +1 -1
  124. package/dist/schedule-script/index.d.ts +1 -0
  125. package/dist/schedule-script/index.js +18 -0
  126. package/dist/schedule-script/index.js.map +1 -0
  127. package/dist/schedule-script/scheduled-script-plugin.d.ts +2 -0
  128. package/dist/schedule-script/scheduled-script-plugin.js +551 -0
  129. package/dist/schedule-script/scheduled-script-plugin.js.map +1 -0
  130. package/dist/schedule-script/timeZoneConverter.d.ts +61 -0
  131. package/dist/schedule-script/timeZoneConverter.js +170 -0
  132. package/dist/schedule-script/timeZoneConverter.js.map +1 -0
  133. package/dist/script-action-plugin.js +2 -0
  134. package/dist/script-action-plugin.js.map +1 -1
  135. package/dist/script-include-plugin.js +2 -0
  136. package/dist/script-include-plugin.js.map +1 -1
  137. package/dist/server-module-plugin/index.js +13 -2
  138. package/dist/server-module-plugin/index.js.map +1 -1
  139. package/dist/service-catalog/catalog-clientscript-plugin.js +2 -0
  140. package/dist/service-catalog/catalog-clientscript-plugin.js.map +1 -1
  141. package/dist/service-catalog/catalog-item-plugin.js +2 -0
  142. package/dist/service-catalog/catalog-item-plugin.js.map +1 -1
  143. package/dist/service-catalog/catalog-ui-policy-plugin.js +2 -0
  144. package/dist/service-catalog/catalog-ui-policy-plugin.js.map +1 -1
  145. package/dist/service-catalog/sc-record-producer-plugin.js +2 -0
  146. package/dist/service-catalog/sc-record-producer-plugin.js.map +1 -1
  147. package/dist/service-catalog/service-catalog-diagnostics.d.ts +6 -0
  148. package/dist/service-catalog/service-catalog-diagnostics.js +20 -0
  149. package/dist/service-catalog/service-catalog-diagnostics.js.map +1 -1
  150. package/dist/service-catalog/shape-to-record.js +7 -2
  151. package/dist/service-catalog/shape-to-record.js.map +1 -1
  152. package/dist/service-catalog/variable-set-plugin.js +2 -0
  153. package/dist/service-catalog/variable-set-plugin.js.map +1 -1
  154. package/dist/service-portal/angular-provider-plugin.js +2 -0
  155. package/dist/service-portal/angular-provider-plugin.js.map +1 -1
  156. package/dist/service-portal/dependency-plugin.js +5 -31
  157. package/dist/service-portal/dependency-plugin.js.map +1 -1
  158. package/dist/service-portal/menu-plugin.d.ts +2 -0
  159. package/dist/service-portal/menu-plugin.js +353 -0
  160. package/dist/service-portal/menu-plugin.js.map +1 -0
  161. package/dist/service-portal/page-plugin.d.ts +2 -0
  162. package/dist/service-portal/page-plugin.js +702 -0
  163. package/dist/service-portal/page-plugin.js.map +1 -0
  164. package/dist/service-portal/portal-plugin.d.ts +2 -0
  165. package/dist/service-portal/portal-plugin.js +296 -0
  166. package/dist/service-portal/portal-plugin.js.map +1 -0
  167. package/dist/service-portal/theme-plugin.d.ts +2 -0
  168. package/dist/service-portal/theme-plugin.js +112 -0
  169. package/dist/service-portal/theme-plugin.js.map +1 -0
  170. package/dist/service-portal/utils.d.ts +8 -0
  171. package/dist/service-portal/utils.js +50 -0
  172. package/dist/service-portal/utils.js.map +1 -0
  173. package/dist/service-portal/widget-plugin.js +45 -8
  174. package/dist/service-portal/widget-plugin.js.map +1 -1
  175. package/dist/sla-plugin.js +2 -0
  176. package/dist/sla-plugin.js.map +1 -1
  177. package/dist/static-content-plugin.js +1 -0
  178. package/dist/static-content-plugin.js.map +1 -1
  179. package/dist/table-plugin.js +1 -0
  180. package/dist/table-plugin.js.map +1 -1
  181. package/dist/ui-action-plugin.js +2 -0
  182. package/dist/ui-action-plugin.js.map +1 -1
  183. package/dist/ui-page-plugin.js +33 -8
  184. package/dist/ui-page-plugin.js.map +1 -1
  185. package/dist/ui-policy-plugin.js +1 -0
  186. package/dist/ui-policy-plugin.js.map +1 -1
  187. package/dist/user-preference-plugin.js +2 -0
  188. package/dist/user-preference-plugin.js.map +1 -1
  189. package/dist/utils.d.ts +20 -2
  190. package/dist/utils.js +34 -3
  191. package/dist/utils.js.map +1 -1
  192. package/dist/ux-list-menu-config-plugin.js +2 -0
  193. package/dist/ux-list-menu-config-plugin.js.map +1 -1
  194. package/dist/view-plugin.js +1 -0
  195. package/dist/view-plugin.js.map +1 -1
  196. package/dist/workspace-plugin.js +2 -0
  197. package/dist/workspace-plugin.js.map +1 -1
  198. package/package.json +10 -11
  199. package/src/_types/eslint-community-eslint-utils.d.ts +15 -0
  200. package/src/acl-plugin.ts +97 -8
  201. package/src/applicability-plugin.ts +2 -0
  202. package/src/application-menu-plugin.ts +2 -0
  203. package/src/arrow-function-plugin.ts +128 -13
  204. package/src/atf/test-plugin.ts +2 -0
  205. package/src/basic-syntax-plugin.ts +21 -0
  206. package/src/call-expression-plugin.ts +1 -0
  207. package/src/claims-plugin.ts +1 -0
  208. package/src/client-script-plugin.ts +2 -1
  209. package/src/column-plugin.ts +1 -0
  210. package/src/cross-scope-privilege-plugin.ts +2 -1
  211. package/src/dashboard/dashboard-plugin.ts +2 -0
  212. package/src/data-plugin.ts +1 -0
  213. package/src/email-notification-plugin.ts +3 -23
  214. package/src/flow/constants/flow-plugin-constants.ts +1 -1
  215. package/src/flow/flow-logic/flow-logic-plugin-helpers.ts +47 -45
  216. package/src/flow/flow-logic/flow-logic-plugin.ts +1 -0
  217. package/src/flow/plugins/approval-rules-plugin.ts +1 -0
  218. package/src/flow/plugins/flow-action-definition-plugin.ts +4 -2
  219. package/src/flow/plugins/flow-data-pill-plugin.ts +1 -0
  220. package/src/flow/plugins/flow-definition-plugin.ts +10 -4
  221. package/src/flow/plugins/flow-diagnostics-plugin.ts +1 -0
  222. package/src/flow/plugins/flow-instance-plugin.ts +103 -14
  223. package/src/flow/plugins/flow-trigger-instance-plugin.ts +1 -0
  224. package/src/flow/plugins/inline-script-plugin.ts +1 -0
  225. package/src/flow/plugins/step-definition-plugin.ts +3 -2
  226. package/src/flow/plugins/step-instance-plugin.ts +1 -0
  227. package/src/flow/plugins/trigger-plugin.ts +2 -0
  228. package/src/flow/plugins/wfa-datapill-plugin.ts +1 -0
  229. package/src/flow/post-install.ts +92 -0
  230. package/src/flow/utils/complex-objects.ts +10 -2
  231. package/src/flow/utils/flow-constants.ts +30 -1
  232. package/src/flow/utils/flow-to-xml.ts +4 -4
  233. package/src/flow/utils/label-cache-processor.ts +14 -2
  234. package/src/flow/utils/service-catalog.ts +5 -2
  235. package/src/form-plugin.ts +1411 -0
  236. package/src/html-import-plugin.ts +1 -0
  237. package/src/import-sets-plugin.ts +2 -0
  238. package/src/index.ts +9 -0
  239. package/src/instance-scan-plugin.ts +318 -0
  240. package/src/json-plugin.ts +1 -0
  241. package/src/list-plugin.ts +2 -1
  242. package/src/now-attach-plugin.ts +1 -0
  243. package/src/now-config-plugin.ts +833 -53
  244. package/src/now-id-plugin.ts +1 -0
  245. package/src/now-include-plugin.ts +1 -0
  246. package/src/now-ref-plugin.ts +1 -0
  247. package/src/now-unresolved-plugin.ts +1 -0
  248. package/src/package-json-plugin.ts +1 -0
  249. package/src/property-plugin.ts +3 -1
  250. package/src/record-plugin.ts +42 -2
  251. package/src/repack/lint/Rules.ts +171 -22
  252. package/src/repack/lint/index.ts +80 -56
  253. package/src/rest-api-plugin.ts +21 -1
  254. package/src/role-plugin.ts +2 -1
  255. package/src/schedule-script/index.ts +1 -0
  256. package/src/schedule-script/scheduled-script-plugin.ts +679 -0
  257. package/src/schedule-script/timeZoneConverter.ts +188 -0
  258. package/src/script-action-plugin.ts +2 -0
  259. package/src/script-include-plugin.ts +2 -0
  260. package/src/server-module-plugin/index.ts +14 -2
  261. package/src/service-catalog/catalog-clientscript-plugin.ts +2 -0
  262. package/src/service-catalog/catalog-item-plugin.ts +2 -0
  263. package/src/service-catalog/catalog-ui-policy-plugin.ts +2 -0
  264. package/src/service-catalog/sc-record-producer-plugin.ts +2 -0
  265. package/src/service-catalog/service-catalog-diagnostics.ts +30 -0
  266. package/src/service-catalog/shape-to-record.ts +8 -2
  267. package/src/service-catalog/variable-set-plugin.ts +2 -0
  268. package/src/service-portal/angular-provider-plugin.ts +2 -0
  269. package/src/service-portal/dependency-plugin.ts +6 -53
  270. package/src/service-portal/menu-plugin.ts +435 -0
  271. package/src/service-portal/page-plugin.ts +830 -0
  272. package/src/service-portal/portal-plugin.ts +319 -0
  273. package/src/service-portal/theme-plugin.ts +135 -0
  274. package/src/service-portal/utils.ts +69 -0
  275. package/src/service-portal/widget-plugin.ts +79 -9
  276. package/src/sla-plugin.ts +2 -0
  277. package/src/static-content-plugin.ts +1 -0
  278. package/src/table-plugin.ts +2 -1
  279. package/src/ui-action-plugin.ts +2 -0
  280. package/src/ui-page-plugin.ts +34 -8
  281. package/src/ui-policy-plugin.ts +2 -1
  282. package/src/user-preference-plugin.ts +2 -0
  283. package/src/utils.ts +42 -2
  284. package/src/ux-list-menu-config-plugin.ts +2 -0
  285. package/src/view-plugin.ts +1 -0
  286. package/src/workspace-plugin.ts +2 -0
  287. package/src/_types/eslint-plugin-es-x.d.ts +0 -17
@@ -0,0 +1,830 @@
1
+ import {
2
+ CallExpressionShape,
3
+ Plugin,
4
+ type Factory,
5
+ //type Diagnostics,
6
+ type Record,
7
+ type RecordId,
8
+ type Shape,
9
+ type ArrayShape,
10
+ type ObjectShape,
11
+ } from '@servicenow/sdk-build-core'
12
+ import isEmpty from 'lodash/isEmpty'
13
+ import { toReference, getFieldAsNumber, createSdkDocEntry } from '../utils'
14
+
15
+ type Dict = { [key: string]: unknown }
16
+
17
+ const default_placeholder_dimensions = `{
18
+ "mobile": {
19
+ "height": "250px",
20
+ "width": "100%"
21
+ },
22
+ "desktop": {
23
+ "height": "250px",
24
+ "width": "100%"
25
+ },
26
+ "tablet": {
27
+ "height": "250px",
28
+ "width": "100%"
29
+ }
30
+ }`
31
+
32
+ const default_placeholder_template = `<!--
33
+ AngularJS template with configurable options.
34
+ Use the "options" object to control dynamic behavior.
35
+ Example: Display an element when max row count is 10:
36
+ <div ng-if="options.maxRowCount === 10"></div>
37
+ The "skeleton-container" class is used for loading placeholders.
38
+ -->
39
+ <div class="skeleton-container">
40
+ <!-- Header Skeleton -->
41
+ <div class="skeleton-box skeleton-header"></div>
42
+ <!-- Body Skeleton -->
43
+ <div class="skeleton-box skeleton-line"></div>
44
+ <div class="skeleton-box skeleton-line small"></div>
45
+ <div class="skeleton-box skeleton-line medium"></div>
46
+ </div>`
47
+
48
+ const default_placeholder_script = `function evaluateConfig(options) { return {
49
+ "mobile": {
50
+ "height": "250px",
51
+ "width": "100%"
52
+ },
53
+ "desktop": {
54
+ "height": "250px",
55
+ "width": "100%"
56
+ },
57
+ "tablet": {
58
+ "height": "250px",
59
+ "width": "100%"
60
+ }
61
+ }; }`
62
+
63
+ const defaultValues = {
64
+ container: {
65
+ width: 'container',
66
+ backgroundStyle: 'default',
67
+ subheader: false,
68
+ bootstrapAlt: false,
69
+ },
70
+ instance: {
71
+ active: true,
72
+ color: 'default',
73
+ size: 'md',
74
+ asyncLoadDeviceType: 'desktop,tablet,mobile',
75
+ },
76
+ column: {
77
+ size: 12,
78
+ },
79
+ page: {
80
+ category: 'custom',
81
+ useSeoScript: false,
82
+ shortDescription: '',
83
+ public: false,
84
+ draft: false,
85
+ omitWatcher: false,
86
+ internal: false,
87
+ },
88
+ }
89
+
90
+ /**
91
+ * Safely parse a size value from a Shape, handling non-numeric strings
92
+ * @param shape - The shape to extract the size from
93
+ * @param fieldName - The field name to extract (e.g., "size", "size_sm")
94
+ * @param defaultValue - Default value if parsing fails
95
+ * @returns Numeric size value or undefined
96
+ */
97
+
98
+ /**
99
+ * Sort an array of shapes by their order field
100
+ * @param shapes - Array of shapes to sort
101
+ * @returns Sorted array
102
+ */
103
+ function sortByOrder<T extends Record>(shapes: T[]): T[] {
104
+ return shapes.sort((a, b) => {
105
+ const aOrder = getFieldAsNumber(a, 'order', 1)
106
+ const bOrder = getFieldAsNumber(b, 'order', 1)
107
+ return (aOrder ?? 1) - (bOrder ?? 1)
108
+ })
109
+ }
110
+
111
+ /**
112
+ * Conditionally adds a property to an object if the value is not empty or undefined.
113
+ * Filters out undefined values, empty arrays, and empty strings to keep objects clean.
114
+ * @param obj - The target object to add the property to
115
+ * @param key - The property key to add
116
+ * @param value - The value to add (will be filtered if empty/undefined)
117
+ */
118
+
119
+ const addProperty = (obj: Dict, key: string, value: unknown, objectType?: keyof typeof defaultValues) => {
120
+ if (
121
+ value === undefined ||
122
+ (Array.isArray(value) && value.length === 0) ||
123
+ (typeof value === 'string' && value === '')
124
+ ) {
125
+ return
126
+ }
127
+
128
+ // Check if value matches default for this object type
129
+ if (objectType && defaultValues[objectType] && (defaultValues[objectType] as Dict)[key] === value) {
130
+ return
131
+ }
132
+ obj[key] = value
133
+ }
134
+
135
+ /**
136
+ * Creates a container object from a ServiceNow sp_container record Shape.
137
+ * Transforms database fields to Fluent API format and includes nested rows.
138
+ * @param container - The sp_container record Shape
139
+ * @param rows - Array of row objects that belong to this container
140
+ * @returns Formatted container object or undefined if container is empty
141
+ */
142
+ const getContainerObject = (container: Record, rows: object[]): Dict | undefined => {
143
+ if (!container || isEmpty(container)) {
144
+ return
145
+ }
146
+
147
+ const name = container.get('name').ifString()?.getValue()
148
+ const width = container.get('width').ifString()?.getValue()
149
+ const backgroundStyle = container.get('background_style').ifString()?.getValue()
150
+ const backgroundColor = container.get('background_color').ifString()?.getValue()
151
+ const backgroundImage = container.get('background_image').ifString()?.getValue()
152
+ const cssClass = container.get('class_name').ifString()?.getValue()
153
+ const parentClass = container.get('container_class_name').ifString()?.getValue()
154
+ const subheader = container.get('subheader').toBoolean()?.getValue()
155
+ const bootstrapAlt = container.get('bootstrap_alt').toBoolean()?.getValue()
156
+ const semanticTag = container.get('semantic_tag').ifString()?.getValue()
157
+ const title = container.get('title').ifString()?.getValue()
158
+
159
+ const containerObject: Dict = {}
160
+
161
+ addProperty(containerObject, '$id', container.getId())
162
+ addProperty(containerObject, 'order', getFieldAsNumber(container, 'order', 1))
163
+ addProperty(containerObject, 'name', name)
164
+ addProperty(containerObject, 'width', width, 'container')
165
+ addProperty(containerObject, 'backgroundStyle', backgroundStyle, 'container')
166
+ addProperty(containerObject, 'backgroundColor', backgroundColor, 'container')
167
+ addProperty(containerObject, 'backgroundImage', backgroundImage)
168
+ addProperty(containerObject, 'cssClass', cssClass)
169
+ addProperty(containerObject, 'parentClass', parentClass)
170
+ addProperty(containerObject, 'subheader', subheader, 'container')
171
+ addProperty(containerObject, 'bootstrapAlt', bootstrapAlt, 'container')
172
+ addProperty(containerObject, 'semanticTag', semanticTag)
173
+ addProperty(containerObject, 'title', title)
174
+ addProperty(containerObject, 'rows', rows)
175
+
176
+ return containerObject
177
+ }
178
+
179
+ /**
180
+ * Creates a row object from a ServiceNow sp_row record Shape.
181
+ * Transforms database fields to Fluent API format and includes nested columns.
182
+ * @param row - The sp_row record Shape
183
+ * @param columns - Array of column objects that belong to this row
184
+ * @returns Formatted row object or undefined if row is empty
185
+ */
186
+ const getRowObject = (row: Record, columns: object[]): Dict | undefined => {
187
+ if (!row || isEmpty(row)) {
188
+ return
189
+ }
190
+ const cssClass = row.get('class_name').ifString()?.getValue()
191
+ const semanticTag = row.get('semantic_tag').ifString()?.getValue()
192
+
193
+ const rowObject: Dict = {}
194
+ addProperty(rowObject, '$id', row.getId())
195
+ addProperty(rowObject, 'cssClass', cssClass)
196
+ addProperty(rowObject, 'semanticTag', semanticTag)
197
+ addProperty(rowObject, 'order', getFieldAsNumber(row, 'order', 1))
198
+ addProperty(rowObject, 'columns', columns)
199
+ return rowObject
200
+ }
201
+
202
+ /**
203
+ * Creates a column object from a ServiceNow sp_column record Shape.
204
+ * @param column - The sp_column record Shape
205
+ * @param instances - Array of instance objects that belong to this column
206
+ * @param nestedRows - Array of nested row objects within this column
207
+ * @returns Formatted column object or undefined if column is empty
208
+ */
209
+ const getColumnObject = (column: Record, instances: object[], nestedRows: object[]): Dict | undefined => {
210
+ if (!column || isEmpty(column)) {
211
+ return
212
+ }
213
+
214
+ const cssClass = column.get('class_name').ifString()?.getValue()
215
+ const semanticTag = column.get('semantic_tag').ifString()?.getValue()
216
+
217
+ const columnObject: Dict = {}
218
+
219
+ addProperty(columnObject, '$id', column.getId())
220
+ addProperty(columnObject, 'size', getFieldAsNumber(column, 'size', 12), 'column')
221
+ addProperty(columnObject, 'sizeSm', getFieldAsNumber(column, 'size_sm'))
222
+ addProperty(columnObject, 'sizeLg', getFieldAsNumber(column, 'size_lg'))
223
+ addProperty(columnObject, 'sizeXs', getFieldAsNumber(column, 'size_xs'))
224
+ addProperty(columnObject, 'cssClass', cssClass)
225
+ addProperty(columnObject, 'semanticTag', semanticTag)
226
+ addProperty(columnObject, 'order', getFieldAsNumber(column, 'order', 1))
227
+ addProperty(columnObject, 'instances', instances)
228
+ addProperty(columnObject, 'nestedRows', nestedRows)
229
+
230
+ return columnObject
231
+ }
232
+ /**
233
+ * Extracts and parses roles from a ServiceNow instance record.
234
+ * Converts comma-separated role string to an array of role names.
235
+ * @param instance - The sp_instance record Shape
236
+ * @returns Array of role names or undefined if no roles exist
237
+ */
238
+ const getRolesArray = (instance: Record): string[] | undefined => {
239
+ const rolesStr = instance.get('roles').ifString()?.getValue()
240
+ if (!rolesStr || rolesStr === '') {
241
+ return
242
+ }
243
+ const rolesArray = rolesStr
244
+ .split(',')
245
+ .map((role) => role.trim())
246
+ .filter((role) => role !== '')
247
+ return rolesArray.length > 0 ? rolesArray : undefined
248
+ }
249
+
250
+ /**
251
+ * Creates an instance object from a ServiceNow sp_instance record Shape.
252
+ * Transforms all instance properties from database format to Fluent API format,
253
+ * including widget references, styling, roles, and configuration.
254
+ * @param instance - The sp_instance record Shape
255
+ * @returns Formatted instance object or undefined if instance is empty
256
+ */
257
+ function getInstanceObject(instance: Record): object | undefined {
258
+ if (!instance || isEmpty(instance)) {
259
+ return
260
+ }
261
+
262
+ const title = instance.get('title').ifString()?.getValue()
263
+ const id = instance.get('id').ifString()?.getValue()
264
+ const widget = instance.get('sp_widget').ifString()?.getValue()
265
+ const widgetParameters = instance.get('widget_parameters').ifString()?.getValue()
266
+ const css = instance.get('css').ifString()?.getValue()
267
+ const url = instance.get('url').ifString()?.getValue()
268
+ const glyph = instance.get('glyph').ifString()?.getValue()
269
+ const size = instance.get('size').ifString()?.getValue()
270
+ const color = instance.get('color').ifString()?.getValue()
271
+ const cssClass = instance.get('class_name').ifString()?.getValue()
272
+ const shortDescription = instance.get('short_description').ifString()?.getValue()
273
+ const active = instance.get('active').toBoolean()?.getValue()
274
+
275
+ const instanceObject: Dict = {}
276
+ addProperty(instanceObject, '$id', instance.getId())
277
+ addProperty(instanceObject, 'title', title)
278
+ addProperty(instanceObject, 'id', id)
279
+ addProperty(instanceObject, 'widget', widget)
280
+ addProperty(instanceObject, 'widgetParameters', widgetParameters)
281
+ addProperty(instanceObject, 'css', css)
282
+ addProperty(instanceObject, 'url', url)
283
+ addProperty(instanceObject, 'glyph', glyph)
284
+ addProperty(instanceObject, 'size', size, 'instance')
285
+ addProperty(instanceObject, 'color', color, 'instance')
286
+ addProperty(instanceObject, 'cssClass', cssClass)
287
+ addProperty(instanceObject, 'active', active, 'instance')
288
+ addProperty(instanceObject, 'order', getFieldAsNumber(instance, 'order', 1))
289
+ addProperty(instanceObject, 'roles', getRolesArray(instance))
290
+ addProperty(instanceObject, 'shortDescription', shortDescription)
291
+
292
+ return instanceObject
293
+ }
294
+
295
+ /**
296
+ * Converts an ArrayShape of roles to a comma-separated string.
297
+ * Handles both role record objects and string values, removes duplicates and empty values.
298
+ * @param roles - ArrayShape containing role records or strings
299
+ * @returns Comma-separated string of unique role names or undefined if no roles
300
+ */
301
+ function getRolesString(roles: ArrayShape | undefined): string | undefined {
302
+ if (!roles) {
303
+ return
304
+ }
305
+ const uniqueRoles = [
306
+ ...new Set(
307
+ roles.getElements().map((role) => {
308
+ const roleName = role.isRecord()
309
+ ? role.get('name')?.ifString()?.getValue()
310
+ : role.ifString()?.getValue()
311
+ return (roleName ?? '').trim()
312
+ })
313
+ ),
314
+ ]
315
+ return uniqueRoles.filter((role) => role !== '').join(',')
316
+ }
317
+
318
+ /**
319
+ * Recursively processes nested rows within a column
320
+ * @param columnRows - Rows that belong to the column
321
+ * @param allColumns - All available columns
322
+ * @param allInstances - All available instances
323
+ * @returns Array of processed SPRow objects with nested structure
324
+ */
325
+ function getNestedRows(columnRows: Record[], allRows: Record[], allColumns: Record[], allInstances: Record[]): Dict[] {
326
+ if (!columnRows || columnRows.length === 0) {
327
+ return []
328
+ }
329
+
330
+ // Sort rows by order
331
+ const sortedRows = sortByOrder(columnRows)
332
+
333
+ return sortedRows
334
+ .map((row) => {
335
+ const rowId = row.getId()
336
+
337
+ // Get columns for this nested row
338
+ const rowColumns = sortByOrder(allColumns.filter((column) => column.get('sp_row').equals(rowId)))
339
+
340
+ const columns = rowColumns
341
+ .map((column) => {
342
+ const columnId = column.getId()
343
+
344
+ // Get instances for this column
345
+ const columnInstances = sortByOrder(
346
+ allInstances.filter((instance) => instance.get('sp_column').equals(columnId))
347
+ )
348
+
349
+ // Recursively get nested rows for this column
350
+ const nestedColumnRows = allRows.filter((row) => row.get('sp_column').equals(columnId))
351
+ const nestedRows = getNestedRows(nestedColumnRows, allRows, allColumns, allInstances)
352
+ const instances = columnInstances
353
+ .map((instance) => {
354
+ return getInstanceObject(instance)
355
+ })
356
+ .filter((instanceObject): instanceObject is Dict => Boolean(instanceObject))
357
+
358
+ return getColumnObject(column, instances, nestedRows)
359
+ })
360
+ .filter((col): col is Dict => Boolean(col))
361
+
362
+ const rowObj = getRowObject(row, columns)
363
+ return rowObj as Dict
364
+ })
365
+ .filter((row): row is Dict => Boolean(row))
366
+ }
367
+
368
+ /**
369
+ * Generates a container name from the container shape or creates a default name.
370
+ * Uses the explicit name if provided, otherwise generates a name based on page title and order.
371
+ * @param $ - The container shape object
372
+ * @param pageTitle - The title of the parent page
373
+ * @param index - The zero-based index of the container in the page
374
+ * @returns The container name string
375
+ */
376
+ const getContainerName = ($: ObjectShape, pageTitle: string, index: number): string => {
377
+ let name = $.get('name').ifString()?.getValue()
378
+ if (!name) {
379
+ const order = $.get('order').ifNumber()?.getValue() || index + 1
380
+ name = `${pageTitle} - Container ${order}`
381
+ }
382
+ return name
383
+ }
384
+
385
+ /**
386
+ * Creates ServiceNow sp_container records from Fluent container shapes and their nested rows/columns/instances.
387
+ *
388
+ * @param containersArray - Array of Fluent container Shape objects to transform
389
+ * @param pageId - RecordId of the parent sp_page record that owns these containers
390
+ * @param pageTitle - Title of the parent page, used for generating default container names
391
+ * @param factory - Factory instance for creating ServiceNow records with proper relationships
392
+ * @returns Promise that resolves to an array of all created records (containers, rows, columns, instances)
393
+ */
394
+ const createContainerRecords = async (
395
+ containersArray: Shape[],
396
+ pageId: RecordId,
397
+ pageTitle: string,
398
+ factory: Factory
399
+ ): Promise<Record[]> => {
400
+ const records: Record[] = []
401
+
402
+ for (let index = 0; index < containersArray.length; index++) {
403
+ const containerShape = containersArray[index]
404
+ if (containerShape?.isObject()) {
405
+ const container = containerShape.asObject()
406
+
407
+ const name = getContainerName(container, pageTitle, index)
408
+
409
+ const containerRecord = await factory.createRecord({
410
+ source: containerShape,
411
+ table: 'sp_container',
412
+ explicitId: container.get('$id'),
413
+ properties: container.transform(({ $ }) => ({
414
+ name: $.val(name),
415
+ sp_page: $.val(pageId),
416
+ width: $.from('width').def('container'),
417
+ background_style: $.from('backgroundStyle').def('default'),
418
+ background_color: $.from('backgroundColor').def(''),
419
+ background_image: $.from('backgroundImage').def(''),
420
+ class_name: $.from('cssClass').def(''),
421
+ container_class_name: $.from('parentClass').def(''),
422
+ subheader: $.from('subheader').def(false),
423
+ bootstrap_alt: $.from('bootstrapAlt').def(false),
424
+ semantic_tag: $.from('semanticTag').def(''),
425
+ title: $.from('title').def(''),
426
+ order: $.from('order').def(index + 1),
427
+ })),
428
+ })
429
+
430
+ records.push(containerRecord)
431
+
432
+ // Handle rows
433
+ const rows = container.get('rows').ifArray()?.getElements() || []
434
+ const rowRecords = await createRowRecords(rows, containerRecord.getId(), factory, undefined)
435
+ records.push(...rowRecords)
436
+ }
437
+ }
438
+ return records
439
+ }
440
+
441
+ /**
442
+ * Creates ServiceNow sp_row records from Fluent row shapes and their nested columns/instances.
443
+ *
444
+ * @param rowsArray - Array of Fluent row Shape objects to transform
445
+ * @param containerId - RecordId of the parent sp_container record that owns these rows
446
+ * @param factory - Factory instance for creating ServiceNow records with proper relationships
447
+ * @param columnId - RecordId of the parent sp_column record (for nested rows within columns)
448
+ * @returns Promise that resolves to an array of all created records (rows, columns, instances)
449
+ */
450
+ const createRowRecords = async (
451
+ rowsArray: Shape[],
452
+ containerId: RecordId | undefined,
453
+ factory: Factory,
454
+ columnId: RecordId | undefined
455
+ ): Promise<Record[]> => {
456
+ const records: Record[] = []
457
+
458
+ for (let index = 0; index < rowsArray.length; index++) {
459
+ const rowShape = rowsArray[index]
460
+ if (rowShape?.isObject()) {
461
+ const row = rowShape.asObject()
462
+
463
+ const rowRecord = await factory.createRecord({
464
+ source: rowShape,
465
+ table: 'sp_row',
466
+ explicitId: row.get('$id'),
467
+ properties: row.transform(({ $ }) => ({
468
+ sp_container: $.val(containerId),
469
+ class_name: $.from('cssClass').def(''),
470
+ semantic_tag: $.from('semanticTag').def(''),
471
+ order: $.from('order').def(index + 1),
472
+ sp_column: $.val(columnId),
473
+ })),
474
+ })
475
+
476
+ records.push(rowRecord)
477
+
478
+ // Handle columns
479
+ const columns = row.get('columns').ifArray()?.getElements() || []
480
+ const columnRecords = await createColumnRecords(columns, rowRecord.getId(), factory)
481
+ records.push(...columnRecords)
482
+ }
483
+ }
484
+
485
+ return records
486
+ }
487
+
488
+ /**
489
+ * Creates ServiceNow sp_column records from Fluent column shapes and their nested instances/rows.
490
+ *
491
+ * @param columnsArray - Array of Fluent column Shape objects to transform
492
+ * @param rowId - RecordId of the parent sp_row record that owns these columns
493
+ * @param factory - Factory instance for creating ServiceNow records with proper relationships
494
+ * @returns Promise that resolves to an array of all created records (columns, instances, nested rows)
495
+ */
496
+ async function createColumnRecords(columnsArray: Shape[], rowId: RecordId, factory: Factory): Promise<Record[]> {
497
+ const records: Record[] = []
498
+
499
+ for (let index = 0; index < columnsArray.length; index++) {
500
+ const columnShape = columnsArray[index]
501
+ if (columnShape?.isObject()) {
502
+ const column = columnShape.asObject()
503
+
504
+ const columnRecord = await factory.createRecord({
505
+ source: columnShape,
506
+ table: 'sp_column',
507
+ explicitId: column.get('$id'),
508
+ properties: column.transform(({ $ }) => ({
509
+ sp_row: $.val(rowId),
510
+ size: $.from('size').def(12),
511
+ size_sm: $.from('sizeSm'),
512
+ size_lg: $.from('sizeLg'),
513
+ size_xs: $.from('sizeXs'),
514
+ class_name: $.from('cssClass').def(''),
515
+ semantic_tag: $.from('semanticTag').def(''),
516
+ order: $.from('order').def(index + 1),
517
+ })),
518
+ })
519
+
520
+ records.push(columnRecord)
521
+
522
+ // Handle instances
523
+ const instances = column.get('instances').ifArray()?.getElements() || []
524
+ const nestedRows = column.get('nestedRows').ifArray()?.getElements() || []
525
+ const nestedRowRecords = await createRowRecords(nestedRows, undefined, factory, columnRecord.getId())
526
+ records.push(...nestedRowRecords)
527
+ const instanceRecords = await createInstanceRecords(instances, columnRecord.getId(), factory)
528
+ records.push(...instanceRecords)
529
+ }
530
+ }
531
+ return records
532
+ }
533
+
534
+ /**
535
+ * Creates ServiceNow sp_instance records from Fluent instance shapes.
536
+ *
537
+ * @param instancesArray - Array of Fluent instance Shape objects to transform
538
+ * @param columnId - RecordId of the parent sp_column record that owns these instances
539
+ * @param factory - Factory instance for creating ServiceNow records with proper relationships
540
+ * @returns Promise that resolves to an array of all created instance records
541
+ */
542
+ async function createInstanceRecords(instancesArray: Shape[], columnId: RecordId, factory: Factory): Promise<Record[]> {
543
+ const records: Record[] = []
544
+
545
+ for (let index = 0; index < instancesArray.length; index++) {
546
+ const instanceShape = instancesArray[index]
547
+ if (instanceShape?.isObject()) {
548
+ const instance = instanceShape.asObject()
549
+
550
+ // Process roles if they exist as an array
551
+ const rolesArray = instance.get('roles').ifArray()
552
+ const rolesString = getRolesString(rolesArray)
553
+
554
+ const instanceRecord = await factory.createRecord({
555
+ source: instanceShape,
556
+ table: 'sp_instance',
557
+ explicitId: instance.get('$id'),
558
+ properties: instance.transform(({ $ }) => ({
559
+ sp_column: $.val(columnId),
560
+ title: $.from('title').def(''),
561
+ id: $.from('id').def(''),
562
+ sp_widget: $.from('widget').map(toReference).def(''),
563
+ widget_parameters: $.from('widgetParameters').def(''),
564
+ short_description: $.from('shortDescription').def(''),
565
+ css: $.from('css').def(''),
566
+ url: $.from('url').def(''),
567
+ glyph: $.from('glyph').def(''),
568
+ size: $.from('size').def('md'),
569
+ color: $.from('color').def('default'),
570
+ class_name: $.from('cssClass').def(''),
571
+ active: $.from('active').def(defaultValues.instance.active),
572
+ order: $.from('order').def(index + 1),
573
+ roles: $.val(rolesString).def(''),
574
+ async_load: $.from('asyncLoad').def(false),
575
+ async_load_trigger: $.from('asyncLoadTrigger').def('viewport'),
576
+ async_load_device_type: $.def(defaultValues.instance.asyncLoadDeviceType),
577
+ preserve_placeholder_size: $.def(false),
578
+ placeholder_dimensions: $.def(default_placeholder_dimensions),
579
+ advanced_placeholder_dimensions: $.def(false),
580
+ placeholder_dimensions_script: $.def(default_placeholder_script),
581
+ placeholder_template: $.def(default_placeholder_template),
582
+ })),
583
+ })
584
+
585
+ records.push(instanceRecord)
586
+ }
587
+ }
588
+ return records
589
+ }
590
+
591
+ export const SPPagePlugin = Plugin.create({
592
+ name: 'SPPagePlugin',
593
+ docs: [createSdkDocEntry('SPPage', ['sp_page'])],
594
+ records: {
595
+ sp_page: {
596
+ coalesce: ['id'],
597
+ relationships: {
598
+ sp_container: {
599
+ via: 'sp_page',
600
+ descendant: true,
601
+ relationships: {
602
+ sp_row: {
603
+ via: 'sp_container',
604
+ descendant: true,
605
+ relationships: {
606
+ sp_column: {
607
+ via: 'sp_row',
608
+ descendant: true,
609
+ relationships: {
610
+ sp_instance: {
611
+ via: 'sp_column',
612
+ descendant: true,
613
+ },
614
+ sp_row: {
615
+ via: 'sp_column',
616
+ descendant: true,
617
+ },
618
+ },
619
+ },
620
+ },
621
+ },
622
+ },
623
+ },
624
+ },
625
+ toShape(record, { descendants }) {
626
+ // Build hierarchical structure from descendants in a single pass
627
+ // to avoid "node that was removed or forgotten" errors
628
+
629
+ // Get all descendants at once to avoid multiple queries
630
+ const allContainers = sortByOrder(
631
+ descendants
632
+ .query('sp_container')
633
+ .filter((container) => container.get('sp_page').equals(record.getId()))
634
+ )
635
+ const allRows = descendants.query('sp_row')
636
+ const allColumns = descendants.query('sp_column')
637
+ const allInstances = descendants.query('sp_instance')
638
+
639
+ const containers = allContainers.map((container) => {
640
+ const containerId = container.getId()
641
+
642
+ // Get rows for this container
643
+ const containerRows = sortByOrder(
644
+ allRows.filter((row) => row.get('sp_container').equals(containerId))
645
+ )
646
+
647
+ const rows = containerRows
648
+ .map((row) => {
649
+ const rowId = row.getId()
650
+
651
+ // Get columns for this row
652
+ const rowColumns = sortByOrder(
653
+ allColumns.filter((column) => column.get('sp_row').equals(rowId))
654
+ )
655
+ const columns = rowColumns
656
+ .map((column) => {
657
+ const columnId = column.getId()
658
+
659
+ // Get instances for this column
660
+ const columnInstances = sortByOrder(
661
+ allInstances.filter((instance) => instance.get('sp_column').equals(columnId))
662
+ )
663
+
664
+ const columnRows = allRows.filter((row) => row.get('sp_column').equals(columnId))
665
+ const nestedRows = getNestedRows(columnRows, allRows, allColumns, allInstances)
666
+
667
+ const instances = columnInstances
668
+ .map((instance) => {
669
+ return getInstanceObject(instance)
670
+ })
671
+ .filter((instanceObject): instanceObject is object => Boolean(instanceObject))
672
+
673
+ return getColumnObject(column, instances, nestedRows)
674
+ })
675
+ .filter((columnObject): columnObject is Dict => Boolean(columnObject))
676
+
677
+ return getRowObject(row, columns)
678
+ })
679
+ .filter((rowObject): rowObject is Dict => Boolean(rowObject))
680
+
681
+ return getContainerObject(container, rows)
682
+ })
683
+
684
+ // Process roles to check if they should be included
685
+ const rolesArray = getRolesArray(record)
686
+
687
+ return {
688
+ success: true,
689
+ value: new CallExpressionShape({
690
+ source: record,
691
+ callee: 'SPPage',
692
+ args: [
693
+ record.transform(({ $ }) => {
694
+ const pageObject: { [key: string]: typeof $ | undefined } = {
695
+ title: $,
696
+ category: $,
697
+ pageId: $.from('id').def(''),
698
+ draft: $.toBoolean().def(false),
699
+ internal: $.toBoolean().def(false),
700
+ omitWatcher: $.from('omit_watcher').toBoolean().def(false),
701
+ public: $.toBoolean().def(false),
702
+ useSeoScript: $.from('use_seo_script').toBoolean().def(false),
703
+ css: $.def(''),
704
+ shortDescription: $.from('short_description').def(''),
705
+ seoScript: $.from('seo_script').def(''),
706
+ dynamicTitleStructure: $.from('dynamic_title_structure').def(''),
707
+ humanReadableUrlStructure: $.from('human_readable_url_structure').def(''),
708
+ }
709
+ // Only add roles if they exist
710
+ if (rolesArray) {
711
+ pageObject['roles'] = $.val(rolesArray)
712
+ }
713
+
714
+ // Only add containers if they exist
715
+ if (containers.length > 0) {
716
+ pageObject['containers'] = $.val(containers)
717
+ }
718
+
719
+ return pageObject
720
+ }),
721
+ ],
722
+ }),
723
+ }
724
+ },
725
+ },
726
+ },
727
+ shapes: [
728
+ {
729
+ shape: CallExpressionShape,
730
+ fileTypes: ['fluent'],
731
+ async toRecord(callExpression, { diagnostics, factory }) {
732
+ if (callExpression.getCallee() !== 'SPPage') {
733
+ return { success: false }
734
+ }
735
+
736
+ const page = callExpression.getArgument(0).asObject()
737
+ const containers = page.get('containers').ifArray()?.getElements() || []
738
+
739
+ const useSeoScriptShape = page.get('useSeoScript')
740
+ const useSeoScript = useSeoScriptShape.ifBoolean()?.getValue() ?? false
741
+ const seoScriptRef = toReference(page.get('seoScript'))
742
+ const seoScript = typeof seoScriptRef === 'string' ? seoScriptRef : seoScriptRef.getValue()
743
+
744
+ if (useSeoScript && (!seoScript || seoScript.trim() === '')) {
745
+ diagnostics.error(
746
+ useSeoScriptShape.getOriginalNode(),
747
+ `Invalid SPPage configuration: when "useSeoScript" is true, "seoScript" must be added.`
748
+ )
749
+ }
750
+
751
+ // Process roles if they exist as an array
752
+ const rolesArray = page.get('roles').ifArray()
753
+ const rolesString = getRolesString(rolesArray)
754
+
755
+ const pageIdShape = page.get('pageId')
756
+ const pageId = pageIdShape.asString().getValue()
757
+
758
+ if (!pageId.trim()) {
759
+ diagnostics.error(
760
+ pageIdShape.getOriginalNode(),
761
+ 'Invalid SPPage configuration: "pageId" must be a non-empty string.'
762
+ )
763
+ }
764
+
765
+ const urlStructureShape = page.get('humanReadableUrlStructure')
766
+ const urlStructure = urlStructureShape.ifString()?.getValue()
767
+ if (urlStructure && urlStructure.length > 0) {
768
+ const delimiter = String(Math.floor(Math.random() * 100000))
769
+ const urlStrings = urlStructure
770
+ .replaceAll(/[/-]/g, delimiter)
771
+ .replaceAll('%', delimiter + '%')
772
+ .split(delimiter)
773
+ const nonVariables: string[] = []
774
+ for (const segment of urlStrings) {
775
+ if (segment.length > 1 && segment.indexOf('%') === 0) {
776
+ continue
777
+ }
778
+ nonVariables.push(segment)
779
+ }
780
+
781
+ if (!/^[a-zA-Z0-9/-]*$/.test(nonVariables.join(''))) {
782
+ diagnostics.error(
783
+ urlStructureShape.getOriginalNode(),
784
+ `Only alphanumeric characters, - and / are allowed in humanReadableUrlStructure.`
785
+ )
786
+ }
787
+
788
+ if (urlStructure.indexOf('/') !== urlStructure.lastIndexOf('/')) {
789
+ diagnostics.error(
790
+ urlStructureShape.getOriginalNode(),
791
+ `No more than one "/" character is allowed in humanReadableUrlStructure.`
792
+ )
793
+ }
794
+ }
795
+
796
+ const title = page.get('title').ifString()?.getValue() || pageId
797
+
798
+ // Create the main page record
799
+ const pageRecord = await factory.createRecord({
800
+ source: callExpression,
801
+ table: 'sp_page',
802
+ properties: page.transform(({ $ }) => ({
803
+ id: $.from('pageId'),
804
+ title: $.val(title),
805
+ category: $.def('custom'),
806
+ css: $.def(''),
807
+ draft: $.def(false),
808
+ dynamic_title_structure: $.from('dynamicTitleStructure').def(''),
809
+ human_readable_url_structure: $.from('humanReadableUrlStructure').def(''),
810
+ internal: $.def(false),
811
+ omit_watcher: $.from('omitWatcher').def(false),
812
+ public: $.def(false),
813
+ roles: $.val(rolesString).def(''),
814
+ seo_script: $.from('seoScript').map(toReference).def(''),
815
+ short_description: $.from('shortDescription').def(''),
816
+ use_seo_script: $.from('useSeoScript').def(false),
817
+ })),
818
+ })
819
+
820
+ // Create container, row, column, and instance records
821
+ const allRecords = await createContainerRecords(containers, pageRecord.getId(), title, factory)
822
+
823
+ return {
824
+ success: true,
825
+ value: pageRecord.with(...allRecords),
826
+ }
827
+ },
828
+ },
829
+ ],
830
+ })