@servicenow/sdk-build-plugins 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/dist/AttachmentPlugin.d.ts +253 -0
  2. package/dist/AttachmentPlugin.js +216 -0
  3. package/dist/AttachmentPlugin.js.map +1 -0
  4. package/dist/BusinessRulePlugin.d.ts +56 -0
  5. package/dist/BusinessRulePlugin.js +171 -0
  6. package/dist/BusinessRulePlugin.js.map +1 -0
  7. package/dist/CrossScopePrivilegePlugin.d.ts +22 -0
  8. package/dist/CrossScopePrivilegePlugin.js +42 -0
  9. package/dist/CrossScopePrivilegePlugin.js.map +1 -0
  10. package/dist/DefaultPlugin.d.ts +71 -0
  11. package/dist/DefaultPlugin.js +238 -0
  12. package/dist/DefaultPlugin.js.map +1 -0
  13. package/dist/IdPlugin.d.ts +17 -0
  14. package/dist/IdPlugin.js +45 -0
  15. package/dist/IdPlugin.js.map +1 -0
  16. package/dist/ListPlugin.d.ts +91 -0
  17. package/dist/ListPlugin.js +398 -0
  18. package/dist/ListPlugin.js.map +1 -0
  19. package/dist/PropertyPlugin.d.ts +122 -0
  20. package/dist/PropertyPlugin.js +165 -0
  21. package/dist/PropertyPlugin.js.map +1 -0
  22. package/dist/ScriptTemplatePlugin.d.ts +31 -0
  23. package/dist/ScriptTemplatePlugin.js +208 -0
  24. package/dist/ScriptTemplatePlugin.js.map +1 -0
  25. package/dist/UserPreferencePlugin.d.ts +16 -0
  26. package/dist/UserPreferencePlugin.js +30 -0
  27. package/dist/UserPreferencePlugin.js.map +1 -0
  28. package/dist/aclAndRole/AclPlugin.d.ts +117 -0
  29. package/dist/aclAndRole/AclPlugin.js +285 -0
  30. package/dist/aclAndRole/AclPlugin.js.map +1 -0
  31. package/dist/aclAndRole/RolePlugin.d.ts +58 -0
  32. package/dist/aclAndRole/RolePlugin.js +152 -0
  33. package/dist/aclAndRole/RolePlugin.js.map +1 -0
  34. package/dist/aclAndRole/Util.d.ts +3 -0
  35. package/dist/aclAndRole/Util.js +106 -0
  36. package/dist/aclAndRole/Util.js.map +1 -0
  37. package/dist/app/ApplicationMenuPlugin.d.ts +32 -0
  38. package/dist/app/ApplicationMenuPlugin.js +106 -0
  39. package/dist/app/ApplicationMenuPlugin.js.map +1 -0
  40. package/dist/atf/ATFComposer.d.ts +492 -0
  41. package/dist/atf/ATFComposer.js +2717 -0
  42. package/dist/atf/ATFComposer.js.map +1 -0
  43. package/dist/atf/TestPlugin.d.ts +31 -0
  44. package/dist/atf/TestPlugin.js +95 -0
  45. package/dist/atf/TestPlugin.js.map +1 -0
  46. package/dist/atf/index.d.ts +1 -0
  47. package/dist/atf/index.js +9 -0
  48. package/dist/atf/index.js.map +1 -0
  49. package/dist/db/ColumnPlugins.d.ts +278 -0
  50. package/dist/db/ColumnPlugins.js +112 -0
  51. package/dist/db/ColumnPlugins.js.map +1 -0
  52. package/dist/db/RecordPlugin.d.ts +208 -0
  53. package/dist/db/RecordPlugin.js +287 -0
  54. package/dist/db/RecordPlugin.js.map +1 -0
  55. package/dist/db/TablePlugin.d.ts +742 -0
  56. package/dist/db/TablePlugin.js +1249 -0
  57. package/dist/db/TablePlugin.js.map +1 -0
  58. package/dist/db/index.d.ts +3 -0
  59. package/dist/db/index.js +27 -0
  60. package/dist/db/index.js.map +1 -0
  61. package/dist/index.d.ts +16 -0
  62. package/dist/index.js +51 -0
  63. package/dist/index.js.map +1 -0
  64. package/dist/scriptedRESTAPI/RESTDeserializationUtils.d.ts +12 -0
  65. package/dist/scriptedRESTAPI/RESTDeserializationUtils.js +371 -0
  66. package/dist/scriptedRESTAPI/RESTDeserializationUtils.js.map +1 -0
  67. package/dist/scriptedRESTAPI/RESTSerializationUtils.d.ts +15 -0
  68. package/dist/scriptedRESTAPI/RESTSerializationUtils.js +177 -0
  69. package/dist/scriptedRESTAPI/RESTSerializationUtils.js.map +1 -0
  70. package/dist/scriptedRESTAPI/RestApiPlugin.d.ts +144 -0
  71. package/dist/scriptedRESTAPI/RestApiPlugin.js +318 -0
  72. package/dist/scriptedRESTAPI/RestApiPlugin.js.map +1 -0
  73. package/dist/scriptedRESTAPI/RestSchemaUtils.d.ts +190 -0
  74. package/dist/scriptedRESTAPI/RestSchemaUtils.js +53 -0
  75. package/dist/scriptedRESTAPI/RestSchemaUtils.js.map +1 -0
  76. package/dist/scriptedRESTAPI/RestUtils.d.ts +75 -0
  77. package/dist/scriptedRESTAPI/RestUtils.js +469 -0
  78. package/dist/scriptedRESTAPI/RestUtils.js.map +1 -0
  79. package/dist/scripts/ClientScriptPlugin.d.ts +43 -0
  80. package/dist/scripts/ClientScriptPlugin.js +190 -0
  81. package/dist/scripts/ClientScriptPlugin.js.map +1 -0
  82. package/dist/scripts/scriptUtils.d.ts +15 -0
  83. package/dist/scripts/scriptUtils.js +83 -0
  84. package/dist/scripts/scriptUtils.js.map +1 -0
  85. package/dist/uxf/ExperiencePlugin.d.ts +22 -0
  86. package/dist/uxf/ExperiencePlugin.js +55 -0
  87. package/dist/uxf/ExperiencePlugin.js.map +1 -0
  88. package/dist/uxf/RoutesPlugin.d.ts +22 -0
  89. package/dist/uxf/RoutesPlugin.js +176 -0
  90. package/dist/uxf/RoutesPlugin.js.map +1 -0
  91. package/dist/uxf/UxfFormulaParser/cleanUxValue.d.ts +4 -0
  92. package/dist/uxf/UxfFormulaParser/cleanUxValue.js +65 -0
  93. package/dist/uxf/UxfFormulaParser/cleanUxValue.js.map +1 -0
  94. package/dist/uxf/UxfFormulaParser/grammerParser/api.d.ts +189 -0
  95. package/dist/uxf/UxfFormulaParser/grammerParser/api.js +158 -0
  96. package/dist/uxf/UxfFormulaParser/grammerParser/api.js.map +1 -0
  97. package/dist/uxf/UxfFormulaParser/grammerParser/clientTransformMap.d.ts +13 -0
  98. package/dist/uxf/UxfFormulaParser/grammerParser/clientTransformMap.js +604 -0
  99. package/dist/uxf/UxfFormulaParser/grammerParser/clientTransformMap.js.map +1 -0
  100. package/dist/uxf/UxfFormulaParser/grammerParser/grammarParser.d.ts +12 -0
  101. package/dist/uxf/UxfFormulaParser/grammerParser/grammarParser.js +551 -0
  102. package/dist/uxf/UxfFormulaParser/grammerParser/grammarParser.js.map +1 -0
  103. package/dist/uxf/UxfFormulaParser/grammerParser/spanHelpers.d.ts +31 -0
  104. package/dist/uxf/UxfFormulaParser/grammerParser/spanHelpers.js +64 -0
  105. package/dist/uxf/UxfFormulaParser/grammerParser/spanHelpers.js.map +1 -0
  106. package/dist/uxf/UxfFormulaParser/index.d.ts +3 -0
  107. package/dist/uxf/UxfFormulaParser/index.js +11 -0
  108. package/dist/uxf/UxfFormulaParser/index.js.map +1 -0
  109. package/dist/uxf/UxfFormulaParser/parser.d.ts +8 -0
  110. package/dist/uxf/UxfFormulaParser/parser.js +87 -0
  111. package/dist/uxf/UxfFormulaParser/parser.js.map +1 -0
  112. package/dist/uxf/UxfFormulaParser/utils/getErrorMsg.d.ts +8 -0
  113. package/dist/uxf/UxfFormulaParser/utils/getErrorMsg.js +17 -0
  114. package/dist/uxf/UxfFormulaParser/utils/getErrorMsg.js.map +1 -0
  115. package/dist/uxf/constants.d.ts +2 -0
  116. package/dist/uxf/constants.js +8 -0
  117. package/dist/uxf/constants.js.map +1 -0
  118. package/dist/uxf/index.d.ts +2 -0
  119. package/dist/uxf/index.js +11 -0
  120. package/dist/uxf/index.js.map +1 -0
  121. package/dist/uxf/tectonicIdGenerator.d.ts +12 -0
  122. package/dist/uxf/tectonicIdGenerator.js +102 -0
  123. package/dist/uxf/tectonicIdGenerator.js.map +1 -0
  124. package/license +9 -0
  125. package/package.json +42 -0
  126. package/src/AttachmentPlugin.ts +262 -0
  127. package/src/BusinessRulePlugin.ts +251 -0
  128. package/src/CrossScopePrivilegePlugin.ts +54 -0
  129. package/src/DefaultPlugin.ts +272 -0
  130. package/src/IdPlugin.ts +47 -0
  131. package/src/ListPlugin.ts +497 -0
  132. package/src/PropertyPlugin.ts +218 -0
  133. package/src/ScriptTemplatePlugin.ts +223 -0
  134. package/src/UserPreferencePlugin.ts +36 -0
  135. package/src/aclAndRole/AclPlugin.ts +410 -0
  136. package/src/aclAndRole/RolePlugin.ts +225 -0
  137. package/src/aclAndRole/Util.ts +104 -0
  138. package/src/app/ApplicationMenuPlugin.ts +158 -0
  139. package/src/atf/ATFComposer.ts +3356 -0
  140. package/src/atf/TestPlugin.ts +119 -0
  141. package/src/atf/index.ts +1 -0
  142. package/src/db/ColumnPlugins.ts +117 -0
  143. package/src/db/RecordPlugin.ts +391 -0
  144. package/src/db/TablePlugin.ts +1581 -0
  145. package/src/db/index.ts +3 -0
  146. package/src/index.ts +16 -0
  147. package/src/scriptedRESTAPI/RESTDeserializationUtils.ts +410 -0
  148. package/src/scriptedRESTAPI/RESTSerializationUtils.ts +227 -0
  149. package/src/scriptedRESTAPI/RestApiPlugin.ts +438 -0
  150. package/src/scriptedRESTAPI/RestSchemaUtils.ts +72 -0
  151. package/src/scriptedRESTAPI/RestUtils.ts +507 -0
  152. package/src/scripts/ClientScriptPlugin.ts +251 -0
  153. package/src/scripts/scriptUtils.ts +81 -0
  154. package/src/uxf/ExperiencePlugin.ts +64 -0
  155. package/src/uxf/RoutesPlugin.ts +215 -0
  156. package/src/uxf/UxfFormulaParser/cleanUxValue.ts +73 -0
  157. package/src/uxf/UxfFormulaParser/grammerParser/api.js +166 -0
  158. package/src/uxf/UxfFormulaParser/grammerParser/clientTransformMap.js +606 -0
  159. package/src/uxf/UxfFormulaParser/grammerParser/grammarParser.js +551 -0
  160. package/src/uxf/UxfFormulaParser/grammerParser/spanHelpers.js +65 -0
  161. package/src/uxf/UxfFormulaParser/index.ts +4 -0
  162. package/src/uxf/UxfFormulaParser/parser.ts +64 -0
  163. package/src/uxf/UxfFormulaParser/utils/getErrorMsg.ts +13 -0
  164. package/src/uxf/constants.ts +4 -0
  165. package/src/uxf/index.ts +2 -0
  166. package/src/uxf/tectonicIdGenerator.ts +81 -0
