@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,410 @@
1
+ import {
2
+ Acl,
3
+ TableAcl,
4
+ NamedAcl,
5
+ AclAttributes,
6
+ AclBase,
7
+ AclOperations,
8
+ AclTypes,
9
+ } from '@servicenow/sdk-core/runtime/app'
10
+ import {
11
+ Context,
12
+ Plugin,
13
+ extractCallExpression,
14
+ generateCallExpressionExportForDocument,
15
+ getOrCreateEntitySourceFile,
16
+ getSysUpdateName,
17
+ linkDocument,
18
+ removeNode,
19
+ transformFunctionArguments,
20
+ FluentDiagnostic,
21
+ findObjectPropertyValue,
22
+ EntityData,
23
+ ObjectData,
24
+ } from '@servicenow/sdk-build-core'
25
+ import { addObjectToArrayById, removeObjectFromArrayById } from './Util'
26
+ import { Record as DBRecord, TableName } from '@servicenow/sdk-core/runtime/db'
27
+ import { scriptInfo, ScriptInfo, buildScriptImport, except } from '../scripts/scriptUtils'
28
+ import * as ts from 'ts-morph'
29
+ import { AclNamedTypes } from '@servicenow/sdk-core/runtime/app'
30
+ import { processScript } from '../ScriptTemplatePlugin'
31
+ import { Diagnostic } from '@servicenow/sdk-project'
32
+
33
+ type AclDoc = {
34
+ table: string
35
+ data: {
36
+ sys_id: string
37
+ }
38
+ }
39
+
40
+ type AclRole = {
41
+ table: string
42
+ data: {
43
+ sys_security_acl: string
44
+ sys_user_role: string
45
+ }
46
+ }
47
+
48
+ type AclData = AclBase & {
49
+ name: string
50
+ type: keyof typeof AclTypes
51
+ }
52
+
53
+ const ExecuteOnlyTypes = new Set([
54
+ 'client_callable_flow_object',
55
+ 'client_callable_script_include',
56
+ 'graphql',
57
+ 'processor',
58
+ 'rest_endpoint',
59
+ ])
60
+
61
+ function aclAsRecord<T extends TableName>(config: Acl<T>) {
62
+ const tableConfig = config as TableAcl<T>
63
+ const namedConfig = config as NamedAcl
64
+
65
+ const field = String(tableConfig.field || '')
66
+ const name = namedConfig.name || (field ? `${tableConfig.table}.${field}` : tableConfig.table)
67
+ if (!name) {
68
+ throw new Error(`ACL of type ${config.type} and $id '${config.$id}' has no name or associated table`)
69
+ }
70
+
71
+ if (config.operation === 'execute' && !ExecuteOnlyTypes.has(config.type)) {
72
+ throw new Error(`ACL ${name} cannot use operation 'execute', because it is of type '${config.type}'`)
73
+ }
74
+ if (config.script && config.type === 'graphql') {
75
+ throw new Error(`ACL ${name} does not support scripts, because it is of type ${config.type}`)
76
+ }
77
+ if (tableConfig.applies_to && tableConfig.type !== 'record') {
78
+ throw new Error(`ACL ${name} cannot set applies_to unless its type is 'record'`)
79
+ }
80
+ if (tableConfig.operation === 'add_to_list' && (config.script || config.condition)) {
81
+ throw new Error(`ACL ${name} cannot have a script or condition due to its 'add_to_list' operation`)
82
+ }
83
+
84
+ return DBRecord({
85
+ table: 'sys_security_acl',
86
+ $id: config.$id,
87
+ data: {
88
+ name,
89
+ active: config.active ?? true,
90
+ type: AclTypes[config.type ?? 'record'],
91
+ operation: AclOperations[config.operation as keyof typeof AclOperations] || config.operation || '',
92
+ decision_type: config.decision_type ?? 'allow',
93
+ description: config.description || '',
94
+ local_or_existing: config.local_or_existing || 'Local',
95
+ admin_overrides: config.admin_overrides ?? true,
96
+ advanced: !!config.script,
97
+ condition: config.condition ?? '',
98
+ applies_to: tableConfig.applies_to ?? '',
99
+ script: config.script as any,
100
+ security_attribute:
101
+ (config.security_attribute && AclAttributes[config.security_attribute as keyof typeof AclAttributes]) ||
102
+ config.security_attribute ||
103
+ '',
104
+ },
105
+ })
106
+ }
107
+
108
+ export default Plugin({
109
+ name: 'Acl',
110
+ ownedTables: {
111
+ sys_security_acl: { diagnosticLevel: Diagnostic.Level.Warn },
112
+ },
113
+ extractors: {
114
+ entity: {
115
+ CallExpression(node, context) {
116
+ const result = extractCallExpression(Acl, 'record', node, context, (acl) =>
117
+ context.registerExplicitId('sys_security_acl', acl.$id)
118
+ )
119
+
120
+ if (!result.handled || !(0 in result.data)) {
121
+ return result
122
+ }
123
+
124
+ const acl = result.data[0]
125
+ const scriptVal = getScript(acl, context)
126
+ const aclRecord = aclAsRecord(acl.getValue())
127
+
128
+ const m2mRoleToAcl =
129
+ acl
130
+ .getProperty('roles')
131
+ ?.getValue()
132
+ ?.map((role) => {
133
+ const roleId = typeof role === 'string' ? role : role.$id
134
+
135
+ const aclId = context.keys.registerExplicitId('sys_security_acl', aclRecord.$id)
136
+ const roleSysId =
137
+ typeof role === 'string' ? role : context.registerExplicitId('sys_user_role', role.$id)
138
+ const guid = context.keys.registerCompositeId('sys_security_acl_role', {
139
+ sys_security_acl: aclRecord.$id,
140
+ sys_user_role: roleId,
141
+ })
142
+
143
+ return new EntityData(
144
+ 'record',
145
+ guid,
146
+ ObjectData.fromObjectValue(
147
+ DBRecord({
148
+ table: 'sys_security_acl_role',
149
+ $id: guid,
150
+ data: {
151
+ sys_security_acl: aclId,
152
+ sys_user_role: roleSysId,
153
+ },
154
+ }),
155
+ node
156
+ ),
157
+ node
158
+ )
159
+ }) || []
160
+
161
+ const diagnostics = performDiagnosticChecksOnSecurityAttribute(
162
+ node,
163
+ acl.getValue(),
164
+ m2mRoleToAcl,
165
+ context.mode
166
+ )
167
+
168
+ const aclWithScript = { ...aclRecord, data: { ...aclRecord.data, script: scriptVal || '' } }
169
+ const aclEntityData = new EntityData(
170
+ 'record',
171
+ acl.getGuid(),
172
+ ObjectData.fromObjectValue(aclWithScript, node),
173
+ node
174
+ )
175
+
176
+ return {
177
+ handled: true,
178
+ diagnostics: [...result.diagnostics, ...diagnostics],
179
+ data: [aclEntityData, ...m2mRoleToAcl],
180
+ }
181
+ },
182
+ },
183
+ raw: {
184
+ FunctionDeclaration(node, context) {
185
+ const info = scriptInfo(node, context, Acl.name)
186
+ if (!info) {
187
+ return { handled: false }
188
+ }
189
+
190
+ return { handled: true, diagnostics: [], data: [info] }
191
+ },
192
+
193
+ FunctionExpression(node, context) {
194
+ const info = scriptInfo(node, context, Acl.name)
195
+ if (!info) {
196
+ return { handled: false }
197
+ }
198
+
199
+ return { handled: true, diagnostics: [], data: [info] }
200
+ },
201
+ },
202
+ },
203
+ arrangers: {
204
+ record(document, context) {
205
+ if ((document.data as any).table !== 'sys_security_acl_role') {
206
+ return { handled: false }
207
+ }
208
+
209
+ const aclId = (document.data as any).data.sys_security_acl
210
+ const aclDoc = context.getDocument(aclId, 'record')
211
+ const aclDeleted = aclDoc && aclDoc.action === 'DELETE'
212
+ if (aclDeleted && document.action === 'INSERT_OR_UPDATE') {
213
+ throw new Error(
214
+ `Existing sys_security_acl_role with id ${document.guid} is referencing deleted ACL ${aclId}. This can be fixed by either deleting the sys_security_acl_role record or updating its sys_security_acl reference.`
215
+ )
216
+ }
217
+
218
+ return {
219
+ handled: true,
220
+ result: aclId ? { kind: 'record', guid: aclId } : undefined,
221
+ }
222
+ },
223
+ },
224
+ generators: {
225
+ record(document, context, linkedDocuments) {
226
+ const documentType = (document.data as any)?.table
227
+ if (documentType === 'sys_security_acl_role') {
228
+ if (!document.parent) {
229
+ return undefined
230
+ }
231
+ const acl = linkedDocuments.find(
232
+ (doc) =>
233
+ (doc.data as AclDoc).table === 'sys_security_acl' &&
234
+ doc.guid === (document.data as AclRole).data.sys_security_acl
235
+ )
236
+
237
+ return acl && acl.node ? { ...document, node: acl.node } : undefined
238
+ }
239
+
240
+ if (documentType !== 'sys_security_acl') {
241
+ return undefined
242
+ }
243
+
244
+ return linkDocument(
245
+ document,
246
+ generateCallExpressionExportForDocument(
247
+ context,
248
+ {
249
+ sourceFile: getOrCreateEntitySourceFile(
250
+ context,
251
+ getSysUpdateName(document, 'sys_security_acl')
252
+ ),
253
+ moduleSpecifier: '@servicenow/sdk/core',
254
+ },
255
+ Acl,
256
+ { $id: document.guid }
257
+ ).getExpressionIfKindOrThrow(ts.SyntaxKind.CallExpression)
258
+ )
259
+ },
260
+ },
261
+ transformers: {
262
+ record: {
263
+ CallExpression(document, context) {
264
+ const documentType = (document.data as any)?.table
265
+
266
+ const [args] = document.node.getArguments()
267
+ if (!ts.Node.isObjectLiteralExpression(args)) {
268
+ return false
269
+ }
270
+
271
+ if (documentType === 'sys_security_acl_role') {
272
+ if (!document.parent) {
273
+ return true
274
+ }
275
+ const acl2Role = (document.data as AclRole).data
276
+ if (document.action === 'DELETE') {
277
+ const roleId = acl2Role.sys_user_role
278
+ if (roleId) {
279
+ removeObjectFromArrayById(document, 'roles', context)
280
+ }
281
+ return true
282
+ }
283
+
284
+ addObjectToArrayById(document, 'roles', acl2Role.sys_user_role, context)
285
+ return true
286
+ }
287
+
288
+ if (document.action === 'DELETE') {
289
+ removeNode(document.node)
290
+ return true
291
+ }
292
+
293
+ if (documentType !== 'sys_security_acl' || !document.changedData) {
294
+ return false
295
+ }
296
+
297
+ const modifiedAclData = (document.changedData as any).data as AclBase
298
+ const unmodifiedAclData = (document.data as any).data as AclData
299
+ const type = reverseLookup(AclTypes, unmodifiedAclData.type)
300
+
301
+ const aclData = {
302
+ ...except(
303
+ modifiedAclData,
304
+ (key) =>
305
+ key.startsWith('sys_') ||
306
+ key === 'advanced' ||
307
+ key === 'name' ||
308
+ key === 'operation' ||
309
+ key === 'script'
310
+ ),
311
+ ...getTableOrName(type, unmodifiedAclData.name),
312
+ security_attribute:
313
+ reverseLookup(AclAttributes, unmodifiedAclData.security_attribute) ||
314
+ unmodifiedAclData.security_attribute,
315
+ operation: reverseLookup(AclOperations, unmodifiedAclData.operation) || unmodifiedAclData.operation,
316
+ type,
317
+ } as any
318
+
319
+ const { script } = modifiedAclData as { script?: string }
320
+ if (script !== undefined) {
321
+ processScript(args, 'script', script, document.guid, 'sys_security_acl', context)
322
+ }
323
+
324
+ return transformFunctionArguments(document.node, Acl, aclData)
325
+ },
326
+ },
327
+ },
328
+ })
329
+
330
+ // security_attribute.is_localizsed ? 'Local' : 'Existing'
331
+ function performDiagnosticChecksOnSecurityAttribute<T>(
332
+ callExpression: ts.CallExpression,
333
+ aclData: AclBase,
334
+ roles: Array<T>,
335
+ mode: 'serialize' | 'transform'
336
+ ) {
337
+ const diagnostics: FluentDiagnostic[] = []
338
+
339
+ if (roles.length < 1) {
340
+ const attributeRecord = aclData.security_attribute as DBRecord<'sys_security_attribute'>
341
+ if (!aclData.security_attribute && !aclData.script && !aclData.condition) {
342
+ diagnostics.push(
343
+ new FluentDiagnostic(
344
+ callExpression,
345
+ 'ACLs must have at least one of the following: roles, security_attribute, condition, or script',
346
+ { level: mode === 'transform' ? Diagnostic.Level.Warn : Diagnostic.Level.Error }
347
+ )
348
+ )
349
+ } else if (attributeRecord && attributeRecord.data) {
350
+ if (
351
+ aclData.local_or_existing === 'Existing' &&
352
+ (attributeRecord.data.type !== 'compound' || attributeRecord.data.is_localized)
353
+ ) {
354
+ diagnostics.push(
355
+ new FluentDiagnostic(
356
+ findObjectPropertyValue(callExpression, 'security_attribute'),
357
+ `Invalid ACL with 'Existing' sys_security_attribute: Must have a security_attribute with a type of 'compound' and is_localized set to false.`
358
+ )
359
+ )
360
+ } else if (
361
+ (!aclData.local_or_existing || aclData.local_or_existing === 'Local') &&
362
+ !attributeRecord.data.is_localized
363
+ ) {
364
+ diagnostics.push(
365
+ new FluentDiagnostic(
366
+ findObjectPropertyValue(callExpression, 'security_attribute'),
367
+ `Invalid ACL with 'Local' security_attribute: security_attribute must have is_localized set to true`
368
+ )
369
+ )
370
+ }
371
+ }
372
+ }
373
+ return diagnostics
374
+ }
375
+
376
+ function getTableOrName(type: keyof typeof AclTypes, name: string) {
377
+ if (AclNamedTypes[type]) {
378
+ return { name }
379
+ }
380
+ const tablePathParts = name.split('.')
381
+ const table = tablePathParts[0] || ''
382
+ const field = tablePathParts[1] || ''
383
+
384
+ return { table, field }
385
+ }
386
+
387
+ // TODO: The casting and type guards in this function are a bit of a mess and prone to errors.
388
+ function getScript(acl: EntityData, context: Context) {
389
+ const scriptData = acl.getProperty('script')?.getValue() as ScriptInfo
390
+ let scriptVal: string
391
+ if (scriptData && scriptData.filePath) {
392
+ const { filePath, functionName, isDefault } = scriptData
393
+ scriptVal = buildScriptImport(
394
+ filePath,
395
+ functionName,
396
+ isDefault,
397
+ context,
398
+ (funcName) => `answer = ${funcName}(current)\n`
399
+ )
400
+ } else {
401
+ scriptVal = scriptData as unknown as string
402
+ }
403
+ return scriptVal
404
+ }
405
+
406
+ function reverseLookup<T extends object>(obj: T, sysId): keyof T {
407
+ return (Object.entries(obj)
408
+ .filter(([_, id]) => id === sysId)
409
+ .map(([key]) => key)[0] || '') as keyof T
410
+ }
@@ -0,0 +1,225 @@
1
+ import { Role, RoleConfig, roleSchema } from '@servicenow/sdk-core/runtime/app'
2
+ import {
3
+ Context,
4
+ Plugin,
5
+ linkDocument,
6
+ extractCallExpression,
7
+ FluentDiagnostic,
8
+ findObjectPropertyValue,
9
+ EntityData,
10
+ ObjectData,
11
+ Data,
12
+ } from '@servicenow/sdk-build-core'
13
+ import { Record } from '@servicenow/sdk-core/runtime/db'
14
+ import {
15
+ generateCallExpressionExportForDocument,
16
+ getOrCreateEntitySourceFile,
17
+ transformFunctionArguments,
18
+ getSysUpdateName,
19
+ removeNode,
20
+ } from '@servicenow/sdk-build-core'
21
+ import { addObjectToArrayById, removeObjectFromArrayById } from './Util'
22
+ import { SyntaxKind, Node } from 'ts-morph'
23
+ import { except } from '../scripts/scriptUtils'
24
+ import { RecordPlugin } from '../db/RecordPlugin'
25
+ import { Diagnostic } from '@servicenow/sdk-project'
26
+
27
+ function mapRoleToRole(context, roleId, containedRoleId) {
28
+ const m2mId = context.keys.registerCompositeId('sys_user_role_contains', {
29
+ role: roleId,
30
+ contains: containedRoleId,
31
+ })
32
+ return Record({
33
+ table: 'sys_user_role_contains',
34
+ $id: m2mId,
35
+ data: {
36
+ contains: containedRoleId,
37
+ role: roleId,
38
+ },
39
+ })
40
+ }
41
+
42
+ type RoleDoc = {
43
+ table: string
44
+ data: {
45
+ sys_id: string
46
+ }
47
+ }
48
+
49
+ type RoleContainsDoc = {
50
+ table: string
51
+ data: {
52
+ role: string
53
+ contains: string
54
+ }
55
+ }
56
+
57
+ export default Plugin({
58
+ name: 'Role',
59
+ ownedTables: {
60
+ sys_user_role: { diagnosticLevel: Diagnostic.Level.Error },
61
+ },
62
+ extractors: {
63
+ entity: {
64
+ CallExpression: (node, context) => {
65
+ const result = extractCallExpression(
66
+ Role as (role: RoleConfig) => RoleConfig,
67
+ 'role',
68
+ node,
69
+ context,
70
+ ({ $id }) => context.registerExplicitId('sys_user_role', $id as string)
71
+ )
72
+
73
+ if (!result.handled || !(0 in result.data)) {
74
+ return result
75
+ }
76
+
77
+ const role = result.data[0]
78
+ const diagnostics = result.diagnostics
79
+ const scope = context.app?.config?.scope
80
+ const name = role.getProperty('name').getValue()
81
+ if (!name.startsWith(`${scope}.`)) {
82
+ const nameValue = findObjectPropertyValue(node, 'name')
83
+ diagnostics.push(new FluentDiagnostic(nameValue, `Role name must begin with '${scope}.'`))
84
+ }
85
+
86
+ return {
87
+ handled: true,
88
+ diagnostics,
89
+ data: [role],
90
+ }
91
+ },
92
+ },
93
+ },
94
+ composers: {
95
+ entity: {
96
+ role(entity, context) {
97
+ const scope = context.app?.config?.scope
98
+ const guid = entity.getGuid()
99
+ const node = entity.getNode()
100
+
101
+ const role = roleSchema.safeParse(entity.getValue())
102
+ if (!role.success) {
103
+ return undefined
104
+ }
105
+
106
+ const { $id: _, name, contains_roles, ...rest } = role.data
107
+
108
+ const roleToRoles: Record<'sys_user_role_contains'>[] = []
109
+ const containsRoles = entity.getProperty('contains_roles')
110
+ if (Data.isArray(containsRoles)) {
111
+ for (const containedRole of containsRoles.getElements()) {
112
+ const containedRoleSysId = Data.isEntity(containedRole)
113
+ ? containedRole.getGuid()
114
+ : containedRole.getValue() // Should be a string
115
+
116
+ roleToRoles.push(mapRoleToRole(context, guid, containedRoleSysId))
117
+ }
118
+ }
119
+
120
+ const suffix = name.replace(new RegExp(`^${scope}\\.`), '')
121
+
122
+ const roleRecord = Record({
123
+ table: 'sys_user_role',
124
+ $id: guid,
125
+ data: {
126
+ name,
127
+ suffix,
128
+ ...rest,
129
+ },
130
+ })
131
+
132
+ return context.composeEntities(
133
+ [roleRecord, ...roleToRoles].map(
134
+ (r) => new EntityData('record', r.$id.toString(), ObjectData.fromObjectValue(r, node), node)
135
+ ),
136
+ [RecordPlugin]
137
+ )
138
+ },
139
+ },
140
+ },
141
+ arrangers: {
142
+ record(document) {
143
+ if ((document.data as any).table !== 'sys_user_role_contains') {
144
+ return { handled: false }
145
+ }
146
+
147
+ const roleId = (document.data as any).data.role
148
+ return {
149
+ handled: true,
150
+ result: roleId ? { kind: 'record', guid: roleId } : undefined,
151
+ }
152
+ },
153
+ },
154
+ generators: {
155
+ record(document, context: Context, linkedDocuments) {
156
+ const documentType = (document.data as any)?.table
157
+ if (documentType === 'sys_user_role_contains') {
158
+ const role = linkedDocuments.find(
159
+ (doc) =>
160
+ (doc.data as RoleDoc).table === 'sys_user_role' &&
161
+ doc.guid === (document.data as RoleContainsDoc).data.role
162
+ )
163
+ return role && role.node ? { ...document, node: role.node } : undefined
164
+ }
165
+
166
+ if (documentType !== 'sys_user_role') {
167
+ return undefined
168
+ }
169
+
170
+ return linkDocument(
171
+ document,
172
+ generateCallExpressionExportForDocument(
173
+ context,
174
+ {
175
+ sourceFile: getOrCreateEntitySourceFile(context, getSysUpdateName(document, 'sys_user_role')),
176
+ moduleSpecifier: '@servicenow/sdk/core',
177
+ },
178
+ Role,
179
+ { $id: document.guid }
180
+ ).getExpressionIfKindOrThrow(SyntaxKind.CallExpression)
181
+ )
182
+ },
183
+ },
184
+ transformers: {
185
+ record: {
186
+ CallExpression(document, context) {
187
+ const documentType = (document.data as any)?.table
188
+ if (documentType === 'sys_user_role_contains') {
189
+ const [args] = document.node.getArguments()
190
+ if (!Node.isObjectLiteralExpression(args) || !document.parent) {
191
+ return false
192
+ }
193
+ if (document.action === 'DELETE') {
194
+ removeObjectFromArrayById(document, 'contains_roles', context)
195
+ return true
196
+ }
197
+ const m2mData = (document.data as RoleContainsDoc).data
198
+ addObjectToArrayById(document, 'contains_roles', m2mData.contains, context)
199
+ return true
200
+ }
201
+
202
+ if (documentType !== 'sys_user_role') {
203
+ return false
204
+ }
205
+
206
+ if (document.action === 'DELETE') {
207
+ removeNode(document.node)
208
+ return true
209
+ }
210
+
211
+ const rawRoleData = (document.changedData as any).data
212
+ const roleData = except(
213
+ rawRoleData,
214
+ (key) =>
215
+ key.startsWith('sys_') ||
216
+ key === 'requires_subscription' ||
217
+ key === 'suffix' ||
218
+ key === 'includes_roles'
219
+ )
220
+
221
+ return transformFunctionArguments(document.node, Role, roleData)
222
+ },
223
+ },
224
+ },
225
+ })