@servicenow/sdk-build-plugins 4.1.0 → 4.2.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 (108) hide show
  1. package/dist/acl-plugin.js +13 -4
  2. package/dist/acl-plugin.js.map +1 -1
  3. package/dist/application-menu-plugin.js +1 -0
  4. package/dist/application-menu-plugin.js.map +1 -1
  5. package/dist/atf/step-configs.d.ts +13 -12
  6. package/dist/atf/step-configs.js.map +1 -1
  7. package/dist/atf/test-plugin.d.ts +1 -1
  8. package/dist/atf/test-plugin.js +8 -5
  9. package/dist/atf/test-plugin.js.map +1 -1
  10. package/dist/basic-syntax-plugin.js +51 -13
  11. package/dist/basic-syntax-plugin.js.map +1 -1
  12. package/dist/business-rule-plugin.js.map +1 -1
  13. package/dist/claims-plugin.js +1 -1
  14. package/dist/claims-plugin.js.map +1 -1
  15. package/dist/client-script-plugin.js +5 -17
  16. package/dist/client-script-plugin.js.map +1 -1
  17. package/dist/column/column-helper.d.ts +1 -1
  18. package/dist/column/column-helper.js +46 -2
  19. package/dist/column/column-helper.js.map +1 -1
  20. package/dist/column/column-to-record.js +6 -4
  21. package/dist/column/column-to-record.js.map +1 -1
  22. package/dist/column-plugin.js +106 -27
  23. package/dist/column-plugin.js.map +1 -1
  24. package/dist/data-plugin.d.ts +3 -0
  25. package/dist/data-plugin.js +208 -0
  26. package/dist/data-plugin.js.map +1 -0
  27. package/dist/import-sets-plugin.d.ts +2 -0
  28. package/dist/import-sets-plugin.js +412 -0
  29. package/dist/import-sets-plugin.js.map +1 -0
  30. package/dist/index.d.ts +4 -0
  31. package/dist/index.js +4 -0
  32. package/dist/index.js.map +1 -1
  33. package/dist/json-plugin.d.ts +4 -4
  34. package/dist/json-plugin.js +21 -7
  35. package/dist/json-plugin.js.map +1 -1
  36. package/dist/list-plugin.js +83 -1
  37. package/dist/list-plugin.js.map +1 -1
  38. package/dist/now-attach-plugin.d.ts +35 -0
  39. package/dist/now-attach-plugin.js +317 -0
  40. package/dist/now-attach-plugin.js.map +1 -0
  41. package/dist/now-config-plugin.js +3 -0
  42. package/dist/now-config-plugin.js.map +1 -1
  43. package/dist/now-include-plugin.js +7 -1
  44. package/dist/now-include-plugin.js.map +1 -1
  45. package/dist/package-json-plugin.js +2 -2
  46. package/dist/package-json-plugin.js.map +1 -1
  47. package/dist/record-plugin.d.ts +6 -0
  48. package/dist/record-plugin.js +50 -23
  49. package/dist/record-plugin.js.map +1 -1
  50. package/dist/repack/lint/Rules.js.map +1 -1
  51. package/dist/rest-api-plugin.js +28 -31
  52. package/dist/rest-api-plugin.js.map +1 -1
  53. package/dist/role-plugin.js +1 -0
  54. package/dist/role-plugin.js.map +1 -1
  55. package/dist/server-module-plugin/index.js +15 -2
  56. package/dist/server-module-plugin/index.js.map +1 -1
  57. package/dist/service-portal/widget-plugin.js +4 -1
  58. package/dist/service-portal/widget-plugin.js.map +1 -1
  59. package/dist/static-content-plugin.d.ts +1 -0
  60. package/dist/static-content-plugin.js +4 -3
  61. package/dist/static-content-plugin.js.map +1 -1
  62. package/dist/table-plugin.js +33 -2
  63. package/dist/table-plugin.js.map +1 -1
  64. package/dist/ui-page-plugin.js +2 -1
  65. package/dist/ui-page-plugin.js.map +1 -1
  66. package/dist/ui-policy-plugin.d.ts +2 -0
  67. package/dist/ui-policy-plugin.js +407 -0
  68. package/dist/ui-policy-plugin.js.map +1 -0
  69. package/dist/utils.d.ts +10 -1
  70. package/dist/utils.js +24 -0
  71. package/dist/utils.js.map +1 -1
  72. package/dist/view-plugin.js +1 -1
  73. package/dist/view-plugin.js.map +1 -1
  74. package/package.json +11 -7
  75. package/src/_types/eslint-plugin-es-x.d.ts +17 -0
  76. package/src/_types/md5.js.d.ts +8 -0
  77. package/src/acl-plugin.ts +19 -9
  78. package/src/application-menu-plugin.ts +1 -0
  79. package/src/atf/step-configs.ts +14 -12
  80. package/src/atf/test-plugin.ts +40 -21
  81. package/src/basic-syntax-plugin.ts +61 -13
  82. package/src/business-rule-plugin.ts +7 -4
  83. package/src/claims-plugin.ts +1 -1
  84. package/src/client-script-plugin.ts +8 -22
  85. package/src/column/column-helper.ts +65 -3
  86. package/src/column/column-to-record.ts +6 -4
  87. package/src/column-plugin.ts +141 -39
  88. package/src/data-plugin.ts +266 -0
  89. package/src/import-sets-plugin.ts +542 -0
  90. package/src/index.ts +4 -0
  91. package/src/json-plugin.ts +31 -12
  92. package/src/list-plugin.ts +91 -1
  93. package/src/now-attach-plugin.ts +399 -0
  94. package/src/now-config-plugin.ts +6 -2
  95. package/src/now-include-plugin.ts +8 -1
  96. package/src/package-json-plugin.ts +3 -3
  97. package/src/record-plugin.ts +61 -30
  98. package/src/repack/lint/Rules.ts +1 -10
  99. package/src/rest-api-plugin.ts +45 -51
  100. package/src/role-plugin.ts +1 -0
  101. package/src/server-module-plugin/index.ts +21 -5
  102. package/src/service-portal/widget-plugin.ts +4 -1
  103. package/src/static-content-plugin.ts +2 -2
  104. package/src/table-plugin.ts +47 -7
  105. package/src/ui-page-plugin.ts +2 -1
  106. package/src/ui-policy-plugin.ts +509 -0
  107. package/src/utils.ts +27 -1
  108. package/src/view-plugin.ts +1 -1