@@ -0,0 +1,251 @@
1
+ import { ClientScript } from '@servicenow/sdk-core/runtime/clientscript'
2
+ import {
3
+ Context,
4
+ Plugin,
5
+ extractCallExpression,
6
+ getCallExpressionName,
7
+ removeNode,
8
+ getOrCreateEntitySourceFile,
9
+ generateCallExpressionExportForDocument,
10
+ linkDocument,
11
+ mergeDataIntoObjectLiteral,
12
+ writeCustomProperty,
13
+ stringify,
14
+ getSysUpdateName,
15
+ FluentDiagnostic,
16
+ EntityData,
17
+ ObjectData,
18
+ } from '@servicenow/sdk-build-core'
19
+ import { Record } from '@servicenow/sdk-core/runtime/db'
20
+ import { ClientScriptSchema } from '@servicenow/sdk-core/runtime/clientscript'
21
+ import { z } from 'zod'
22
+ import * as ts from 'ts-morph'
23
+ import { processScript } from '../ScriptTemplatePlugin'
24
+ import { Diagnostic } from '@servicenow/sdk-project'
25
+ import { except } from './scriptUtils'
26
+
27
+ const UITypeMapping = {
28
+ desktop: 0,
29
+ mobile_or_service_portal: 1,
30
+ all: 10,
31
+ }
32
+ const ClientScriptXmlSchema = ClientScriptSchema.extend({
33
+ table: z.string(),
34
+ ui_type: z.union([z.literal(0), z.literal(1), z.literal(10)]),
35
+ sys_class_name: z.literal('sys_script_client'),
36
+ })
37
+ const PartialClientScriptXMLSchema = ClientScriptXmlSchema.partial()
38
+
39
+ type ClientScriptSchema = z.infer<typeof ClientScriptSchema>
40
+ type PatialClientScriptConfig = z.infer<typeof PartialClientScriptXMLSchema>
41
+
42
+ export default Plugin({
43
+ name: 'ClientScript',
44
+ ownedTables: {
45
+ sys_script_client: { diagnosticLevel: Diagnostic.Level.Warn },
46
+ },
47
+ extractors: {
48
+ entity: {
49
+ CallExpression: (node, context) => {
50
+ const result = extractCallExpression(ClientScript, 'clientScript', node, context, (script) =>
51
+ context.registerExplicitId('sys_script_client', script.$id as string)
52
+ )
53
+
54
+ if (!result.handled || !(0 in result.data)) {
55
+ return result
56
+ }
57
+
58
+ const clientScriptEntityData = result.data[0]
59
+ const args = node.getArguments()[0] as ts.ObjectLiteralExpression
60
+
61
+ if (!args) {
62
+ result.diagnostics.push(new FluentDiagnostic(node, 'Client script arguments are missing'))
63
+ return result
64
+ }
65
+
66
+ const diagnostics = result.diagnostics
67
+ const data = ClientScriptSchema.parse(clientScriptEntityData.getValue())
68
+ const { script, type, field } = data
69
+
70
+ if (field && !type) {
71
+ diagnostics.push(
72
+ new FluentDiagnostic(
73
+ args.getProperty('field')!,
74
+ `Field values are allowed only when type is set to onChange or onCellEdit`
75
+ )
76
+ )
77
+ }
78
+
79
+ if (script && !validateClientSideScripts(script, context)) {
80
+ diagnostics.push(
81
+ new FluentDiagnostic(
82
+ args.getProperty('script')!,
83
+ `Client side scripts cannot import or require modules.`
84
+ )
85
+ )
86
+ }
87
+
88
+ /**
89
+ * Removing this diagnostic : Currently on instance if you empty script field and click update, it would show basic template set
90
+ * for the field but the xml is not updated. Only after saving it again the xml gets updated with basic template.
91
+ */
92
+ // if (type && !script) {
93
+ // diagnostics.push(
94
+ // new FluentDiagnostic(
95
+ // args.getProperty('type')!,
96
+ // `Cannot have empty script value for type: ${type}`
97
+ // )
98
+ // )
99
+ // }
100
+
101
+ const record = createClientScriptRecord(data, script)
102
+
103
+ return {
104
+ handled: true,
105
+ diagnostics,
106
+ data: [
107
+ new EntityData(
108
+ 'record',
109
+ clientScriptEntityData.getGuid(),
110
+ ObjectData.fromObjectValue(record, node),
111
+ node
112
+ ),
113
+ ],
114
+ }
115
+ },
116
+ },
117
+ },
118
+
119
+ generators: {
120
+ record(document, context) {
121
+ if (!ClientScriptXmlSchema.safeParse(document.data!['data']).success) {
122
+ return undefined
123
+ }
124
+
125
+ return linkDocument(
126
+ document,
127
+ generateCallExpressionExportForDocument(
128
+ context,
129
+ {
130
+ sourceFile: getOrCreateEntitySourceFile(
131
+ context,
132
+ getSysUpdateName(document, 'sys_script_client')
133
+ ),
134
+ moduleSpecifier: '@servicenow/sdk/core',
135
+ },
136
+ ClientScript,
137
+ {
138
+ $id: document.guid,
139
+ }
140
+ ).getExpressionIfKindOrThrow(ts.SyntaxKind.CallExpression)
141
+ )
142
+ },
143
+ },
144
+
145
+ transformers: {
146
+ record: {
147
+ CallExpression(document, context) {
148
+ if (getCallExpressionName(document.node) !== ClientScript.name) {
149
+ return false
150
+ }
151
+
152
+ if (document.action === 'DELETE') {
153
+ removeNode(document.node)
154
+ return true
155
+ }
156
+
157
+ const modifiedClientScriptProps = ClientScriptXmlSchema.partial().safeParse(
158
+ (document.changedData as any).data
159
+ )
160
+
161
+ if (!modifiedClientScriptProps.success) {
162
+ return false
163
+ }
164
+
165
+ return transformClientScriptArguments(
166
+ document.guid,
167
+ document.node,
168
+ context,
169
+ except(modifiedClientScriptProps.data, (key) => key === 'sys_class_name')
170
+ )
171
+ },
172
+ },
173
+ },
174
+ })
175
+
176
+ function validateClientSideScripts(script: string, context: Context) {
177
+ const source = context.compiler.createSourceFile('tmp-file.ts', script)
178
+ const importDeclarations = source.getDescendantsOfKind(ts.SyntaxKind.ImportDeclaration)
179
+ const requireCalls = source.getDescendantsOfKind(ts.SyntaxKind.CallExpression).filter((callExpression) => {
180
+ return isRequire(callExpression)
181
+ })
182
+ const isValid = !(importDeclarations.length > 0 || requireCalls.length > 0)
183
+ context.compiler.removeSourceFile(source)
184
+ return isValid
185
+ }
186
+
187
+ function getUITypeFromId(id: number) {
188
+ const type = Object.keys(UITypeMapping).find((e) => UITypeMapping[e] === id)
189
+ if (!type) {
190
+ throw Error('Invalid UI Type encountered, check XML data before transforming again.')
191
+ }
192
+ return type
193
+ }
194
+
195
+ function transformClientScriptArguments(
196
+ documentId: string,
197
+ node: ts.CallExpression,
198
+ context: Context,
199
+ data: PatialClientScriptConfig
200
+ ) {
201
+ const argNode = node.getArguments()[0]!.asKindOrThrow(ts.SyntaxKind.ObjectLiteralExpression)
202
+ const { script, table, ui_type, type, ...rest } = data
203
+
204
+ if (type !== undefined) {
205
+ if (type) {
206
+ writeCustomProperty(argNode, 'type', '', stringify(type))
207
+ } else {
208
+ argNode.getProperty('type')?.remove()
209
+ }
210
+ }
211
+
212
+ if (Object.values(UITypeMapping).some((value) => value === ui_type)) {
213
+ const uiType = getUITypeFromId(ui_type!)
214
+ writeCustomProperty(argNode, 'ui_type', '', stringify(uiType))
215
+ }
216
+
217
+ if (table) {
218
+ writeCustomProperty(argNode, 'table', '{}', stringify(table))
219
+ }
220
+
221
+ if (script !== undefined) {
222
+ processScript(argNode, 'script', script, documentId, 'sys_script_client', context)
223
+ }
224
+
225
+ mergeDataIntoObjectLiteral(argNode, rest)
226
+
227
+ return true
228
+ }
229
+
230
+ function createClientScriptRecord(data: ClientScriptSchema, script: string) {
231
+ data.script = script as any
232
+ const { $id, ...csData } = ClientScriptSchema.parse(data)
233
+ csData.ui_type = getUITypeId(csData.ui_type)
234
+ return Record({
235
+ table: 'sys_script_client',
236
+ $id,
237
+ data: csData as any,
238
+ })
239
+ }
240
+
241
+ function getUITypeId(value: string) {
242
+ if (value in UITypeMapping) {
243
+ return UITypeMapping[value]
244
+ }
245
+ throw Error('Invalid ui_type found in xml')
246
+ }
247
+
248
+ export function isRequire(callExpression: ts.CallExpression): boolean {
249
+ const expression = callExpression.getExpression()
250
+ return ts.Node.isIdentifier(expression) && expression.getText() === 'require'
251
+ }
@@ -0,0 +1,81 @@
1
+ import { CURRENT_CALL_EXPRESSION, Context, ObjectData } from '@servicenow/sdk-build-core'
2
+ import * as path from 'path'
3
+ import { SyntaxKind, Node } from 'ts-morph'
4
+
5
+ export type ScriptInfo = {
6
+ filePath: string
7
+ functionName: string
8
+ isDefault: boolean
9
+ event?: string
10
+ }
11
+
12
+ export function scriptInfo(node: Node, context: Context, pluginName: string) {
13
+ if (!context.store) {
14
+ return
15
+ }
16
+
17
+ if (context.store[CURRENT_CALL_EXPRESSION] !== pluginName) {
18
+ if (pluginName !== 'Table' && (context.store[CURRENT_CALL_EXPRESSION] as string).includes('Column')) {
19
+ return
20
+ }
21
+ }
22
+
23
+ if (node.isKind(SyntaxKind.FunctionDeclaration)) {
24
+ return ObjectData.fromObjectValue(
25
+ {
26
+ filePath: path.normalize(node.getSourceFile().getFilePath()),
27
+ functionName: node.getName(),
28
+ isDefault: node.isDefaultExport(),
29
+ },
30
+ node
31
+ )
32
+ }
33
+
34
+ if (node.isKind(SyntaxKind.FunctionExpression)) {
35
+ const parent = node.getParentIfKind(SyntaxKind.VariableDeclaration)
36
+ if (!parent) {
37
+ return
38
+ }
39
+
40
+ return ObjectData.fromObjectValue(
41
+ {
42
+ filePath: path.normalize(node.getSourceFile().getFilePath()),
43
+ functionName: parent.getName(),
44
+ isDefault: parent.isDefaultExport(),
45
+ },
46
+ node
47
+ )
48
+ }
49
+
50
+ return
51
+ }
52
+
53
+ export function except(obj: object, excludePredicate: (string) => boolean) {
54
+ return Object.keys(obj)
55
+ .filter((k) => !excludePredicate(k))
56
+ .reduce((acc, k) => ({ ...acc, [k]: obj[k] }), {})
57
+ }
58
+
59
+ export function buildScriptImport(
60
+ scriptFileImport: string,
61
+ importName: string,
62
+ isDefault: boolean,
63
+ context: Context,
64
+ codeFunc: (funcName: string) => string
65
+ ) {
66
+ scriptFileImport = scriptFileImport.replace(/\.ts$/, '.js')
67
+ const func_name = importName ?? path.basename(scriptFileImport, '.js')
68
+ let relativeImport = scriptFileImport.replace(context.app.rootDir, '')
69
+ relativeImport = relativeImport.replace(context.app.config.sourceDir, context.app.config.transpiledSourceDir)
70
+ relativeImport = relativeImport.replaceAll(path.sep, path.posix.sep)
71
+ if (relativeImport.startsWith(path.posix.sep)) {
72
+ relativeImport = relativeImport.substring(1)
73
+ }
74
+
75
+ if (isDefault) {
76
+ scriptFileImport = `const ${func_name} = require('./${relativeImport}').default\n` + codeFunc(func_name)
77
+ } else {
78
+ scriptFileImport = `const { ${func_name} } = require('./${relativeImport}')\n` + codeFunc(func_name)
79
+ }
80
+ return scriptFileImport
81
+ }
@@ -0,0 +1,64 @@
1
+ import { Experience, ExperienceSchema } from '@servicenow/sdk-core/runtime/experience'
2
+ import { Record } from '@servicenow/sdk-core/runtime/db'
3
+ import { EntityData, ObjectData, Plugin, extractCallExpression } from '@servicenow/sdk-build-core'
4
+ import { PARENT_APP_SHELL_ID, WORKSPACE_APP_SHELL_ID } from './constants'
5
+ import { RecordPlugin } from '../db/RecordPlugin'
6
+ import { Diagnostic } from '@servicenow/sdk-project'
7
+
8
+ export default Plugin({
9
+ name: 'Experience',
10
+ ownedTables: {
11
+ sys_ux_app_config: { diagnosticLevel: Diagnostic.Level.Warn },
12
+ },
13
+ extractors: {
14
+ entity: {
15
+ CallExpression: (node, context) =>
16
+ extractCallExpression(Experience, 'experience', node, context, (experience) =>
17
+ context.registerExplicitId('sys_ux_app_config', experience.$id as string)
18
+ ),
19
+ },
20
+ },
21
+ composers: {
22
+ entity: {
23
+ experience(entity, context) {
24
+ const node = entity.getNode()
25
+ const data = entity.getValue()
26
+ const guid = entity.getGuid()
27
+ const recordData = ExperienceSchema.parse(data)
28
+
29
+ const appConfig = Record({
30
+ table: 'sys_ux_app_config',
31
+ $id: guid,
32
+ data: {
33
+ name: recordData.name,
34
+ active: recordData.active,
35
+ base_url_path: recordData.baseUrlPath,
36
+ landing_path: recordData.landingPath,
37
+ description: recordData.description,
38
+ },
39
+ })
40
+
41
+ const pageRegistry = Record({
42
+ table: 'sys_ux_page_registry',
43
+ $id: context.registerExplicitId('sys_ux_page_registry', `${guid}_registry`),
44
+ data: {
45
+ title: recordData.name,
46
+ active: recordData.active,
47
+ admin_panel: appConfig,
48
+ admin_panel_table: 'sys_ux_app_config',
49
+ parent_app: PARENT_APP_SHELL_ID,
50
+ path: recordData.baseUrlPath,
51
+ root_macroponent: WORKSPACE_APP_SHELL_ID,
52
+ },
53
+ })
54
+
55
+ return context.composeEntities(
56
+ [appConfig, pageRegistry].map(
57
+ (r) => new EntityData('record', r.$id as string, ObjectData.fromObjectValue(r, node), node)
58
+ ),
59
+ [RecordPlugin]
60
+ )
61
+ },
62
+ },
63
+ },
64
+ })
@@ -0,0 +1,215 @@
1
+ import { BaseUXRouteSchema, ExperienceSchema, Routes } from '@servicenow/sdk-core/runtime/experience'
2
+ import { Record } from '@servicenow/sdk-core/runtime/db'
3
+ import { SdkError } from '@servicenow/sdk-metrics'
4
+ import { EntityData, ObjectData, Plugin, extractCallExpression } from '@servicenow/sdk-build-core'
5
+ import { parser as uxValueParser, cleanUxValue } from './UxfFormulaParser'
6
+ import { idGenerator } from './tectonicIdGenerator'
7
+ import { WORKSPACE_APP_SHELL_ID } from './constants'
8
+ import { RecordPlugin } from '../db/RecordPlugin'
9
+ import { Diagnostic } from '@servicenow/sdk-project'
10
+
11
+ const PAGE_TEMPLATE_SYS_ID = '19be392623033300f4b4c50947bf65ba'
12
+ type Component = Routes['routes'][string]['component']
13
+
14
+ type RouteParts = {
15
+ pathname: string
16
+ parameters: {
17
+ required: string[]
18
+ optional: string[]
19
+ }
20
+ }
21
+
22
+ function getRouteParts(route: string): RouteParts {
23
+ const chunks = route.split('/').filter((chunk) => chunk && chunk.length > 0)
24
+ if (chunks.length === 0) {
25
+ throw new SdkError('Invalid route - routes may not be empty', { type: 'plugin_error' })
26
+ }
27
+
28
+ const parts: RouteParts = {
29
+ pathname: chunks[0] || '',
30
+ parameters: {
31
+ required: [],
32
+ optional: [],
33
+ },
34
+ }
35
+
36
+ chunks.forEach((part) => {
37
+ if (part.startsWith(':')) {
38
+ if (part.endsWith('?')) {
39
+ parts.parameters.optional.push(part.slice(1, -1))
40
+ } else {
41
+ parts.parameters.required.push(part.slice(1))
42
+ }
43
+ }
44
+ })
45
+
46
+ return parts
47
+ }
48
+
49
+ const HARDCODED_LAYOUT = {
50
+ default: {
51
+ children: null,
52
+ items: [{ element_id: 'x', styles: {} }],
53
+ root: null,
54
+ rules: null,
55
+ styles: { 'flex-direction': 'column' },
56
+ templateId: '5832fd4d53c31010e6bcddeeff7b12db',
57
+ type: 'flex',
58
+ },
59
+ version: '3.0.0',
60
+ }
61
+
62
+ const createComposition = (component: Component) => {
63
+ const { tagName, properties = {} } = component
64
+ const componentId = idGenerator(tagName)
65
+
66
+ const UxfFormulaParser = new uxValueParser()
67
+ const propertyValues = Object.keys(properties).reduce((newObj, key) => {
68
+ const parseResult = UxfFormulaParser.parse(properties[key])
69
+ if (parseResult.errors.length) {
70
+ throw new SdkError('Failed to parse formula.', { type: 'plugin_error' })
71
+ }
72
+ newObj[key] = cleanUxValue(parseResult.result.uxValue)
73
+ return newObj
74
+ }, {})
75
+
76
+ return [
77
+ {
78
+ definition: { id: componentId, type: 'MACROPONENT' },
79
+ elementId: 'x',
80
+ elementLabel: tagName,
81
+ eventMappings: [],
82
+ isHidden: { type: 'JSON_LITERAL', value: null },
83
+ preset: null,
84
+ propertyValues,
85
+ slot: null,
86
+ styles: null,
87
+ },
88
+ ]
89
+ }
90
+
91
+ export default Plugin({
92
+ name: 'Routes',
93
+ ownedTables: {
94
+ sys_ux_app_route: { diagnosticLevel: Diagnostic.Level.Warn },
95
+ },
96
+ extractors: {
97
+ entity: {
98
+ // TODO: This needs to extract separate entities for each route and the experience. Currently it is extracting an
99
+ // anonymous "container" entity which goes against the design of the plugin architecture.
100
+ CallExpression: (node, context) =>
101
+ extractCallExpression(Routes, 'routes', node, context, () => 'NO_GUID_GENERATED'), // TODO: Generate GUID
102
+ },
103
+ },
104
+ composers: {
105
+ entity: {
106
+ routes(entity, context) {
107
+ const node = entity.getNode()
108
+ const data = entity.getValue()
109
+ const { experience, routes } = data as Routes
110
+
111
+ const { $id: appConfigId } = ExperienceSchema.parse(experience)
112
+ const appConfigSysId = context.registerExplicitId('sys_ux_app_config', appConfigId)
113
+
114
+ const records: Record[] = []
115
+ for (const [path, route] of Object.entries(routes)) {
116
+ const { $id: routeId, name, component, active, description } = BaseUXRouteSchema.parse(route)
117
+ const routeParts = getRouteParts(path)
118
+
119
+ const screenType = Record({
120
+ table: 'sys_ux_screen_type',
121
+ $id: `${routeId}_screen_type`,
122
+ data: {
123
+ name,
124
+ },
125
+ })
126
+
127
+ const routeRecord = Record({
128
+ table: 'sys_ux_app_route',
129
+ $id: routeId,
130
+ data: {
131
+ name,
132
+ app_config: appConfigSysId,
133
+ route_type: routeParts.pathname,
134
+ fields: routeParts.parameters.required.join(','),
135
+ optional_parameters: routeParts.parameters.optional.join(','),
136
+ screen_type: screenType,
137
+ },
138
+ })
139
+
140
+ const routeParameters = [...routeParts.parameters.required, ...routeParts.parameters.optional]
141
+ const screenMcp = Record({
142
+ table: 'sys_ux_macroponent',
143
+ $id: `${routeId}_page`,
144
+ data: {
145
+ category: 'page',
146
+ composition: JSON.stringify(createComposition(component), null, 2),
147
+ extends: PAGE_TEMPLATE_SYS_ID,
148
+ internal_event_mappings: JSON.stringify({}),
149
+ layout: JSON.stringify(HARDCODED_LAYOUT, null, 2),
150
+ name: `${name} - Page`,
151
+ // Injecting Route parameters as props of the page, so that they can be passed into the
152
+ // component in the composition.
153
+ props: JSON.stringify(
154
+ routeParameters.map((param) => {
155
+ return {
156
+ defaultValue: '',
157
+ description: null,
158
+ disabled: false,
159
+ fieldType: 'string',
160
+ label: param,
161
+ mandatory: false,
162
+ name: param,
163
+ readOnly: true,
164
+ selectable: false,
165
+ typeMetadata: null,
166
+ valueType: 'string',
167
+ }
168
+ }),
169
+ null,
170
+ 2
171
+ ),
172
+ schema_version: '1.0.0',
173
+ },
174
+ })
175
+
176
+ // TODO(patrick): Still a lot of hardcoded stuff in here - needs
177
+ // to be represented in the config, at least optionally.
178
+ const screen = Record({
179
+ table: 'sys_ux_screen',
180
+ $id: `${routeId}_screen`,
181
+ data: {
182
+ active: active!,
183
+ app_config: appConfigSysId,
184
+ description: description!,
185
+ disable_auto_reflow: false,
186
+ event_mappings: JSON.stringify([]),
187
+ macroponent: screenMcp,
188
+ macroponent_config: JSON.stringify({}),
189
+ name: `${name} - Default`,
190
+ order: 0,
191
+ parent_macroponent: WORKSPACE_APP_SHELL_ID,
192
+ required_translations: JSON.stringify([]),
193
+ screen_type: screenType,
194
+ },
195
+ })
196
+
197
+ records.push(screenType, routeRecord, screenMcp, screen)
198
+ }
199
+
200
+ return context.composeEntities(
201
+ records.map(
202
+ (r) =>
203
+ new EntityData(
204
+ 'record',
205
+ context.registerExplicitId(r.table, r.$id as string),
206
+ ObjectData.fromObjectValue(r, node),
207
+ node
208
+ )
209
+ ),
210
+ [RecordPlugin]
211
+ )
212
+ },
213
+ },
214
+ },
215
+ })
@@ -0,0 +1,73 @@
1
+ // Copied from sn-uxf-formula-parser-library - https://code.devsnc.com/dev/uxf-formula-parser
2
+
3
+ /**
4
+ * https://code.devsnc.com/dev/uxf-formula-parser/blob/ci/master/src/uxf-formula-parser/clean/cleanUxValue.js
5
+ */
6
+
7
+ export const cleanUxValue = (uxValue) => {
8
+ switch (uxValue?.type) {
9
+ case 'JSON_LITERAL':
10
+ return { type: uxValue.type, value: uxValue.value }
11
+
12
+ case 'CONTEXT_BINDING':
13
+ case 'DATA_OUTPUT_BINDING':
14
+ case 'STATE_BINDING':
15
+ case 'EVENT_PAYLOAD_BINDING':
16
+ case 'ELEMENT_BINDING':
17
+ case 'REPEATER_ITEM_BINDING':
18
+ case 'ENV_BINDING':
19
+ return { type: uxValue.type, binding: uxValue.binding }
20
+
21
+ case 'CLIENT_TRANSFORM':
22
+ return {
23
+ type: uxValue.type,
24
+ transform: {
25
+ operator: uxValue.transform.operator,
26
+ operands: cleanUxValue(uxValue.transform.operands),
27
+ },
28
+ }
29
+
30
+ case 'UNARY':
31
+ return {
32
+ type: uxValue.type,
33
+ operation: {
34
+ ...uxValue.operation,
35
+ operand: cleanUxValue(uxValue.operation.operand),
36
+ },
37
+ }
38
+
39
+ case 'BINARY':
40
+ return {
41
+ type: uxValue.type,
42
+ operation: {
43
+ ...uxValue.operation,
44
+ operator: uxValue.operation.operator,
45
+ left: cleanUxValue(uxValue.operation.left),
46
+ right: cleanUxValue(uxValue.operation.right),
47
+ },
48
+ }
49
+
50
+ case 'LIST_CONTAINER':
51
+ return {
52
+ type: uxValue.type,
53
+ container: uxValue.container.map((item) => cleanUxValue(item)),
54
+ }
55
+
56
+ case 'MAP_CONTAINER':
57
+ return {
58
+ type: uxValue.type,
59
+ container: Object.entries(uxValue.container).reduce(
60
+ (acc, [key, item]) => ({
61
+ ...acc,
62
+ [key]: cleanUxValue(item),
63
+ }),
64
+ {}
65
+ ),
66
+ }
67
+
68
+ case 'TRANSLATION_LITERAL':
69
+ return { type: uxValue.type, value: uxValue.value }
70
+ }
71
+
72
+ return uxValue
73
+ }