@servicenow/sdk-build-plugins 4.2.0 → 4.4.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 (349) hide show
  1. package/dist/acl-plugin.js +11 -0
  2. package/dist/acl-plugin.js.map +1 -1
  3. package/dist/applicability-plugin.d.ts +2 -0
  4. package/dist/applicability-plugin.js +72 -0
  5. package/dist/applicability-plugin.js.map +1 -0
  6. package/dist/atf/test-plugin.js +5 -2
  7. package/dist/atf/test-plugin.js.map +1 -1
  8. package/dist/basic-syntax-plugin.js +7 -1
  9. package/dist/basic-syntax-plugin.js.map +1 -1
  10. package/dist/business-rule-plugin.js +1 -0
  11. package/dist/business-rule-plugin.js.map +1 -1
  12. package/dist/call-expression-plugin.js +1 -107
  13. package/dist/call-expression-plugin.js.map +1 -1
  14. package/dist/column/column-to-record.d.ts +10 -3
  15. package/dist/column/column-to-record.js +44 -7
  16. package/dist/column/column-to-record.js.map +1 -1
  17. package/dist/column-plugin.d.ts +3 -1
  18. package/dist/column-plugin.js +12 -12
  19. package/dist/column-plugin.js.map +1 -1
  20. package/dist/dashboard/dashboard-component-property-defaults.d.ts +152 -0
  21. package/dist/dashboard/dashboard-component-property-defaults.js +264 -0
  22. package/dist/dashboard/dashboard-component-property-defaults.js.map +1 -0
  23. package/dist/dashboard/dashboard-component-resolver.d.ts +13 -0
  24. package/dist/dashboard/dashboard-component-resolver.js +69 -0
  25. package/dist/dashboard/dashboard-component-resolver.js.map +1 -0
  26. package/dist/dashboard/dashboard-plugin.d.ts +12 -0
  27. package/dist/dashboard/dashboard-plugin.js +397 -0
  28. package/dist/dashboard/dashboard-plugin.js.map +1 -0
  29. package/dist/data-plugin.d.ts +3 -0
  30. package/dist/data-plugin.js +61 -113
  31. package/dist/data-plugin.js.map +1 -1
  32. package/dist/email-notification-plugin.d.ts +2 -0
  33. package/dist/email-notification-plugin.js +541 -0
  34. package/dist/email-notification-plugin.js.map +1 -0
  35. package/dist/flow/constants/flow-plugin-constants.d.ts +58 -0
  36. package/dist/flow/constants/flow-plugin-constants.js +70 -0
  37. package/dist/flow/constants/flow-plugin-constants.js.map +1 -0
  38. package/dist/flow/flow-logic/flow-logic-constants.d.ts +38 -0
  39. package/dist/flow/flow-logic/flow-logic-constants.js +118 -0
  40. package/dist/flow/flow-logic/flow-logic-constants.js.map +1 -0
  41. package/dist/flow/flow-logic/flow-logic-diagnostics.d.ts +19 -0
  42. package/dist/flow/flow-logic/flow-logic-diagnostics.js +503 -0
  43. package/dist/flow/flow-logic/flow-logic-diagnostics.js.map +1 -0
  44. package/dist/flow/flow-logic/flow-logic-plugin-helpers.d.ts +62 -0
  45. package/dist/flow/flow-logic/flow-logic-plugin-helpers.js +2092 -0
  46. package/dist/flow/flow-logic/flow-logic-plugin-helpers.js.map +1 -0
  47. package/dist/flow/flow-logic/flow-logic-plugin.d.ts +52 -0
  48. package/dist/flow/flow-logic/flow-logic-plugin.js +283 -0
  49. package/dist/flow/flow-logic/flow-logic-plugin.js.map +1 -0
  50. package/dist/flow/flow-logic/flow-logic-shapes.d.ts +104 -0
  51. package/dist/flow/flow-logic/flow-logic-shapes.js +201 -0
  52. package/dist/flow/flow-logic/flow-logic-shapes.js.map +1 -0
  53. package/dist/flow/plugins/approval-rules-plugin.d.ts +2 -0
  54. package/dist/flow/plugins/approval-rules-plugin.js +49 -0
  55. package/dist/flow/plugins/approval-rules-plugin.js.map +1 -0
  56. package/dist/flow/plugins/flow-action-definition-plugin.d.ts +2 -0
  57. package/dist/flow/plugins/flow-action-definition-plugin.js +286 -0
  58. package/dist/flow/plugins/flow-action-definition-plugin.js.map +1 -0
  59. package/dist/flow/plugins/flow-data-pill-plugin.d.ts +9 -0
  60. package/dist/flow/plugins/flow-data-pill-plugin.js +212 -0
  61. package/dist/flow/plugins/flow-data-pill-plugin.js.map +1 -0
  62. package/dist/flow/plugins/flow-definition-plugin.d.ts +2 -0
  63. package/dist/flow/plugins/flow-definition-plugin.js +1668 -0
  64. package/dist/flow/plugins/flow-definition-plugin.js.map +1 -0
  65. package/dist/flow/plugins/flow-diagnostics-plugin.d.ts +26 -0
  66. package/dist/flow/plugins/flow-diagnostics-plugin.js +217 -0
  67. package/dist/flow/plugins/flow-diagnostics-plugin.js.map +1 -0
  68. package/dist/flow/plugins/flow-instance-plugin.d.ts +12 -0
  69. package/dist/flow/plugins/flow-instance-plugin.js +1205 -0
  70. package/dist/flow/plugins/flow-instance-plugin.js.map +1 -0
  71. package/dist/flow/plugins/flow-trigger-instance-plugin.d.ts +2 -0
  72. package/dist/flow/plugins/flow-trigger-instance-plugin.js +338 -0
  73. package/dist/flow/plugins/flow-trigger-instance-plugin.js.map +1 -0
  74. package/dist/flow/plugins/inline-script-plugin.d.ts +39 -0
  75. package/dist/flow/plugins/inline-script-plugin.js +80 -0
  76. package/dist/flow/plugins/inline-script-plugin.js.map +1 -0
  77. package/dist/flow/plugins/step-definition-plugin.d.ts +5 -0
  78. package/dist/flow/plugins/step-definition-plugin.js +71 -0
  79. package/dist/flow/plugins/step-definition-plugin.js.map +1 -0
  80. package/dist/flow/plugins/step-instance-plugin.d.ts +31 -0
  81. package/dist/flow/plugins/step-instance-plugin.js +339 -0
  82. package/dist/flow/plugins/step-instance-plugin.js.map +1 -0
  83. package/dist/flow/plugins/trigger-plugin.d.ts +2 -0
  84. package/dist/flow/plugins/trigger-plugin.js +96 -0
  85. package/dist/flow/plugins/trigger-plugin.js.map +1 -0
  86. package/dist/flow/plugins/wfa-datapill-plugin.d.ts +15 -0
  87. package/dist/flow/plugins/wfa-datapill-plugin.js +178 -0
  88. package/dist/flow/plugins/wfa-datapill-plugin.js.map +1 -0
  89. package/dist/flow/utils/approval-rules-processor.d.ts +13 -0
  90. package/dist/flow/utils/approval-rules-processor.js +267 -0
  91. package/dist/flow/utils/approval-rules-processor.js.map +1 -0
  92. package/dist/flow/utils/built-in-complex-objects.d.ts +19 -0
  93. package/dist/flow/utils/built-in-complex-objects.js +62 -0
  94. package/dist/flow/utils/built-in-complex-objects.js.map +1 -0
  95. package/dist/flow/utils/complex-object-resolver.d.ts +8 -0
  96. package/dist/flow/utils/complex-object-resolver.js +614 -0
  97. package/dist/flow/utils/complex-object-resolver.js.map +1 -0
  98. package/dist/flow/utils/complex-objects.d.ts +36 -0
  99. package/dist/flow/utils/complex-objects.js +481 -0
  100. package/dist/flow/utils/complex-objects.js.map +1 -0
  101. package/dist/flow/utils/data-pill-shapes.d.ts +58 -0
  102. package/dist/flow/utils/data-pill-shapes.js +135 -0
  103. package/dist/flow/utils/data-pill-shapes.js.map +1 -0
  104. package/dist/flow/utils/datapill-transformer.d.ts +110 -0
  105. package/dist/flow/utils/datapill-transformer.js +503 -0
  106. package/dist/flow/utils/datapill-transformer.js.map +1 -0
  107. package/dist/flow/utils/flow-constants.d.ts +72 -0
  108. package/dist/flow/utils/flow-constants.js +230 -0
  109. package/dist/flow/utils/flow-constants.js.map +1 -0
  110. package/dist/flow/utils/flow-io-to-record.d.ts +44 -0
  111. package/dist/flow/utils/flow-io-to-record.js +409 -0
  112. package/dist/flow/utils/flow-io-to-record.js.map +1 -0
  113. package/dist/flow/utils/flow-shapes.d.ts +161 -0
  114. package/dist/flow/utils/flow-shapes.js +255 -0
  115. package/dist/flow/utils/flow-shapes.js.map +1 -0
  116. package/dist/flow/utils/flow-to-xml.d.ts +16 -0
  117. package/dist/flow/utils/flow-to-xml.js +237 -0
  118. package/dist/flow/utils/flow-to-xml.js.map +1 -0
  119. package/dist/flow/utils/flow-variable-processor.d.ts +51 -0
  120. package/dist/flow/utils/flow-variable-processor.js +69 -0
  121. package/dist/flow/utils/flow-variable-processor.js.map +1 -0
  122. package/dist/flow/utils/label-cache-parser.d.ts +7 -0
  123. package/dist/flow/utils/label-cache-parser.js +24 -0
  124. package/dist/flow/utils/label-cache-parser.js.map +1 -0
  125. package/dist/flow/utils/label-cache-processor.d.ts +119 -0
  126. package/dist/flow/utils/label-cache-processor.js +719 -0
  127. package/dist/flow/utils/label-cache-processor.js.map +1 -0
  128. package/dist/flow/utils/pill-string-parser.d.ts +88 -0
  129. package/dist/flow/utils/pill-string-parser.js +306 -0
  130. package/dist/flow/utils/pill-string-parser.js.map +1 -0
  131. package/dist/flow/utils/schema-to-flow-object.d.ts +22 -0
  132. package/dist/flow/utils/schema-to-flow-object.js +318 -0
  133. package/dist/flow/utils/schema-to-flow-object.js.map +1 -0
  134. package/dist/flow/utils/service-catalog.d.ts +47 -0
  135. package/dist/flow/utils/service-catalog.js +137 -0
  136. package/dist/flow/utils/service-catalog.js.map +1 -0
  137. package/dist/flow/utils/utils.d.ts +117 -0
  138. package/dist/flow/utils/utils.js +345 -0
  139. package/dist/flow/utils/utils.js.map +1 -0
  140. package/dist/index.d.ts +20 -1
  141. package/dist/index.js +21 -1
  142. package/dist/index.js.map +1 -1
  143. package/dist/list-plugin.js +1 -1
  144. package/dist/list-plugin.js.map +1 -1
  145. package/dist/now-attach-plugin.d.ts +1 -0
  146. package/dist/now-attach-plugin.js +10 -10
  147. package/dist/now-attach-plugin.js.map +1 -1
  148. package/dist/now-ref-plugin.js +1 -1
  149. package/dist/now-ref-plugin.js.map +1 -1
  150. package/dist/record-plugin.d.ts +29 -0
  151. package/dist/record-plugin.js +66 -7
  152. package/dist/record-plugin.js.map +1 -1
  153. package/dist/repack/index.d.ts +2 -0
  154. package/dist/repack/index.js +8 -0
  155. package/dist/repack/index.js.map +1 -1
  156. package/dist/rest-api-plugin.js +54 -44
  157. package/dist/rest-api-plugin.js.map +1 -1
  158. package/dist/server-module-plugin/index.d.ts +10 -0
  159. package/dist/server-module-plugin/index.js +83 -59
  160. package/dist/server-module-plugin/index.js.map +1 -1
  161. package/dist/service-catalog/catalog-clientscript-plugin.d.ts +2 -0
  162. package/dist/service-catalog/catalog-clientscript-plugin.js +117 -0
  163. package/dist/service-catalog/catalog-clientscript-plugin.js.map +1 -0
  164. package/dist/service-catalog/catalog-item-plugin.d.ts +2 -0
  165. package/dist/service-catalog/catalog-item-plugin.js +115 -0
  166. package/dist/service-catalog/catalog-item-plugin.js.map +1 -0
  167. package/dist/service-catalog/catalog-ui-policy-plugin.d.ts +2 -0
  168. package/dist/service-catalog/catalog-ui-policy-plugin.js +266 -0
  169. package/dist/service-catalog/catalog-ui-policy-plugin.js.map +1 -0
  170. package/dist/service-catalog/index.d.ts +5 -0
  171. package/dist/service-catalog/index.js +22 -0
  172. package/dist/service-catalog/index.js.map +1 -0
  173. package/dist/service-catalog/record-to-shape.d.ts +6 -0
  174. package/dist/service-catalog/record-to-shape.js +93 -0
  175. package/dist/service-catalog/record-to-shape.js.map +1 -0
  176. package/dist/service-catalog/sc-record-producer-plugin.d.ts +2 -0
  177. package/dist/service-catalog/sc-record-producer-plugin.js +140 -0
  178. package/dist/service-catalog/sc-record-producer-plugin.js.map +1 -0
  179. package/dist/service-catalog/service-catalog-base.d.ts +311 -0
  180. package/dist/service-catalog/service-catalog-base.js +542 -0
  181. package/dist/service-catalog/service-catalog-base.js.map +1 -0
  182. package/dist/service-catalog/service-catalog-diagnostics.d.ts +45 -0
  183. package/dist/service-catalog/service-catalog-diagnostics.js +172 -0
  184. package/dist/service-catalog/service-catalog-diagnostics.js.map +1 -0
  185. package/dist/service-catalog/shape-to-record.d.ts +8 -0
  186. package/dist/service-catalog/shape-to-record.js +235 -0
  187. package/dist/service-catalog/shape-to-record.js.map +1 -0
  188. package/dist/service-catalog/utils.d.ts +323 -0
  189. package/dist/service-catalog/utils.js +1216 -0
  190. package/dist/service-catalog/utils.js.map +1 -0
  191. package/dist/service-catalog/variable-helper.d.ts +43 -0
  192. package/dist/service-catalog/variable-helper.js +92 -0
  193. package/dist/service-catalog/variable-helper.js.map +1 -0
  194. package/dist/service-catalog/variable-set-plugin.d.ts +2 -0
  195. package/dist/service-catalog/variable-set-plugin.js +175 -0
  196. package/dist/service-catalog/variable-set-plugin.js.map +1 -0
  197. package/dist/service-catalog/variables-transform.d.ts +139 -0
  198. package/dist/service-catalog/variables-transform.js +403 -0
  199. package/dist/service-catalog/variables-transform.js.map +1 -0
  200. package/dist/sla/sla-validators.d.ts +61 -0
  201. package/dist/sla/sla-validators.js +224 -0
  202. package/dist/sla/sla-validators.js.map +1 -0
  203. package/dist/sla-plugin.d.ts +5 -0
  204. package/dist/sla-plugin.js +280 -0
  205. package/dist/sla-plugin.js.map +1 -0
  206. package/dist/static-content-plugin.js +25 -2
  207. package/dist/static-content-plugin.js.map +1 -1
  208. package/dist/table-plugin.js +32 -15
  209. package/dist/table-plugin.js.map +1 -1
  210. package/dist/ui-page-plugin.js +832 -19
  211. package/dist/ui-page-plugin.js.map +1 -1
  212. package/dist/ui-policy-plugin.js +5 -7
  213. package/dist/ui-policy-plugin.js.map +1 -1
  214. package/dist/utils.d.ts +10 -1
  215. package/dist/utils.js +16 -0
  216. package/dist/utils.js.map +1 -1
  217. package/dist/ux-list-menu-config-plugin.d.ts +2 -0
  218. package/dist/ux-list-menu-config-plugin.js +292 -0
  219. package/dist/ux-list-menu-config-plugin.js.map +1 -0
  220. package/dist/workspace-plugin/chrome-tab.d.ts +2 -0
  221. package/dist/workspace-plugin/chrome-tab.js +46 -0
  222. package/dist/workspace-plugin/chrome-tab.js.map +1 -0
  223. package/dist/workspace-plugin/constants.d.ts +52 -0
  224. package/dist/workspace-plugin/constants.js +56 -0
  225. package/dist/workspace-plugin/constants.js.map +1 -0
  226. package/dist/workspace-plugin/fluent-utils.d.ts +9 -0
  227. package/dist/workspace-plugin/fluent-utils.js +60 -0
  228. package/dist/workspace-plugin/fluent-utils.js.map +1 -0
  229. package/dist/workspace-plugin/page.d.ts +8 -0
  230. package/dist/workspace-plugin/page.js +108 -0
  231. package/dist/workspace-plugin/page.js.map +1 -0
  232. package/dist/workspace-plugin/screen.d.ts +1 -0
  233. package/dist/workspace-plugin/screen.js +38 -0
  234. package/dist/workspace-plugin/screen.js.map +1 -0
  235. package/dist/workspace-plugin/templates/index.d.ts +10 -0
  236. package/dist/workspace-plugin/templates/index.js +20 -0
  237. package/dist/workspace-plugin/templates/index.js.map +1 -0
  238. package/dist/workspace-plugin/templates/record-page-composition.d.ts +1 -0
  239. package/dist/workspace-plugin/templates/record-page-composition.js +4043 -0
  240. package/dist/workspace-plugin/templates/record-page-composition.js.map +1 -0
  241. package/dist/workspace-plugin/templates/record-page-data.d.ts +1 -0
  242. package/dist/workspace-plugin/templates/record-page-data.js +527 -0
  243. package/dist/workspace-plugin/templates/record-page-data.js.map +1 -0
  244. package/dist/workspace-plugin/templates/record-page-interalEventMappings.d.ts +1 -0
  245. package/dist/workspace-plugin/templates/record-page-interalEventMappings.js +39 -0
  246. package/dist/workspace-plugin/templates/record-page-interalEventMappings.js.map +1 -0
  247. package/dist/workspace-plugin/templates/record-page-layoutModel.d.ts +1 -0
  248. package/dist/workspace-plugin/templates/record-page-layoutModel.js +55 -0
  249. package/dist/workspace-plugin/templates/record-page-layoutModel.js.map +1 -0
  250. package/dist/workspace-plugin/templates/record-page-properties.d.ts +1 -0
  251. package/dist/workspace-plugin/templates/record-page-properties.js +135 -0
  252. package/dist/workspace-plugin/templates/record-page-properties.js.map +1 -0
  253. package/dist/workspace-plugin/templates/record-page.d.ts +3 -0
  254. package/dist/workspace-plugin/templates/record-page.js +8 -0
  255. package/dist/workspace-plugin/templates/record-page.js.map +1 -0
  256. package/dist/workspace-plugin.d.ts +2 -0
  257. package/dist/workspace-plugin.js +453 -0
  258. package/dist/workspace-plugin.js.map +1 -0
  259. package/package.json +10 -12
  260. package/src/acl-plugin.ts +16 -1
  261. package/src/applicability-plugin.ts +82 -0
  262. package/src/atf/test-plugin.ts +6 -3
  263. package/src/basic-syntax-plugin.ts +10 -1
  264. package/src/business-rule-plugin.ts +2 -1
  265. package/src/call-expression-plugin.ts +2 -130
  266. package/src/column/column-to-record.ts +54 -8
  267. package/src/column-plugin.ts +29 -13
  268. package/src/dashboard/dashboard-component-property-defaults.ts +277 -0
  269. package/src/dashboard/dashboard-component-resolver.ts +69 -0
  270. package/src/dashboard/dashboard-plugin.ts +450 -0
  271. package/src/data-plugin.ts +67 -139
  272. package/src/email-notification-plugin.ts +850 -0
  273. package/src/flow/constants/flow-plugin-constants.ts +79 -0
  274. package/src/flow/flow-logic/flow-logic-constants.ts +120 -0
  275. package/src/flow/flow-logic/flow-logic-diagnostics.ts +591 -0
  276. package/src/flow/flow-logic/flow-logic-plugin-helpers.ts +2550 -0
  277. package/src/flow/flow-logic/flow-logic-plugin.ts +337 -0
  278. package/src/flow/flow-logic/flow-logic-shapes.ts +215 -0
  279. package/src/flow/plugins/approval-rules-plugin.ts +48 -0
  280. package/src/flow/plugins/flow-action-definition-plugin.ts +295 -0
  281. package/src/flow/plugins/flow-data-pill-plugin.ts +258 -0
  282. package/src/flow/plugins/flow-definition-plugin.ts +2173 -0
  283. package/src/flow/plugins/flow-diagnostics-plugin.ts +280 -0
  284. package/src/flow/plugins/flow-instance-plugin.ts +1499 -0
  285. package/src/flow/plugins/flow-trigger-instance-plugin.ts +444 -0
  286. package/src/flow/plugins/inline-script-plugin.ts +83 -0
  287. package/src/flow/plugins/step-definition-plugin.ts +67 -0
  288. package/src/flow/plugins/step-instance-plugin.ts +431 -0
  289. package/src/flow/plugins/trigger-plugin.ts +95 -0
  290. package/src/flow/plugins/wfa-datapill-plugin.ts +213 -0
  291. package/src/flow/utils/approval-rules-processor.ts +298 -0
  292. package/src/flow/utils/built-in-complex-objects.ts +81 -0
  293. package/src/flow/utils/complex-object-resolver.ts +875 -0
  294. package/src/flow/utils/complex-objects.ts +656 -0
  295. package/src/flow/utils/data-pill-shapes.ts +165 -0
  296. package/src/flow/utils/datapill-transformer.ts +632 -0
  297. package/src/flow/utils/flow-constants.ts +285 -0
  298. package/src/flow/utils/flow-io-to-record.ts +533 -0
  299. package/src/flow/utils/flow-shapes.ts +296 -0
  300. package/src/flow/utils/flow-to-xml.ts +318 -0
  301. package/src/flow/utils/flow-variable-processor.ts +100 -0
  302. package/src/flow/utils/label-cache-parser.ts +37 -0
  303. package/src/flow/utils/label-cache-processor.ts +870 -0
  304. package/src/flow/utils/pill-string-parser.ts +375 -0
  305. package/src/flow/utils/schema-to-flow-object.ts +385 -0
  306. package/src/flow/utils/service-catalog.ts +174 -0
  307. package/src/flow/utils/utils.ts +395 -0
  308. package/src/index.ts +20 -1
  309. package/src/list-plugin.ts +1 -1
  310. package/src/now-attach-plugin.ts +14 -11
  311. package/src/now-ref-plugin.ts +1 -1
  312. package/src/record-plugin.ts +76 -11
  313. package/src/repack/index.ts +14 -0
  314. package/src/rest-api-plugin.ts +62 -50
  315. package/src/server-module-plugin/index.ts +112 -86
  316. package/src/service-catalog/catalog-clientscript-plugin.ts +140 -0
  317. package/src/service-catalog/catalog-item-plugin.ts +162 -0
  318. package/src/service-catalog/catalog-ui-policy-plugin.ts +324 -0
  319. package/src/service-catalog/index.ts +5 -0
  320. package/src/service-catalog/record-to-shape.ts +109 -0
  321. package/src/service-catalog/sc-record-producer-plugin.ts +201 -0
  322. package/src/service-catalog/service-catalog-base.ts +600 -0
  323. package/src/service-catalog/service-catalog-diagnostics.ts +254 -0
  324. package/src/service-catalog/shape-to-record.ts +279 -0
  325. package/src/service-catalog/utils.ts +1455 -0
  326. package/src/service-catalog/variable-helper.ts +135 -0
  327. package/src/service-catalog/variable-set-plugin.ts +197 -0
  328. package/src/service-catalog/variables-transform.ts +438 -0
  329. package/src/sla/sla-validators.ts +331 -0
  330. package/src/sla-plugin.ts +358 -0
  331. package/src/static-content-plugin.ts +25 -2
  332. package/src/table-plugin.ts +49 -16
  333. package/src/ui-page-plugin.ts +1063 -20
  334. package/src/ui-policy-plugin.ts +5 -9
  335. package/src/utils.ts +24 -1
  336. package/src/ux-list-menu-config-plugin.ts +312 -0
  337. package/src/workspace-plugin/chrome-tab.ts +44 -0
  338. package/src/workspace-plugin/constants.ts +53 -0
  339. package/src/workspace-plugin/fluent-utils.ts +60 -0
  340. package/src/workspace-plugin/page.ts +139 -0
  341. package/src/workspace-plugin/screen.ts +34 -0
  342. package/src/workspace-plugin/templates/index.ts +17 -0
  343. package/src/workspace-plugin/templates/record-page-composition.ts +4051 -0
  344. package/src/workspace-plugin/templates/record-page-data.ts +523 -0
  345. package/src/workspace-plugin/templates/record-page-interalEventMappings.ts +35 -0
  346. package/src/workspace-plugin/templates/record-page-layoutModel.ts +51 -0
  347. package/src/workspace-plugin/templates/record-page-properties.ts +131 -0
  348. package/src/workspace-plugin/templates/record-page.ts +6 -0
  349. package/src/workspace-plugin.ts +574 -0