@@ -1,5 +1,4 @@
1
- import { CallExpressionShape, Plugin } from '@servicenow/sdk-build-core'
2
- import { unloadBuilder } from '@servicenow/sdk-build-core'
1
+ import { CallExpressionShape, Database, Plugin, unloadBuilder, type Record } from '@servicenow/sdk-build-core'
3
2
  import { NowIdShape } from './now-id-plugin'
4
3
  import {
5
4
  Acl,
@@ -10,6 +9,7 @@ import {
10
9
  Role,
11
10
  Test,
12
11
  UserPreference,
12
+ ImportSet,
13
13
  } from '@servicenow/sdk-core/runtime/app'
14
14
  import {
15
15
  SPWidget,
@@ -23,7 +23,7 @@ import { ScriptAction, ScriptInclude } from '@servicenow/sdk-core/runtime/sys'
23
23
  import { List } from '@servicenow/sdk-core/runtime/ui'
24
24
  import { Table } from '@servicenow/sdk-core/runtime/db'
25
25
  import { RestApi } from '@servicenow/sdk-core/runtime/rest'
26
- import { UiAction, UiPage } from '@servicenow/sdk-core/runtime/ui'
26
+ import { UiAction, UiPage, UiPolicy } from '@servicenow/sdk-core/runtime/ui'
27
27
 
28
28
  export const RecordPlugin = Plugin.create({
29
29
  name: 'RecordPlugin',
@@ -32,31 +32,46 @@ export const RecordPlugin = Plugin.create({
32
32
  getUpdateName(record) {
33
33
  return { success: true, value: `${record.getTable()}_${record.getId().getValue()}` }
34
34
  },
35
- toShape(record) {
35
+ async diff(existing, incoming) {
36
+ const changeDatabase = existing.compare(incoming)
36
37
  return {
37
38
  success: true,
38
- value: new CallExpressionShape({
39
- source: record,
40
- callee: 'Record',
41
- args: [
42
- {
43
- $id: NowIdShape.from(record),
44
- table: record.getTable(),
45
- data: record.transform(
46
- ({ $ }) =>
47
- Object.fromEntries(
48
- record
49
- .keys()
50
- .map((key) => [key, record.get(key).isString() ? $.def('') : $])
51
- ) //to avoid writing empty string values to the code
52
- ),
53
- },
54
- ],
55
- }),
39
+ value: changeDatabase.hasChanges() ? new Database(changeDatabase.query()) : new Database(),
40
+ }
41
+ },
42
+ async toShape(record) {
43
+ const value = new CallExpressionShape({
44
+ source: record,
45
+ callee: 'Record',
46
+ exportName: record.get('sys_name')?.ifDefined()?.asString().getValue(),
47
+ args: [
48
+ {
49
+ $id: NowIdShape.from(record),
50
+ table: record.getTable(),
51
+ data: record.transform(
52
+ ({ $ }) =>
53
+ Object.fromEntries(
54
+ record
55
+ .keys()
56
+ .filter((key) => key !== 'sys_name')
57
+ .map((key) => [key, record.get(key).isString() ? $.def('') : $])
58
+ ) //to avoid writing empty string values to the code
59
+ ),
60
+ },
61
+ ],
62
+ })
63
+
64
+ return {
65
+ success: true,
66
+ value,
56
67
  }
57
68
  },
58
69
  async toFile(record, { config, database, transform }) {
59
- const recordBuilder = unloadBuilder(config)
70
+ const recordBuilder = unloadBuilder({
71
+ scope: config.scope,
72
+ scopeId: config.scopeId,
73
+ table: record.getTable(),
74
+ })
60
75
  const updateName = await transform.getUpdateName(record)
61
76
  const builder = recordBuilder.record(record, updateName)
62
77
 
@@ -101,13 +116,14 @@ export const RecordPlugin = Plugin.create({
101
116
 
102
117
  const record = callExpression.getArgument(0).asObject()
103
118
  const table = record.get('table').asString().getValue()
104
- const tableOwningPlugin = TableOwnership[table]
119
+ const tableOwningPlugin = TableOwnership[table as keyof typeof TableOwnership]
105
120
  if (tableOwningPlugin) {
106
121
  diagnostics.hint(
107
122
  callExpression,
108
123
  `For a better experience, consider using the ${tableOwningPlugin} API`
109
124
  )
110
125
  }
126
+
111
127
  return {
112
128
  success: true,
113
129
  value: await factory.createRecord({
@@ -125,16 +141,25 @@ export const RecordPlugin = Plugin.create({
125
141
  matcher: /\.xml$/,
126
142
  async toRecord(file, { parser, logger }) {
127
143
  try {
128
- const [first, ...rest] = await parser.parsePayload(file)
144
+ const records = await parser.parsePayload(file)
145
+ const recordMap = new Map<string, Record>()
146
+ for (const record of records) {
147
+ const key = `${record.getTable()}::${record.getId().getValue()}`
148
+ const existing = recordMap.get(key)
129
149
 
130
- if (!first) {
131
- return { success: false }
150
+ const merged = existing
151
+ ? existing.merge(record.properties()) // merge properties only to retain the action
152
+ : record
153
+
154
+ recordMap.set(key, merged)
132
155
  }
133
156
 
134
- return {
135
- success: true,
136
- value: first.with(...rest),
157
+ const mergedRecords = Array.from(recordMap.values())
158
+ const [mergedFirst, ...mergedRest] = mergedRecords
159
+ if (!mergedFirst) {
160
+ return { success: false }
137
161
  }
162
+ return { success: true, value: mergedFirst.with(...mergedRest) }
138
163
  } catch (e) {
139
164
  logger.debug(e)
140
165
  return { success: false }
@@ -181,7 +206,13 @@ export const TableOwnership = {
181
206
  sys_ui_action: UiAction.name,
182
207
  sys_ui_action_role: UiAction.name,
183
208
  sys_ui_action_view: UiAction.name,
209
+ sys_ui_policy: UiPolicy.name,
210
+ sys_ui_policy_action: UiPolicy.name,
211
+ sys_ui_policy_rl_action: UiPolicy.name,
184
212
  sysevent_script_action: ScriptAction.name,
185
213
  sys_db_object: Table.name,
186
214
  sys_dictionary: Table.name,
215
+ sys_transform_map: ImportSet.name,
216
+ sys_transform_entry: ImportSet.name,
217
+ sys_transform_script: ImportSet.name,
187
218
  }
@@ -10,15 +10,6 @@ const NO_GLOBAL_THIS = 'ES2020 `globalThis` variable is not supported by the now
10
10
 
11
11
  type LogLevel = 'warn' | 'error'
12
12
 
13
- type ClassMethods = {
14
- [className: string]: string[]
15
- }
16
-
17
- type GlideAPIMapping = {
18
- defaults: ClassMethods
19
- [namespace: string]: ClassMethods
20
- }
21
-
22
13
  type ESLintPropertyRule = {
23
14
  message: string
24
15
  object?: string
@@ -26,7 +17,7 @@ type ESLintPropertyRule = {
26
17
  name?: string
27
18
  }
28
19
 
29
- const glideAPIs: GlideAPIMapping = GlideAPIs
20
+ const glideAPIs = GlideAPIs
30
21
 
31
22
  const globalsAllowList: string[] = ['console', 'fetch']
32
23
 
@@ -11,6 +11,7 @@ import {
11
11
  } from '@servicenow/sdk-build-core'
12
12
  import { ModuleFunctionShape } from './server-module-plugin'
13
13
  import { NowIdShape } from './now-id-plugin'
14
+ import { NowIncludeShape } from './now-include-plugin'
14
15
  import { generateDeprecatedDiagnostics } from './utils'
15
16
 
16
17
  const DEFAULT_MEDIA_TYPE = 'application/json,application/xml,text/xml'
@@ -453,7 +454,7 @@ export const RestApiPlugin = Plugin.create({
453
454
  descendant: true,
454
455
  },
455
456
  },
456
- toShape(record, { descendants }) {
457
+ async toShape(record, { descendants, transform }) {
457
458
  const versions = descendants.query('sys_ws_version')
458
459
  const routes = descendants.query('sys_ws_operation')
459
460
  const headers = descendants.query('sys_ws_header')
@@ -476,6 +477,48 @@ export const RestApiPlugin = Plugin.create({
476
477
  validateAttributesAndAssociations(headers, headerRouteAssociations, 'header')
477
478
  validateAttributesAndAssociations(parameters, paramRouteAssociations, 'query_parameter')
478
479
 
480
+ const routesWithScript = await Promise.all(
481
+ routes.map(async (r) => {
482
+ const script = await NowIncludeShape.fromRecord(r, r.get('operation_script'), transform)
483
+ return r
484
+ .transform(({ $ }) => ({
485
+ $id: $.val(NowIdShape.from(r)),
486
+ name: $,
487
+ active: $.toBoolean().def(true),
488
+ consumes: $,
489
+ method: $.from('http_method').def('GET'),
490
+ script: $.val(script),
491
+ produces: $,
492
+ path: $.from('relative_path').def('/'),
493
+ enforceAcl: $.val(splitAcls(r.get('enforce_acl'))).def([DEFAULT_REST_ENFORCED_ACL]),
494
+ authorization: $.from('requires_acl_authorization').toBoolean().def(true),
495
+ authentication: $.from('requires_authentication').toBoolean().def(true),
496
+ internalRole: $.from('requires_snc_internal_role').toBoolean().def(true),
497
+ shortDescription: $.from('short_description').def(''),
498
+ requestExample: $.from('request_example').def(''),
499
+ policy: $.from('sys_policy').def(''),
500
+ parameters: $.val(
501
+ routeAttributeTransform(
502
+ paramRouteAssociations,
503
+ parameters,
504
+ r.getId().getValue(),
505
+ 'query_parameter'
506
+ )
507
+ ).def([]),
508
+ headers: $.val(
509
+ routeAttributeTransform(
510
+ headerRouteAssociations,
511
+ headers,
512
+ r.getId().getValue(),
513
+ 'header'
514
+ )
515
+ ).def([]),
516
+ version: $.from('web_service_version').map((v) => getVersionNumber(v, versions)),
517
+ }))
518
+ .withAliasedKeys(routeAliases)
519
+ })
520
+ )
521
+
479
522
  return {
480
523
  success: true,
481
524
  value: new CallExpressionShape({
@@ -496,56 +539,7 @@ export const RestApiPlugin = Plugin.create({
496
539
  shortDescription: $.from('short_description').def(''),
497
540
  policy: $.from('sys_policy').def(''),
498
541
  docLink: $.from('doc_link').def(''),
499
- routes: $.val(
500
- routes.map((r) =>
501
- r
502
- .transform(({ $ }) => ({
503
- $id: $.val(NowIdShape.from(r)),
504
- name: $,
505
- active: $.toBoolean().def(true),
506
- consumes: $,
507
- method: $.from('http_method').def('GET'),
508
- script: $.from('operation_script'),
509
- produces: $,
510
- path: $.from('relative_path').def('/'),
511
- enforceAcl: $.val(splitAcls(r.get('enforce_acl'))).def([
512
- DEFAULT_REST_ENFORCED_ACL,
513
- ]),
514
- authorization: $.from('requires_acl_authorization')
515
- .toBoolean()
516
- .def(true),
517
- authentication: $.from('requires_authentication')
518
- .toBoolean()
519
- .def(true),
520
- internalRole: $.from('requires_snc_internal_role')
521
- .toBoolean()
522
- .def(true),
523
- shortDescription: $.from('short_description').def(''),
524
- requestExample: $.from('request_example').def(''),
525
- policy: $.from('sys_policy').def(''),
526
- parameters: $.val(
527
- routeAttributeTransform(
528
- paramRouteAssociations,
529
- parameters,
530
- r.getId().getValue(),
531
- 'query_parameter'
532
- )
533
- ).def([]),
534
- headers: $.val(
535
- routeAttributeTransform(
536
- headerRouteAssociations,
537
- headers,
538
- r.getId().getValue(),
539
- 'header'
540
- )
541
- ).def([]),
542
- version: $.from('web_service_version').map((v) =>
543
- getVersionNumber(v, versions)
544
- ),
545
- }))
546
- .withAliasedKeys(routeAliases)
547
- )
548
- ).def([]),
542
+ routes: $.val(routesWithScript).def([]),
549
543
  versions: $.val(versionsTransform(versions)).def([]),
550
544
  }))
551
545
  .withAliasedKeys(restDefAliases),
@@ -32,6 +32,7 @@ export const RolePlugin = Plugin.create({
32
32
  success: true,
33
33
  value: new CallExpressionShape({
34
34
  source: record,
35
+ exportName: record.get('name').ifString()?.getValue(),
35
36
  callee: 'Role',
36
37
  args: [
37
38
  record
@@ -223,8 +223,8 @@ function isValidRequireCall(callExpression: ts.CallExpression, requirePath: ts.S
223
223
  return isRequire && !isRelativePath
224
224
  }
225
225
 
226
- function buildParentPathMap(dependencyNodes: DependencyNode[]) {
227
- const importerMap = {}
226
+ function buildParentPathMap(dependencyNodes: DependencyNode[]): { [key: string]: string[] } {
227
+ const importerMap: { [key: string]: string[] } = {}
228
228
  for (const node of dependencyNodes) {
229
229
  let parent = node.parentPackage
230
230
 
@@ -233,7 +233,9 @@ function buildParentPathMap(dependencyNodes: DependencyNode[]) {
233
233
  if (!importerMap[importerMapKey]) {
234
234
  importerMap[importerMapKey] = []
235
235
  }
236
- importerMap[importerMapKey].push(parent.pkgName)
236
+ if (parent.pkgName) {
237
+ importerMap[importerMapKey].push(parent.pkgName)
238
+ }
237
239
 
238
240
  const newParent = dependencyNodes.find((potentialParent) => {
239
241
  if (!parent || !parent.pkgName) {
@@ -284,6 +286,11 @@ export function clearDependencyCache() {
284
286
  function parseModuleDependency(
285
287
  node: ts.ImportDeclaration | ts.ExportDeclaration | ts.CallExpression
286
288
  ): Result<ModuleDependencyShape> {
289
+ //Check if this is a type only import and skip it
290
+ if (node.asKind(ts.SyntaxKind.ImportDeclaration)?.getImportClause()?.isTypeOnly()) {
291
+ return { success: false }
292
+ }
293
+
287
294
  let moduleName: string | undefined
288
295
  if (ts.Node.isImportDeclaration(node) && !node.isModuleSpecifierRelative()) {
289
296
  moduleName = node.getModuleSpecifierValue()
@@ -386,6 +393,7 @@ export const ServerModulePlugin = Plugin.create({
386
393
  success: false,
387
394
  }
388
395
  }
396
+
389
397
  const sbomContent = generateSBOMContent(context)
390
398
 
391
399
  return {
@@ -417,6 +425,9 @@ export const ServerModulePlugin = Plugin.create({
417
425
  shape: SourceFileShape,
418
426
  fileTypes: ['module'],
419
427
  async toRecord(file, { factory, fs, diagnostics, project, config, packageJson, compiler }) {
428
+ if (config.type === 'configuration') {
429
+ throw new Error(`Modules cannot be used in a configuration project`)
430
+ }
420
431
  const path = file.getPath()
421
432
  if (!path.startsWith(project.resolvePath(config.serverModulesDir))) {
422
433
  return { success: false }
@@ -467,6 +478,10 @@ export const ServerModulePlugin = Plugin.create({
467
478
  fileTypes: ['module'],
468
479
  // TODO: When managed cache is provided to plugins, cache dependencies that were already handled to avoid reprocessing
469
480
  async toRecord(shape, { packageJson, diagnostics, fs, logger, project, factory, config }) {
481
+ if (config.type === 'configuration') {
482
+ throw new Error(`Modules cannot be used in a configuration project`)
483
+ }
484
+
470
485
  const dependencies = packageJson.dependencies ?? {}
471
486
  const { name: parentName, entry } = validateAndGetModuleSpecifier(shape.getModuleName())
472
487
 
@@ -486,7 +501,7 @@ export const ServerModulePlugin = Plugin.create({
486
501
  throw new Error(`Failed to build dependency ${id}`)
487
502
  }
488
503
 
489
- let importerMap = {}
504
+ let importerMap: { [key: string]: string[] } = {}
490
505
  if (!config.hoistDependencies) {
491
506
  importerMap = buildParentPathMap(dependencyNodes)
492
507
  }
@@ -507,6 +522,7 @@ export const ServerModulePlugin = Plugin.create({
507
522
  }
508
523
  }
509
524
 
525
+ const importerPath = importerMap[`${pkgName}@${version}`]
510
526
  modules.push({
511
527
  id: `${name}@${version}/${file}`,
512
528
  path: getModuleDependencyPath(config, {
@@ -514,7 +530,7 @@ export const ServerModulePlugin = Plugin.create({
514
530
  file,
515
531
  version,
516
532
  packageJson,
517
- importerPath: importerMap[`${pkgName}@${version}`],
533
+ ...(importerPath ? { importerPath } : {}),
518
534
  }),
519
535
  content: fileContent,
520
536
  })
@@ -171,7 +171,10 @@ export const SPWidgetPlugin = Plugin.create({
171
171
  explicitId: widget.get('$id'),
172
172
  properties: widget.transform(({ $ }) => ({
173
173
  name: $,
174
- category: $.map((v) => WidgetCategories[v.ifString()?.getValue() || '']).def('custom'),
174
+ category: $.map((v) => {
175
+ const catKey = v.ifString()?.getValue() || ''
176
+ return WidgetCategories[catKey as keyof typeof WidgetCategories]
177
+ }).def('custom'),
175
178
  client_script: $.from('clientScript').def(getDefaultClientScript(controller)),
176
179
  script: $.from('serverScript').def(SP_WIDGET_DEFAULT_SERVER_SCRIPT),
177
180
  controller_as: $.from('controllerAs').def('c'),
@@ -22,7 +22,7 @@ export const chunkData = (data: string): string[] => {
22
22
  return chunks
23
23
  }
24
24
 
25
- const generateId = (...parts: Array<string | number>): string => new MD5().update(parts.join(':')).digest('hex')
25
+ export const generateId = (...parts: Array<string | number>): string => new MD5().update(parts.join(':')).digest('hex')
26
26
 
27
27
  // based on tectonic code for validating XML content
28
28
  // https://code.devsnc.com/dev/sn-tectonic/blob/b3ab42ce742158cb5a0d00efd540b97eeafcbdd7/core/metadata-transform-san-diego/utils/index.js#L51-L63
@@ -71,7 +71,7 @@ const attachmentRelationships = {
71
71
  },
72
72
  }
73
73
 
74
- const toNoOpShape = (record) => {
74
+ const toNoOpShape = (record: Record) => {
75
75
  return { success: true, value: Shape.noOp(record) }
76
76
  }
77
77
 
@@ -63,6 +63,7 @@ const ColumnSchema = z
63
63
  '@_max_length': z.coerce.number().or(z.string()).optional(),
64
64
  '@_mandatory': BooleanFromString.optional(),
65
65
  '@_read_only': BooleanFromString.optional(),
66
+ '@_read_only_option': z.string().optional(),
66
67
  '@_reference_table': z.string().optional(),
67
68
  '@_reference_qual': z.string().optional(),
68
69
  '@_label': z.string().optional(),
@@ -150,6 +151,7 @@ type ColumnDefinition = {
150
151
  maxLength: number | string | undefined
151
152
  isMandatory: boolean | undefined
152
153
  isReadOnly: boolean | undefined
154
+ readOnlyOption: string | undefined
153
155
  referenceTable: string | undefined
154
156
  referenceQual: string | undefined
155
157
  columnLabel: string | undefined
@@ -245,6 +247,7 @@ type SysDictionaryProperties = {
245
247
  max_length: number | string | undefined
246
248
  mandatory: boolean | undefined
247
249
  read_only: boolean | undefined
250
+ read_only_option: string | undefined
248
251
  reference_table: string | undefined
249
252
  reference_qual: string | undefined
250
253
  column_label: string | undefined
@@ -461,7 +464,7 @@ export const TablePlugin = Plugin.create({
461
464
  },
462
465
  },
463
466
  toShape(record, { descendants }) {
464
- const schema = {}
467
+ const schema: { [key: string]: CallExpressionShape } = {}
465
468
  let displayColumn: string | undefined
466
469
  const columns = descendants.query('sys_dictionary')
467
470
  let collectionRecord: Record
@@ -497,6 +500,7 @@ export const TablePlugin = Plugin.create({
497
500
  const callExpression = new CallExpressionShape({
498
501
  source: record,
499
502
  callee: 'Table',
503
+ exportName: record.get('name').ifString()?.getValue(),
500
504
  args: [
501
505
  record
502
506
  .transform(({ $ }) => ({
@@ -545,7 +549,7 @@ export const TablePlugin = Plugin.create({
545
549
  if (!attributes?.isString()) {
546
550
  return undefined
547
551
  }
548
- const result = {}
552
+ const result: { [key: string]: string | boolean } = {}
549
553
  attributes
550
554
  .toString()
551
555
  .getValue()
@@ -555,12 +559,15 @@ export const TablePlugin = Plugin.create({
555
559
  return
556
560
  }
557
561
  const [key, value] = attr.split('=').map((s) => s.trim())
562
+ if (!key || value === undefined) {
563
+ return
564
+ }
558
565
  if (value === 'true') {
559
- result[key!] = true
566
+ result[key] = true
560
567
  } else if (value === 'false') {
561
- result[key!] = false
568
+ result[key] = false
562
569
  } else {
563
- result[key!] = value
570
+ result[key] = value
564
571
  }
565
572
  })
566
573
  return result
@@ -688,7 +695,7 @@ export const TablePlugin = Plugin.create({
688
695
  }
689
696
  },
690
697
  async toFile(record, { descendants, config, transform }) {
691
- if (config.tableOutputFormat !== 'bootstrap' || record.isDeleted()) {
698
+ if (config.tableOutputFormat !== 'bootstrap' || config.type === 'configuration' || record.isDeleted()) {
692
699
  // Defer to record plugin
693
700
  return { success: false }
694
701
  }
@@ -739,6 +746,7 @@ export const TablePlugin = Plugin.create({
739
746
  ['max_length', maxLength.ifNumber()?.getValue().toString() ?? maxLength.ifString()?.getValue()],
740
747
  ['mandatory', column.get('mandatory').ifBoolean()?.getValue().toString()],
741
748
  ['read_only', column.get('read_only').ifBoolean()?.getValue().toString()],
749
+ ['read_only_option', column.get('read_only_option').ifString()?.getValue()],
742
750
  ['reference_cascade_rule', column.get('reference_cascade_rule').ifString()?.getValue()],
743
751
  ['calculation', column.get('calculation').ifString()?.getValue()],
744
752
  ['choice', column.get('choice').ifNumber()?.getValue().toString()],
@@ -947,6 +955,7 @@ export const TablePlugin = Plugin.create({
947
955
  let ignoreColumnNameCheck = false
948
956
  const scopeName = config.scope
949
957
  const scopeRegex = new RegExp(`^${scopeName}_`)
958
+ const globalRegex = /^u_/
950
959
  const tableNameMatch = tableName.getValue().match(scopeRegex)
951
960
  if (!tableNameMatch && !isSNScope(scopeName) && scopeName !== 'global') {
952
961
  const nameNode = tableName.getOriginalNode()
@@ -973,6 +982,29 @@ export const TablePlugin = Plugin.create({
973
982
  )
974
983
  }
975
984
 
985
+ const globalTableNameMatch = tableName.getValue().match(globalRegex)
986
+ let anyNonPrefixedGlobalColumn = false
987
+
988
+ if (scopeName === 'global' && !globalTableNameMatch) {
989
+ const schema = table.get('schema').asObject()
990
+ for (const [name, _] of schema.entries()) {
991
+ if (!name.match(globalRegex)) {
992
+ anyNonPrefixedGlobalColumn = true
993
+ break
994
+ }
995
+ }
996
+
997
+ if (anyNonPrefixedGlobalColumn) {
998
+ diagnostics.error(
999
+ table.get('name'),
1000
+ `Global table 'name' property should start with custom prefix 'u_'`
1001
+ )
1002
+ }
1003
+ } else if (scopeName === 'global') {
1004
+ // Global table starts with custom prefix `u_`, allow any column name prefix
1005
+ ignoreColumnNameCheck = true
1006
+ }
1007
+
976
1008
  // sys_dictionary
977
1009
  const schema = table.get('schema').asObject()
978
1010
  for (const [name, column] of schema.entries()) {
@@ -994,6 +1026,11 @@ export const TablePlugin = Plugin.create({
994
1026
  column.getOriginalNode().getParentIfKind(ts.SyntaxKind.PropertyAssignment) ?? column,
995
1027
  `Column name should be prefixed with scope '${scopeName}_' if table name does not contain prefix`
996
1028
  )
1029
+ } else if (scopeName === 'global' && !globalTableNameMatch && !name.match(globalRegex)) {
1030
+ diagnostics.error(
1031
+ column.getOriginalNode().getParentIfKind(ts.SyntaxKind.PropertyAssignment) ?? column,
1032
+ `Column name should be prefixed with 'u_' custom prefix if table name does not contain this prefix, such as when adding columns to an existing global table`
1033
+ )
997
1034
  }
998
1035
  const display = table.get('display').ifString()?.getValue() === name
999
1036
  const result = await transform.toRecord(
@@ -1279,6 +1316,7 @@ function parseTableBootstrapXml(xml: unknown): TableDefinition | null {
1279
1316
  maxLength: column['@_max_length'],
1280
1317
  isMandatory: column['@_mandatory'],
1281
1318
  isReadOnly: column['@_read_only'],
1319
+ readOnlyOption: column['@_read_only_option'],
1282
1320
  referenceTable: column['@_reference_table'],
1283
1321
  referenceQual: column['@_reference_qual'],
1284
1322
  columnLabel: column['@_label'],
@@ -1441,6 +1479,7 @@ function tableDefToRecordProperties(tableDef: TableDefinition): {
1441
1479
  max_length: column.maxLength,
1442
1480
  mandatory: column.isMandatory,
1443
1481
  read_only: column.isReadOnly,
1482
+ read_only_option: column.readOnlyOption,
1444
1483
  reference_table: column.referenceTable,
1445
1484
  reference_qual: column.referenceQual,
1446
1485
  column_label: column.columnLabel,
@@ -1522,6 +1561,7 @@ function tableDefToRecordProperties(tableDef: TableDefinition): {
1522
1561
  default_value: undefined,
1523
1562
  max_length: undefined,
1524
1563
  mandatory: undefined,
1564
+ read_only_option: undefined,
1525
1565
  reference_table: undefined,
1526
1566
  reference_qual: undefined,
1527
1567
  column_label: undefined,
@@ -1673,7 +1713,7 @@ async function generateRecordXml(
1673
1713
  ): Promise<OutputFile[]> {
1674
1714
  const files: OutputFile[] = []
1675
1715
  for (const record of records) {
1676
- const recordBuilder = unloadBuilder({ scope: config.scope, scopeId: config.scopeId })
1716
+ const recordBuilder = unloadBuilder({ scope: config.scope, scopeId: config.scopeId, table: record.getTable() })
1677
1717
  const updateName = await transform.getUpdateName(record)
1678
1718
  const builder = recordBuilder.record(record, updateName)
1679
1719
 
@@ -22,7 +22,7 @@ const builderOptions: XmlBuilderOptions = {
22
22
  processEntities: true,
23
23
  // A valid but undocumented option
24
24
  // https://github.com/NaturalIntelligence/fast-xml-parser/blob/master/src/xmlbuilder/json2xml.js#L26
25
- // @ts-ignore
25
+ // @ts-expect-error
26
26
  entities: [
27
27
  // Match on &, &test, but not &amp;, &lt;, &gt;, &apos;, and &quot;
28
28
  // See: https://en.wikipedia.org/wiki/List_of_XML_and_HTML_character_entity_references#Standard_public_entity_sets_for_characters for the list of default XML entities
@@ -70,6 +70,7 @@ window.g_ck = "$[gs.getSession().getSessionToken() || gs.getSessionToken()]";
70
70
  * @param nodes - parsed XML nodes
71
71
  * @returns - parsed XML nodes and any replacements of synthetic tags
72
72
  */
73
+ // biome-ignore lint/suspicious/noExplicitAny: Fast-xml-parser types are not defined
73
74
  const nodeTransformer = (nodes: any[]) => {
74
75
  for (let i = 0; i < nodes.length; i++) {
75
76
  const node = nodes[i]