@@ -1,7 +1,27 @@
1
- import { CallExpressionShape, Plugin } from '@servicenow/sdk-build-core'
2
- import { isSNScope } from '@servicenow/sdk-build-core'
1
+ import {
2
+ CallExpressionShape,
3
+ Plugin,
4
+ Shape,
5
+ Database,
6
+ path,
7
+ SourceFileShape,
8
+ isSNScope,
9
+ zipSync,
10
+ unzipSync,
11
+ type Logger,
12
+ type Diagnostics,
13
+ type Project,
14
+ FileSystem,
15
+ type Record,
16
+ type NowConfig,
17
+ type Factory,
18
+ type Transform,
19
+ } from '@servicenow/sdk-build-core'
3
20
  import { XMLParser, XMLBuilder, type X2jOptions, type XmlBuilderOptions } from 'fast-xml-parser'
21
+ import { create } from 'xmlbuilder2'
4
22
  import { NowIdShape } from './now-id-plugin'
23
+ import { CHUNK_SIZE, chunkData, generateId } from './static-content-plugin'
24
+ import { sha256 } from './now-attach-plugin'
5
25
 
6
26
  const parserOptions: X2jOptions = {
7
27
  ignoreAttributes: false,
@@ -31,6 +51,51 @@ const builderOptions: XmlBuilderOptions = {
31
51
  }
32
52
 
33
53
  const POLARIS_APPSHELL_THEME_ID = 'c86a62e2c7022010099a308dc7c26022'
54
+ const BYOUI_ARTIFACT_NAME_SUFFIX = 'BYOUI Files'
55
+ // Matches the prefix HtmlImportPlugin prepends when it resolves an `import x from '*.html'`
56
+ // identifier. Its presence on the html value means the developer used the import variable,
57
+ // not an inline string — the only case where a source artifact should be created.
58
+ const HTML_IMPORT_PREFIX = '<!-- @fluent-import-html'
59
+
60
+ // sys_update_xml.payload has a max length of 4,096,000 characters. The artifact content goes
61
+ // through two rounds of base64 encoding with gzip compression in between:
62
+ // raw → base64 → gzip → base64 (stored in sys_attachment_doc.data)
63
+ // This means payload_chars ≈ raw_bytes × (4/3) × C × (4/3) = raw_bytes × 16C/9, where C is the
64
+ // gzip compression ratio. For typical TS/JS source files (C ≈ 0.25), 4 MB of raw source produces
65
+ // ~1.8 MB of payload data — safely within the 4,096,000-character limit.
66
+ const MAX_FILE_SIZE = 2 * 1024 * 1024 // 2 MB — per-file guard (half of total budget)
67
+ const MAX_TOTAL_SIZE = 4 * 1024 * 1024 // 4 MB — derived from sys_update_xml.payload limit
68
+
69
+ /**
70
+ * Relationship configuration for source artifact tables.
71
+ *
72
+ * Defines the hierarchy: M2M → Artifact → Attachment → Attachment Docs
73
+ */
74
+ const SOURCE_ARTIFACT_RELATIONSHIPS = {
75
+ sn_glider_source_artifact_m2m: {
76
+ via: 'application_file',
77
+ descendant: true,
78
+ relationships: {
79
+ sn_glider_source_artifact: {
80
+ via: 'source_artifact',
81
+ inverse: true,
82
+ descendant: true,
83
+ relationships: {
84
+ sys_attachment: {
85
+ via: 'table_sys_id',
86
+ descendant: true,
87
+ relationships: {
88
+ sys_attachment_doc: {
89
+ via: 'sys_attachment',
90
+ descendant: true,
91
+ },
92
+ },
93
+ },
94
+ },
95
+ },
96
+ },
97
+ },
98
+ }
34
99
 
35
100
  const parser = new XMLParser(parserOptions)
36
101
 
@@ -74,7 +139,10 @@ window.g_ck = "$[gs.getSession().getSessionToken() || gs.getSessionToken()]";
74
139
  const nodeTransformer = (nodes: any[]) => {
75
140
  for (let i = 0; i < nodes.length; i++) {
76
141
  const node = nodes[i]
77
- const tag = Object.keys(node)[0]!
142
+ const tag = Object.keys(node)[0]
143
+ if (!tag) {
144
+ continue
145
+ }
78
146
  if (tag === 'sdk:now-ux-globals') {
79
147
  const themeId = node[':@']?.['@_theme-id']
80
148
  nodes.splice(i, 1, ...nowUxGlobals(themeId))
@@ -92,7 +160,24 @@ export const UiPagePlugin = Plugin.create({
92
160
  name: 'UiPagePlugin',
93
161
  records: {
94
162
  sys_ui_page: {
95
- toShape(record) {
163
+ composite: true,
164
+ coalesce: ['endpoint'],
165
+ relationships: SOURCE_ARTIFACT_RELATIONSHIPS,
166
+ async toShape(record, { descendants, fs, project, config, logger, diagnostics }) {
167
+ const shapeWithSourceArtifacts = await getShapeWithSourceArtifacts(record, descendants, {
168
+ fs,
169
+ project,
170
+ config,
171
+ logger,
172
+ diagnostics,
173
+ })
174
+ if (shapeWithSourceArtifacts) {
175
+ return {
176
+ success: true,
177
+ value: shapeWithSourceArtifacts,
178
+ }
179
+ }
180
+
96
181
  return {
97
182
  success: true,
98
183
  value: new CallExpressionShape({
@@ -113,13 +198,125 @@ export const UiPagePlugin = Plugin.create({
113
198
  }),
114
199
  }
115
200
  },
201
+
202
+ async toFile(uiPage, { config, descendants, transform }) {
203
+ if (!uiPage.has('endpoint')) {
204
+ return { success: false }
205
+ }
206
+
207
+ // Only use custom serialization if source artifacts are present
208
+ const sourceArtifacts = descendants.query('sn_glider_source_artifact_m2m')
209
+ if (sourceArtifacts.length === 0) {
210
+ // No source artifacts, use default serialization
211
+ return { success: false }
212
+ }
213
+
214
+ const uiPagePropsToSerialize = [
215
+ 'name',
216
+ 'endpoint',
217
+ 'description',
218
+ 'direct',
219
+ 'category',
220
+ 'html',
221
+ 'client_script',
222
+ 'processing_script',
223
+ ]
224
+
225
+ const result = await serializeWithArtifact(uiPage, uiPagePropsToSerialize, {
226
+ config,
227
+ descendants,
228
+ transform,
229
+ })
230
+
231
+ return {
232
+ success: true,
233
+ value: result,
234
+ }
235
+ },
236
+ },
237
+
238
+ // These records are embedded as descendants of sys_ui_page XML but are also
239
+ // downloaded as standalone records during `init + transform`. Defining coalesce
240
+ // here ensures their sys_ids are registered in keys.ts during transform, so
241
+ // subsequent builds by any user reuse the same IDs rather than generating fresh UUIDs.
242
+
243
+ sn_glider_source_artifact: {
244
+ coalesce: ['name'],
245
+ relationships:
246
+ SOURCE_ARTIFACT_RELATIONSHIPS.sn_glider_source_artifact_m2m.relationships.sn_glider_source_artifact
247
+ .relationships,
248
+
249
+ async toShape(record) {
250
+ return { success: true, value: Shape.noOp(record) }
251
+ },
252
+
253
+ // Custom diff: attachments use hash-based immutable IDs. New content → new attachment ID.
254
+ // Stale cleanup is handled via delete_multiple in serializeWithArtifact, so we must NOT
255
+ // emit DELETE records here — the default diff would conflict with that mechanism.
256
+ async diff(existing, incoming) {
257
+ if (incoming.query().length === 0 || existing.query().length === 0) {
258
+ return {
259
+ success: true,
260
+ value: incoming.query().length === 0 ? new Database() : new Database(incoming.query()),
261
+ }
262
+ }
263
+
264
+ const changedRecords = []
265
+ const existingArtifacts = existing.query('sn_glider_source_artifact')
266
+ const incomingArtifacts = incoming.query('sn_glider_source_artifact')
267
+ const incomingAttachments = incoming.query('sys_attachment')
268
+ const incomingAttachmentDocs = incoming.query('sys_attachment_doc')
269
+
270
+ for (const incomingArtifact of incomingArtifacts) {
271
+ const existingArtifact = existingArtifacts.find(
272
+ (a) => a.getId().getValue() === incomingArtifact.getId().getValue()
273
+ )
274
+
275
+ changedRecords.push(existingArtifact ? existingArtifact.merge(incomingArtifact) : incomingArtifact)
276
+
277
+ // Always add new attachments — unique hash-based IDs per content version.
278
+ const artifactAttachments = incomingAttachments.filter(
279
+ (att) => att.get('table_sys_id').toString().getValue() === incomingArtifact.getId().getValue()
280
+ )
281
+ for (const attachment of artifactAttachments) {
282
+ changedRecords.push(attachment)
283
+ changedRecords.push(
284
+ ...incomingAttachmentDocs.filter(
285
+ (doc) =>
286
+ doc.get('sys_attachment').toString().getValue() === attachment.getId().getValue()
287
+ )
288
+ )
289
+ }
290
+ }
291
+
292
+ return { success: true, value: new Database(changedRecords) }
293
+ },
294
+ },
295
+
296
+ sn_glider_source_artifact_m2m: {
297
+ coalesce: ['application_file', 'source_artifact'],
298
+ relationships: SOURCE_ARTIFACT_RELATIONSHIPS.sn_glider_source_artifact_m2m.relationships,
299
+
300
+ async toShape(record) {
301
+ return { success: true, value: Shape.noOp(record) }
302
+ },
303
+
304
+ async toFile() {
305
+ // Page M2Ms are already in handledGuids as descendants of sys_ui_page, so
306
+ // this handler only runs for asset M2Ms (application_file = sys_ux_lib_asset
307
+ // sys_id). Those are embedded in each sys_ux_lib_asset XML by
308
+ // static-content-plugin via sourceArtifactRelationships. Return success with
309
+ // no output to mark them as handled and prevent RecordPlugin's catch-all from
310
+ // serializing them as standalone XMLs.
311
+ return { success: true, value: [] }
312
+ },
116
313
  },
117
314
  },
118
315
  shapes: [
119
316
  {
120
317
  shape: CallExpressionShape,
121
318
  fileTypes: ['fluent'],
122
- async toRecord(callExpression, { config, factory, diagnostics }) {
319
+ async toRecord(callExpression, { config, factory, diagnostics, fs, project, logger }) {
123
320
  if (callExpression.getCallee() !== 'UiPage') {
124
321
  return { success: false }
125
322
  }
@@ -133,6 +330,38 @@ export const UiPagePlugin = Plugin.create({
133
330
  }
134
331
  const name = endpoint.asString().getValue().replace(`${scope}_`, '').replace(/\.do$/, '')
135
332
  let html = arg.get('html').toString().getValue()
333
+ let sourceFilePaths: string[] = []
334
+ let assetNames: string[] = []
335
+
336
+ // HtmlImportPlugin (which runs before toRecord) resolves `html: _html` identifiers
337
+ // to the file content and prepends an HTML_IMPORT_PREFIX warning comment.
338
+ // Source artifacts are only created when that prefix is present, meaning the html
339
+ // argument actually referenced an imported .html file.
340
+ // An inline string (e.g. `html: '<h1>...</h1>'`) never gets the prefix, so an
341
+ // unrelated `.html` import in the same file does NOT trigger artifact creation.
342
+ if (html.trimStart().startsWith(HTML_IMPORT_PREFIX)) {
343
+ const originalNode = callExpression.getOriginalNode()
344
+ const sourceFile = originalNode.getSourceFile()
345
+ // biome-ignore lint/suspicious/noExplicitAny: ts-morph ImportDeclaration type not exported
346
+ const htmlImport = (sourceFile.getImportDeclarations() as any[]).find((imp) =>
347
+ imp.getModuleSpecifierValue().endsWith('.html')
348
+ )
349
+ if (htmlImport) {
350
+ const relativeHtmlPath = htmlImport.getModuleSpecifierValue()
351
+ const sourceFileDir = path.dirname(sourceFile.getFilePath())
352
+ const absoluteHtmlPath = path.resolve(sourceFileDir, relativeHtmlPath)
353
+ const manifest = getUIPageSourceFilePaths(
354
+ absoluteHtmlPath,
355
+ fs,
356
+ logger,
357
+ config,
358
+ project.getRootDir()
359
+ )
360
+ sourceFilePaths = manifest.files
361
+ assetNames = manifest.assetNames
362
+ }
363
+ }
364
+
136
365
  if (html) {
137
366
  try {
138
367
  const nodes = parser.parse(html)
@@ -148,25 +377,839 @@ export const UiPagePlugin = Plugin.create({
148
377
  }
149
378
  }
150
379
 
380
+ const record = await factory.createRecord({
381
+ source: callExpression,
382
+ table: 'sys_ui_page',
383
+ explicitId: arg.get('$id'),
384
+ properties: arg.transform(({ $ }) => ({
385
+ name: $.val(name),
386
+ endpoint: $.val(endpoint),
387
+ description: $,
388
+ direct: $.def(false),
389
+ category: $,
390
+ html: $.val(html).toCdata(),
391
+ client_script: $.from('clientScript').toCdata(),
392
+ processing_script: $.from('processingScript').toCdata(),
393
+ })),
394
+ })
395
+
396
+ // Build source artifact if source files are present in the manifest
397
+ if (sourceFilePaths.length > 0) {
398
+ logger.debug(`Found ${sourceFilePaths.length} source files in manifest`)
399
+
400
+ // Manifest files already contain paths relative to project root
401
+ const files = sourceFilePaths.map((file) => file.replace(/\\/g, '/'))
402
+ const prebuildPath = path.join(project.getRootDir(), 'now.prebuild.mjs')
403
+ if (FileSystem.existsSync(fs, prebuildPath)) {
404
+ files.push('now.prebuild.mjs')
405
+ }
406
+
407
+ const artifactName = `${endpoint.getValue()} - ${BYOUI_ARTIFACT_NAME_SUFFIX}`
408
+ const sourceArtifactRecords = await buildArtifact(
409
+ artifactName,
410
+ files,
411
+ record,
412
+ {
413
+ fs,
414
+ project,
415
+ factory,
416
+ config,
417
+ logger,
418
+ diagnostics,
419
+ },
420
+ assetNames
421
+ )
422
+
423
+ if (sourceArtifactRecords.length > 0) {
424
+ record.with(...sourceArtifactRecords)
425
+ } else {
426
+ diagnostics.warn(
427
+ record,
428
+ 'No source artifact records were created despite source files being present'
429
+ )
430
+ }
431
+ }
432
+
151
433
  return {
152
434
  success: true,
153
- value: await factory.createRecord({
154
- source: callExpression,
155
- table: 'sys_ui_page',
156
- explicitId: arg.get('$id'),
157
- properties: arg.transform(({ $ }) => ({
158
- name: $.val(name),
159
- endpoint: $.val(endpoint),
160
- description: $,
161
- direct: $.def(false),
162
- category: $,
163
- html: $.val(html).toCdata(),
164
- client_script: $.from('clientScript').toCdata(),
165
- processing_script: $.from('processingScript').toCdata(),
166
- })),
167
- }),
435
+ value: record,
168
436
  }
169
437
  },
170
438
  },
171
439
  ],
172
440
  })
441
+
442
+ /**
443
+ * Reads source file paths from the UI source manifest file.
444
+ *
445
+ * The manifest is generated by isomorphic-rollup's sourceManifest plugin during build.
446
+ * It's a JSON file with the structure: { html, entry, files: string[] }
447
+ *
448
+ * @param htmlFilePath - Path to the HTML file
449
+ * @param fs - File system interface
450
+ * @param logger - Logger for diagnostics
451
+ * @param config - NowConfig with staticContentDir
452
+ * @param rootDir - Project root directory
453
+ * @returns Array of source file paths (empty array if manifest not found)
454
+ */
455
+ const getUIPageSourceFilePaths = (
456
+ htmlFilePath: string,
457
+ fs: FileSystem,
458
+ logger: Logger,
459
+ config: NowConfig,
460
+ rootDir: string
461
+ ): { files: string[]; assetNames: string[] } => {
462
+ const empty = { files: [], assetNames: [] }
463
+ try {
464
+ // Derive manifest path from HTML path
465
+ // The manifest is in the build output directory (staticContentDir), not the source directory
466
+ // e.g., src/client/index.html -> dist/static/index.ui-source-manifest.json
467
+ const htmlBasename = path.basename(htmlFilePath, '.html')
468
+ const staticContentAbsDir = path.join(rootDir, config.staticContentDir)
469
+ const manifestPath = path.join(staticContentAbsDir, `${htmlBasename}.ui-source-manifest.json`)
470
+
471
+ // Check if manifest file exists
472
+ try {
473
+ fs.accessSync(manifestPath)
474
+ } catch {
475
+ logger.debug(`No source manifest found at ${manifestPath}`)
476
+ return empty
477
+ }
478
+
479
+ const manifestContent = fs.readFileSync(manifestPath, { encoding: 'utf-8' })
480
+ const manifest = JSON.parse(manifestContent)
481
+
482
+ if (!manifest.files || !Array.isArray(manifest.files)) {
483
+ logger.warn(`Invalid manifest format at ${manifestPath}`)
484
+ return empty
485
+ }
486
+
487
+ // Derive the JS asset name from the manifest's entry field, matching
488
+ // static-content-plugin's formula: path.join(scope, relativePath_without_ext).
489
+ // The manifest file is named after the HTML entry (e.g. index.ui-source-manifest.json)
490
+ // but the JS bundle is named after the JS entry (e.g. main.tsx -> main.jsdbx).
491
+ // Using manifest.entry's basename ensures the names align.
492
+ if (!manifest.entry || typeof manifest.entry !== 'string') {
493
+ logger.warn(`No entry field in manifest at ${manifestPath}`)
494
+ return empty
495
+ }
496
+ const entryBasename = path.basename(manifest.entry, path.extname(manifest.entry))
497
+ const entryAssetName = path.join(config.scope, entryBasename).replace(/\\/g, '/')
498
+
499
+ // Check if a source map bundle also exists in staticContentDir.
500
+ // static-content-plugin names source map assets as: path.join(scope, relativePath.replace('dbx', ''))
501
+ // e.g. main.jsdbx.map -> main.js.map -> scope/main.js.map
502
+ const assetNames = [entryAssetName]
503
+ const sourceMapFilePath = path.join(staticContentAbsDir, `${entryBasename}.jsdbx.map`)
504
+ try {
505
+ fs.accessSync(sourceMapFilePath)
506
+ const sourceMapAssetName = path.join(config.scope, `${entryBasename}.js.map`).replace(/\\/g, '/')
507
+ assetNames.push(sourceMapAssetName)
508
+ } catch {
509
+ // no source map in this build output — skip
510
+ }
511
+
512
+ return { files: manifest.files, assetNames }
513
+ } catch (error) {
514
+ logger.warn(`Failed to read source manifest: ${error}`)
515
+ return empty
516
+ }
517
+ }
518
+
519
+ /**
520
+ * Extracts source files from artifacts and generates a SourceFileShape for the UI page.
521
+ *
522
+ * This function is called during transform to:
523
+ * 1. Find the source artifact associated with the UI page
524
+ * 2. Extract source files to the project directory
525
+ * 3. Generate a .now.ts file that imports the extracted HTML
526
+ *
527
+ * @param uiPageRecord - The UI page record being transformed
528
+ * @param descendants - Database containing descendant records (artifacts, attachments)
529
+ * @param context - Transform context with config, file system, project, diagnostics, and logger
530
+ * @returns SourceFileShape for the UI page, or undefined if no source artifact found
531
+ */
532
+ async function getShapeWithSourceArtifacts(
533
+ uiPageRecord: Record,
534
+ descendants: Database,
535
+ context: { config: NowConfig; fs: FileSystem; project: Project; diagnostics: Diagnostics; logger: Logger }
536
+ ): Promise<Shape | undefined> {
537
+ const sourceArtifact = getSourceArtifact(descendants, { name: new RegExp(`${BYOUI_ARTIFACT_NAME_SUFFIX}$`) })
538
+ if (!sourceArtifact) {
539
+ return undefined
540
+ }
541
+
542
+ const artifactName = sourceArtifact.get('name').toString().getValue()
543
+ context.logger.debug(`Found source artifact ${artifactName}`)
544
+ const unpackedFiles = await extractArtifact(sourceArtifact, descendants, '', {
545
+ fs: context.fs,
546
+ project: context.project,
547
+ logger: context.logger,
548
+ diagnostics: context.diagnostics,
549
+ })
550
+ context.logger.debug(`Unpacked ${unpackedFiles.length} files from artifact ${artifactName}`)
551
+
552
+ const entryHtmlFilePath = unpackedFiles.find((file) => file.endsWith('.html'))
553
+ if (!entryHtmlFilePath) {
554
+ context.logger.debug(`Failed to find entry HTML file in artifact ${artifactName}`)
555
+ return undefined
556
+ }
557
+ const { generatedDir, taxonomy } = context.config
558
+ const tableName = uiPageRecord.getTable()
559
+ const fluentFileDirByTaxonomy =
560
+ tableName && taxonomy.mapping[tableName] ? path.join(generatedDir, taxonomy.mapping[tableName]) : generatedDir
561
+
562
+ const originalFilePath = uiPageRecord.getOriginalFilePath()
563
+ let dirWhereTheFluentFileWillExistEventually = path.dirname(originalFilePath)
564
+ if (context.project.isInMetadataDir(originalFilePath) || !context.project.isInRootDir(originalFilePath)) {
565
+ // context.project.isInMetadataDir(originalFilePath) => init --from downloads XML to metadata dir
566
+ // !context.project.isInRootDir(originalFilePath) => simple transform downloads XML is in a temp metadata dir
567
+ dirWhereTheFluentFileWillExistEventually = fluentFileDirByTaxonomy
568
+ }
569
+
570
+ // need an import statement for the html file; normalize to forward slashes for cross-platform imports
571
+ const htmlFileRelativePath = path
572
+ .relative(dirWhereTheFluentFileWillExistEventually, entryHtmlFilePath)
573
+ .replace(/\\/g, '/')
574
+
575
+ if (htmlFileRelativePath && !context.project.isInFluentDir(originalFilePath)) {
576
+ const endpoint = uiPageRecord.get('endpoint').toString().getValue()
577
+ const description = uiPageRecord.get('description').toString().getValue()
578
+ const category = uiPageRecord.get('category').toString().getValue()
579
+ const direct = uiPageRecord.get('direct').toString().getValue()
580
+ const clientScript = uiPageRecord.get('client_script').toString().getValue()
581
+ const processingScript = uiPageRecord.get('processing_script').toString().getValue()
582
+
583
+ const fileContent = `
584
+ import '@servicenow/sdk/global'
585
+ import { UiPage } from '@servicenow/sdk/core'
586
+ import htmlFile from '${htmlFileRelativePath}'
587
+
588
+ UiPage({
589
+ $id: Now.ID['${uiPageRecord.getId().getValue()}'],
590
+ endpoint: ${JSON.stringify(endpoint)},
591
+ description: ${JSON.stringify(description)},
592
+ category: ${JSON.stringify(category)},
593
+ direct: ${direct},
594
+ html: htmlFile,
595
+ clientScript: ${JSON.stringify(clientScript)},
596
+ processingScript: ${JSON.stringify(processingScript)},
597
+ })
598
+ `
599
+ const newFileName = endpoint.replace('.do', '.now.ts')
600
+ const sourceFileShape = new SourceFileShape({
601
+ source: uiPageRecord,
602
+ path: `${path.join(fluentFileDirByTaxonomy, newFileName)}`,
603
+ content: fileContent,
604
+ })
605
+
606
+ return sourceFileShape
607
+ }
608
+
609
+ return undefined
610
+ }
611
+
612
+ // ─── Source Artifact: Build ───────────────────────────────────────────────────
613
+ //
614
+ // Asset M2M coordination between UiPagePlugin and StaticContentPlugin:
615
+ //
616
+ // UiPagePlugin creates two kinds of M2M records:
617
+ // - Page M2M (application_file = sys_ui_page sys_id): embedded in sys_ui_page XML.
618
+ // - Asset M2Ms (application_file = sys_ux_lib_asset sys_id): embedded in
619
+ // sys_ux_lib_asset XML by StaticContentPlugin (transform direction), or claimed
620
+ // with no output by sn_glider_source_artifact_m2m.toFile (build direction) to
621
+ // prevent RecordPlugin from serializing them as standalone XMLs.
622
+ //
623
+ // A dummy sys_ux_lib_asset is created (but not added to the build database) solely
624
+ // to derive the same stable sys_id that StaticContentPlugin would use, ensuring both
625
+ // plugins always agree on the asset's sys_id.
626
+
627
+ /**
628
+ * Builds a source artifact record from the given files and associates it with the provided metadata record.
629
+ *
630
+ * Reads files from disk, encodes them as base64, compresses with gzip, creates
631
+ * artifact/attachment/attachment_doc records with hash-based IDs, and links to metadata via M2M.
632
+ *
633
+ * @param artifactName - Descriptive name for the artifact (e.g., "home_page.do - BYOUI Files")
634
+ * @param files - Array of file paths relative to project root
635
+ * @param record - Parent metadata record to link to (the artifact will be associated via M2M)
636
+ * @param context - Build context (fs, project, factory, config, logger, diagnostics)
637
+ * @returns Array of records: artifact, attachment, attachment docs, M2M link record, and
638
+ * per-asset M2M records (one per assetName). Returns empty array if no files
639
+ * provided or all files were skipped.
640
+ */
641
+ async function buildArtifact(
642
+ artifactName: string,
643
+ files: string[],
644
+ record: Record,
645
+ context: {
646
+ fs: FileSystem
647
+ project: Project
648
+ factory: Factory
649
+ config: NowConfig
650
+ logger: Logger
651
+ diagnostics: Diagnostics
652
+ },
653
+ assetNames?: string[]
654
+ ): Promise<Record[]> {
655
+ if (files.length === 0) {
656
+ return []
657
+ }
658
+
659
+ const {
660
+ files: attachmentFiles,
661
+ skippedFiles,
662
+ totalSize,
663
+ } = await getAttachmentContent(context.fs, context.project.getRootDir(), files)
664
+
665
+ // Report skipped files as warnings before the empty-content check so the
666
+ // caller knows *why* no records were created (e.g. all files exceeded size limits).
667
+ for (const warning of skippedFiles) {
668
+ context.diagnostics.warn(record, warning)
669
+ }
670
+
671
+ if (!attachmentFiles.size) {
672
+ context.logger.warn(
673
+ `No source artifact records created for "${artifactName}": all ${files.length} source file(s) were skipped`
674
+ )
675
+ return []
676
+ }
677
+
678
+ const artifactRecord = await createArtifactRecord(artifactName, record, context)
679
+ const attachmentRecords = await createAttachmentRecords(context.factory, artifactRecord, attachmentFiles, totalSize)
680
+ const m2mRecord = await createArtifactM2mRecord(artifactRecord, record, context)
681
+
682
+ const totalSizeMB = (totalSize / (1024 * 1024)).toFixed(2)
683
+ context.logger.info(`Built source artifact "${artifactName}" (${totalSizeMB} MB, ${files.length} files)`)
684
+
685
+ const assetM2mRecords = await Promise.all(
686
+ (assetNames ?? []).map((assetName) => createAssetArtifactM2mRecord(artifactRecord, assetName, record, context))
687
+ )
688
+
689
+ return [artifactRecord, ...attachmentRecords, m2mRecord, ...assetM2mRecords]
690
+ }
691
+
692
+ const createArtifactRecord = async (
693
+ artifactName: string,
694
+ metadataRecord: Record,
695
+ context: { factory: Factory; config: NowConfig }
696
+ ): Promise<Record> => {
697
+ return context.factory.createRecord({
698
+ source: metadataRecord.getSource(),
699
+ table: 'sn_glider_source_artifact',
700
+ explicitId: artifactName,
701
+ properties: {
702
+ name: artifactName,
703
+ },
704
+ })
705
+ }
706
+
707
+ const createArtifactM2mRecord = async (
708
+ artifactRecord: Record,
709
+ metadataRecord: Record,
710
+ context: { factory: Factory; config: NowConfig }
711
+ ): Promise<Record> => {
712
+ return context.factory.createRecord({
713
+ source: metadataRecord.getSource(),
714
+ table: 'sn_glider_source_artifact_m2m',
715
+ explicitId: `${metadataRecord.getId().getValue()}-${artifactRecord.getId().getValue()}`,
716
+ properties: {
717
+ application_file: metadataRecord.getId().getValue(),
718
+ source_artifact: artifactRecord.getId().getValue(),
719
+ },
720
+ })
721
+ }
722
+
723
+ /**
724
+ * Creates a sn_glider_source_artifact_m2m record linking the given asset to the source artifact.
725
+ *
726
+ * A dummy sys_ux_lib_asset record is created with the same explicitId as static-content-plugin's
727
+ * real asset so both plugins resolve to the same sys_id. Only the M2M record is returned and
728
+ * added to the page's .with() tree so it enters the build database. static-content-plugin picks
729
+ * up the M2M via sourceArtifactRelationships (application_file = assetSysId) and embeds it in
730
+ * the sys_ux_lib_asset XML. UiPagePlugin's sn_glider_source_artifact_m2m.toFile marks the M2M as
731
+ * handled so RecordPlugin does not serialize it as a standalone XML in fluentFile.getOutput().
732
+ */
733
+ const createAssetArtifactM2mRecord = async (
734
+ artifactRecord: Record,
735
+ entryAssetName: string,
736
+ metadataRecord: Record,
737
+ context: { factory: Factory; config: NowConfig }
738
+ ): Promise<Record> => {
739
+ const dummyAsset = await context.factory.createRecord({
740
+ source: metadataRecord.getSource(),
741
+ table: 'sys_ux_lib_asset',
742
+ explicitId: entryAssetName,
743
+ properties: {
744
+ name: entryAssetName,
745
+ },
746
+ })
747
+
748
+ return context.factory.createRecord({
749
+ source: metadataRecord.getSource(),
750
+ table: 'sn_glider_source_artifact_m2m',
751
+ explicitId: `${dummyAsset.getId().getValue()}-${artifactRecord.getId().getValue()}`,
752
+ properties: {
753
+ application_file: dummyAsset.getId().getValue(),
754
+ source_artifact: artifactRecord.getId().getValue(),
755
+ },
756
+ })
757
+ }
758
+
759
+ /**
760
+ * Creates attachment and attachment_doc records for the artifact.
761
+ *
762
+ * Attachments are compressed using zip and chunked for storage. The attachment ID includes
763
+ * a hash prefix to ensure immutability (each content version gets a unique ID).
764
+ */
765
+ const createAttachmentRecords = async (
766
+ factory: Factory,
767
+ artifactRecord: Record,
768
+ files: Map<string, Buffer>,
769
+ totalSize: number
770
+ ): Promise<Record[]> => {
771
+ // Build zip entries with a fixed mtime for deterministic output.
772
+ // ZIP format only supports dates 1980-2099; we use the minimum valid date.
773
+ // new Date(1982, 0, 1) creates midnight January 1, 1982 in local time,
774
+ // which fflate's local-time date encoding renders identically in any timezone.
775
+ const FIXED_MTIME = new Date(1982, 0, 1)
776
+ const zipEntries: { [path: string]: [Buffer, { mtime: Date }] } = {}
777
+ for (const [filePath, content] of files) {
778
+ zipEntries[filePath] = [content, { mtime: FIXED_MTIME }]
779
+ }
780
+ const zipped = zipSync(zipEntries)
781
+ const compressedData = Buffer.from(zipped)
782
+ const hash = await sha256(compressedData)
783
+
784
+ // Uniform chunking — no special header split
785
+ const allChunks = chunkData(compressedData.toString('base64'))
786
+
787
+ // Deterministic sys_id derived from artifact + content hash — no keys registry entry needed.
788
+ // Same content always produces the same attachment ID without inflating keys.json.
789
+ const attachmentSysId = generateId(artifactRecord.getId().getValue(), 'sys_attachment', hash)
790
+ const attachment = await factory.createRecord({
791
+ source: artifactRecord.getSource(),
792
+ table: 'sys_attachment',
793
+ properties: {
794
+ sys_id: attachmentSysId,
795
+ average_image_color: '',
796
+ chunk_size_bytes: CHUNK_SIZE,
797
+ compressed: true,
798
+ content_type: 'application/zip',
799
+ hash,
800
+ image_height: '',
801
+ image_width: '',
802
+ size_bytes: totalSize,
803
+ size_compressed: compressedData.length,
804
+ file_name: `${artifactRecord.get('name').getValue()}.zip`,
805
+ table_name: artifactRecord.getTable(),
806
+ table_sys_id: artifactRecord.getId().getValue(),
807
+ },
808
+ })
809
+
810
+ const attachmentDocs: Record[] = []
811
+ for (let i = 0; i < allChunks.length; i++) {
812
+ const doc = await factory.createRecord({
813
+ source: artifactRecord.getSource(),
814
+ table: 'sys_attachment_doc',
815
+ properties: {
816
+ sys_id: generateId(attachmentSysId, 'sys_attachment_doc', i),
817
+ data: allChunks[i],
818
+ position: i,
819
+ sys_attachment: attachment.getId().getValue(),
820
+ },
821
+ })
822
+ attachmentDocs.push(doc)
823
+ }
824
+
825
+ return [attachment, ...attachmentDocs]
826
+ }
827
+
828
+ /**
829
+ * Reads files from disk and collects them for attachment storage.
830
+ *
831
+ * Error handling uses two distinct strategies:
832
+ * - **Recoverable** (per-file): files that cannot be read or exceed the per-file size limit are
833
+ * skipped and their paths are collected in the returned `skippedFiles` array.
834
+ * - **Fatal** (aggregate): if adding a file would push the total over `MAX_TOTAL_SIZE`, an error
835
+ * is thrown immediately.
836
+ *
837
+ * @returns Map of file paths to raw buffers, total size in bytes, and array of warning messages for skipped files
838
+ * @throws {Error} if the cumulative size of valid files would exceed `MAX_TOTAL_SIZE`
839
+ */
840
+ const getAttachmentContent = async (
841
+ fs: FileSystem,
842
+ rootDir: string,
843
+ files: string[]
844
+ ): Promise<{ files: Map<string, Buffer>; totalSize: number; skippedFiles: string[] }> => {
845
+ const fileMap = new Map<string, Buffer>()
846
+ let totalSize = 0
847
+ const skippedFiles: string[] = []
848
+
849
+ for (const filePath of files) {
850
+ let content: Buffer
851
+ try {
852
+ const absolutePath = path.join(rootDir, filePath.replace(/^[/\\]/, ''))
853
+ content = fs.readFileSync(absolutePath) as Buffer
854
+ } catch (error) {
855
+ skippedFiles.push(`Failed to read file ${filePath}: ${error}`)
856
+ continue
857
+ }
858
+
859
+ const fileSize = content.length
860
+
861
+ if (fileSize > MAX_FILE_SIZE) {
862
+ const sizeMB = (fileSize / (1024 * 1024)).toFixed(2)
863
+ skippedFiles.push(`Skipping file ${filePath}: exceeds max file size (${sizeMB} MB)`)
864
+ continue
865
+ }
866
+
867
+ if (totalSize + fileSize > MAX_TOTAL_SIZE) {
868
+ const currentMB = (totalSize / (1024 * 1024)).toFixed(2)
869
+ const limitMB = (MAX_TOTAL_SIZE / (1024 * 1024)).toFixed(0)
870
+ throw new Error(`Total artifact size would exceed limit (${currentMB}/${limitMB} MB)`)
871
+ }
872
+
873
+ fileMap.set(filePath, content)
874
+ totalSize += fileSize
875
+ }
876
+
877
+ return { files: fileMap, totalSize, skippedFiles }
878
+ }
879
+
880
+ // ─── Source Artifact: Extract ─────────────────────────────────────────────────
881
+
882
+ /**
883
+ * Extracts and unpacks files from a source artifact record.
884
+ *
885
+ * Finds attachments for the artifact, reconstructs compressed data from chunks,
886
+ * decompresses with unzipSync, and writes files to disk.
887
+ *
888
+ * @param artifactRecord - The source artifact record (type: sn_glider_source_artifact)
889
+ * @param allDescendants - Query interface to access attachment and attachment_doc records
890
+ * @param targetDir - Target directory path relative to project root (use '' for project root)
891
+ * @param context - Context with fs, project, logger, diagnostics
892
+ * @returns Array of file paths that were unpacked (relative to project root)
893
+ */
894
+ async function extractArtifact(
895
+ artifactRecord: Record,
896
+ allDescendants: { query: (table: string) => Record[] },
897
+ targetDir: string = '',
898
+ context: {
899
+ fs: FileSystem
900
+ project: Project
901
+ logger: Logger
902
+ diagnostics: Diagnostics
903
+ }
904
+ ): Promise<string[]> {
905
+ const artifactSysId = artifactRecord.getId().getValue()
906
+
907
+ // Find all attachments for this artifact and select the most recent one
908
+ const allAttachments = allDescendants.query('sys_attachment')
909
+ const matchingAttachments = allAttachments
910
+ .filter((att) => att.get('table_sys_id').toString().getValue() === artifactSysId)
911
+ .sort((a, b) => {
912
+ const aTime = a.get('sys_created_on').toString().getValue()
913
+ const bTime = b.get('sys_created_on').toString().getValue()
914
+ return aTime.localeCompare(bTime)
915
+ })
916
+
917
+ if (matchingAttachments.length === 0) {
918
+ context.logger.debug(`No attachments found for artifact ${artifactSysId}`)
919
+ return []
920
+ }
921
+
922
+ // Use the most recent attachment (last after sorting by creation time)
923
+ const latestAttachment = matchingAttachments.at(-1)
924
+ if (!latestAttachment) {
925
+ return []
926
+ }
927
+
928
+ // Get attachment docs (chunks) for the latest attachment
929
+ const allAttachmentDocs = allDescendants.query('sys_attachment_doc')
930
+ const attachmentDocs = allAttachmentDocs.filter(
931
+ (doc) => doc.get('sys_attachment').toString().getValue() === latestAttachment.getId().getValue()
932
+ )
933
+
934
+ if (attachmentDocs.length === 0) {
935
+ context.logger.debug(`No attachment docs found for attachment ${latestAttachment.getId().getValue()}`)
936
+ return []
937
+ }
938
+
939
+ // Sort docs by position to ensure correct byte order
940
+ const sortedDocs = attachmentDocs.sort(
941
+ (a, b) => Number(a.get('position').toString().getValue()) - Number(b.get('position').toString().getValue())
942
+ )
943
+
944
+ // Reconstruct zip bytes from all chunks uniformly
945
+ const chunks = sortedDocs.map((doc) => Buffer.from(doc.get('data').toString().getValue(), 'base64'))
946
+ const compressedData = Buffer.concat(chunks)
947
+
948
+ // Decompress — returns { [filePath]: Uint8Array }
949
+ const entries = unzipSync(compressedData)
950
+
951
+ const unpackedFiles: string[] = []
952
+
953
+ for (const [filePath, data] of Object.entries(entries)) {
954
+ try {
955
+ const fileContent = Buffer.from(data)
956
+ const absolutePath = path.join(context.project.getRootDir(), targetDir, filePath)
957
+ const dir = path.dirname(absolutePath)
958
+
959
+ context.fs.mkdirSync(dir, { recursive: true })
960
+ context.fs.writeFileSync(absolutePath, fileContent)
961
+ context.project.addFile({ path: absolutePath, content: fileContent.toString('utf-8') })
962
+
963
+ unpackedFiles.push(filePath)
964
+ context.logger.debug(`Extracted file: ${filePath}`)
965
+ } catch (error) {
966
+ context.diagnostics.error(artifactRecord, `Failed to write file ${filePath}: ${error}`)
967
+ }
968
+ }
969
+
970
+ if (unpackedFiles.length > 0) {
971
+ context.logger.info(
972
+ `Extracted ${unpackedFiles.length} files from artifact "${artifactRecord.get('name').toString().getValue()}"`
973
+ )
974
+ }
975
+
976
+ return unpackedFiles
977
+ }
978
+
979
+ /**
980
+ * Finds a source artifact record by ID or name pattern in descendants.
981
+ *
982
+ * @param descendants - Query interface to access source artifact records
983
+ * @param query - Search criteria: use either id (exact sys_id) OR name (regex against name)
984
+ * @returns The matching source artifact record, or undefined if not found
985
+ */
986
+ function getSourceArtifact(
987
+ descendants: { query: (table: string) => Record[] },
988
+ query: { id?: string; name?: RegExp }
989
+ ): Record | undefined {
990
+ return descendants.query('sn_glider_source_artifact').find((record) => {
991
+ if (query.id) {
992
+ return record.getId().getValue() === query.id
993
+ }
994
+ if (query.name) {
995
+ const nameMatch = record.get('name').toString().getValue().match(query.name)
996
+ return nameMatch && nameMatch.length > 0
997
+ }
998
+ return false
999
+ })
1000
+ }
1001
+
1002
+ // ─── Source Artifact: Serialize ───────────────────────────────────────────────
1003
+
1004
+ /**
1005
+ * Serializes a metadata record along with its embedded source artifacts to XML.
1006
+ *
1007
+ * Creates a custom XML format where source artifacts, attachments, M2M records,
1008
+ * and the parent metadata record are all embedded within a single XML file.
1009
+ *
1010
+ * The generated XML contains (in order):
1011
+ * 1. M2M records (sn_glider_source_artifact_m2m)
1012
+ * 2. Artifact records (sn_glider_source_artifact)
1013
+ * 3. Attachment records (sys_attachment)
1014
+ * 4. Attachment doc records (sys_attachment_doc)
1015
+ * 5. Parent metadata record
1016
+ *
1017
+ * Also emits delete_multiple elements to clean up stale attachments per artifact,
1018
+ * since attachments use hash-based IDs that change with each new content version.
1019
+ *
1020
+ * @param metadataRecord - The main metadata record (e.g., sys_ui_page)
1021
+ * @param metadataProps - List of property names to serialize from the metadata record
1022
+ * @param context - Serialization context (descendants, config, transform)
1023
+ */
1024
+ async function serializeWithArtifact(
1025
+ metadataRecord: Record,
1026
+ metadataProps: string[],
1027
+ context: {
1028
+ descendants: { query: (table: string) => Record[] }
1029
+ config: { scope: string; scopeId: string }
1030
+ transform: Transform
1031
+ }
1032
+ ) {
1033
+ const recordUpdate = create().ele('record_update', { table: metadataRecord.getTable() })
1034
+
1035
+ // Embed the metadata record itself at the root, before descendant records
1036
+ const metadataElement = recordUpdate.ele(metadataRecord.getTable(), { action: metadataRecord.getAction() })
1037
+ metadataElement.ele('sys_id').txt(metadataRecord.getId().getValue())
1038
+ metadataElement.ele('sys_scope', { display_value: context.config.scope }).txt(context.config.scopeId)
1039
+
1040
+ const updateName = await context.transform.getUpdateName(metadataRecord)
1041
+ metadataElement.ele('sys_update_name').txt(updateName)
1042
+
1043
+ for (const prop of metadataProps) {
1044
+ const value = metadataRecord.get(prop)
1045
+ if (value.isDefined()) {
1046
+ const stringValue = value.toString().getValue()
1047
+ const contentType = value.toString().getContentType()
1048
+ if (stringValue) {
1049
+ if (contentType === 'plain') {
1050
+ metadataElement.ele(prop).txt(stringValue)
1051
+ } else {
1052
+ // Handle CDATA content
1053
+ // The ]]> sequence terminates CDATA, so we escape it as: ]]]]><![CDATA[>
1054
+ // This creates: <![CDATA[content before]]]]><![CDATA[>content after]]>
1055
+ if (stringValue.includes(']]>')) {
1056
+ // Split by ]]> and create multiple adjacent CDATA sections
1057
+ const parts = stringValue.split(']]>')
1058
+ const propElement = metadataElement.ele(prop)
1059
+
1060
+ // Create adjacent CDATA sections for each part
1061
+ // First part
1062
+ propElement.dat(`${parts[0]}]]`)
1063
+
1064
+ // Remaining parts (each starts with a new CDATA section containing >)
1065
+ for (let i = 1; i < parts.length; i++) {
1066
+ propElement.dat(`>${parts[i]}`)
1067
+ }
1068
+ } else {
1069
+ // No ]]> sequences, safe to use .dat() directly
1070
+ metadataElement.ele(prop).dat(stringValue)
1071
+ }
1072
+ }
1073
+ } else {
1074
+ metadataElement.ele(prop)
1075
+ }
1076
+ }
1077
+ }
1078
+
1079
+ // Embed the M2M record linking this metadata record to its source artifact.
1080
+ // Asset M2Ms (application_file = sys_ux_lib_asset sys_id) are embedded in
1081
+ // each sys_ux_lib_asset XML by static-content-plugin.
1082
+ context.descendants.query('sn_glider_source_artifact_m2m').forEach((m2m) => {
1083
+ const m2mElement = recordUpdate.ele('sn_glider_source_artifact_m2m', {
1084
+ action: m2m.getAction(),
1085
+ })
1086
+ m2mElement.ele('sys_id').txt(m2m.getId().getValue())
1087
+
1088
+ const applicationFile = m2m.get('application_file')
1089
+ if (applicationFile.isDefined()) {
1090
+ const appFileId = applicationFile.isRecord()
1091
+ ? applicationFile.asRecord().getId().getValue()
1092
+ : applicationFile.toString().getValue()
1093
+ m2mElement.ele('application_file').txt(appFileId)
1094
+ }
1095
+
1096
+ const artifactId = m2m.get('source_artifact')
1097
+ if (artifactId.isDefined()) {
1098
+ const artifactSysId = artifactId.isRecord()
1099
+ ? artifactId.asRecord().getId().getValue()
1100
+ : artifactId.toString().getValue()
1101
+ m2mElement.ele('source_artifact').txt(artifactSysId)
1102
+ }
1103
+ })
1104
+
1105
+ // Embed artifact records
1106
+ context.descendants.query('sn_glider_source_artifact').forEach((artifact) => {
1107
+ const artifactElement = recordUpdate.ele('sn_glider_source_artifact', {
1108
+ action: artifact.getAction(),
1109
+ })
1110
+ artifactElement.ele('sys_id').txt(artifact.getId().getValue())
1111
+
1112
+ const artifactName = artifact.get('name')
1113
+ if (artifactName.isDefined()) {
1114
+ artifactElement.ele('name').txt(artifactName.toString().getValue())
1115
+ }
1116
+ })
1117
+
1118
+ // Embed attachment records
1119
+ context.descendants.query('sys_attachment').forEach((attachment) => {
1120
+ const attachmentElement = recordUpdate.ele('sys_attachment', { action: attachment.getAction() })
1121
+ attachmentElement.ele('sys_id').txt(attachment.getId().getValue())
1122
+ attachmentElement.ele('sys_scope', { display_value: context.config.scope }).txt(context.config.scopeId)
1123
+
1124
+ const tableId = attachment.get('table_sys_id')
1125
+ if (tableId.isDefined()) {
1126
+ attachmentElement.ele('table_sys_id').txt(tableId.toString().getValue())
1127
+ }
1128
+
1129
+ const tableName = attachment.get('table_name')
1130
+ if (tableName.isDefined()) {
1131
+ attachmentElement.ele('table_name').txt(tableName.toString().getValue())
1132
+ }
1133
+
1134
+ const attachmentProps = [
1135
+ 'file_name',
1136
+ 'content_type',
1137
+ 'size_bytes',
1138
+ 'size_compressed',
1139
+ 'compressed',
1140
+ 'chunk_size_bytes',
1141
+ 'hash',
1142
+ 'average_image_color',
1143
+ 'image_width',
1144
+ 'image_height',
1145
+ ]
1146
+ for (const prop of attachmentProps) {
1147
+ const value = attachment.get(prop)
1148
+ if (value.isDefined()) {
1149
+ attachmentElement.ele(prop).txt(value.toString().getValue())
1150
+ }
1151
+ }
1152
+ })
1153
+
1154
+ // Embed attachment doc records (chunks)
1155
+ context.descendants.query('sys_attachment_doc').forEach((doc) => {
1156
+ const docElement = recordUpdate.ele('sys_attachment_doc', { action: doc.getAction() })
1157
+ docElement.ele('sys_id').txt(doc.getId().getValue())
1158
+
1159
+ // Link to the attachment
1160
+ const attachmentId = doc.get('sys_attachment')
1161
+ if (attachmentId.isDefined()) {
1162
+ const sysId = attachmentId.isRecord()
1163
+ ? attachmentId.asRecord().getId().getValue()
1164
+ : attachmentId.toString().getValue()
1165
+ docElement.ele('sys_attachment').txt(sysId)
1166
+ }
1167
+
1168
+ const position = doc.get('position')
1169
+ if (position.isDefined()) {
1170
+ docElement.ele('position').txt(position.toString().getValue())
1171
+ }
1172
+
1173
+ const data = doc.get('data')
1174
+ if (data.isDefined()) {
1175
+ docElement.ele('data').txt(data.toString().getValue())
1176
+ }
1177
+ })
1178
+
1179
+ // Emit delete_multiple elements to clean up stale attachments and docs per artifact.
1180
+ // Since attachments use hash-based IDs, each build produces a new attachment ID.
1181
+ // These queries tell the platform to remove any attachment/doc on this artifact
1182
+ // that is not part of the current build.
1183
+ context.descendants.query('sn_glider_source_artifact').forEach((artifact) => {
1184
+ const artifactId = artifact.getId().getValue()
1185
+
1186
+ const currentAttachments = context.descendants
1187
+ .query('sys_attachment')
1188
+ .filter((att) => att.get('table_sys_id').toString().getValue() === artifactId)
1189
+ const currentAttachmentIds = currentAttachments.map((att) => att.getId().getValue())
1190
+
1191
+ const currentDocIds = context.descendants
1192
+ .query('sys_attachment_doc')
1193
+ .filter((doc) => currentAttachmentIds.includes(doc.get('sys_attachment').toString().getValue()))
1194
+ .map((doc) => doc.getId().getValue())
1195
+
1196
+ const attachmentQuery =
1197
+ currentAttachmentIds.length > 0
1198
+ ? `table_sys_id=${artifactId}^sys_idNOT IN${currentAttachmentIds.join(',')}`
1199
+ : `table_sys_id=${artifactId}`
1200
+ recordUpdate.ele('sys_attachment', { action: 'delete_multiple', query: attachmentQuery })
1201
+
1202
+ const docQuery =
1203
+ currentDocIds.length > 0
1204
+ ? `sys_attachment.table_sys_id=${artifactId}^sys_idNOT IN${currentDocIds.join(',')}`
1205
+ : `sys_attachment.table_sys_id=${artifactId}`
1206
+ recordUpdate.ele('sys_attachment_doc', { action: 'delete_multiple', query: docQuery })
1207
+ })
1208
+
1209
+ return {
1210
+ source: metadataRecord,
1211
+ name: `${updateName}.xml`,
1212
+ category: metadataRecord.getInstallCategory(),
1213
+ content: recordUpdate.end({ prettyPrint: true }),
1214
+ }
1215
+ }