@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,119 @@
1
+ import { CallExpression } from 'ts-morph'
2
+ import {
3
+ ConfigEntity,
4
+ ConfigurationFunctionExtractor,
5
+ Context,
6
+ EntityData,
7
+ LinkedDocument,
8
+ ObjectData,
9
+ Plugin,
10
+ recordXml,
11
+ unloadBuilder,
12
+ } from '@servicenow/sdk-build-core'
13
+ import { ATF, getRecordIdIfRecord } from './ATFComposer'
14
+ import { RecordPlugin } from '../db/RecordPlugin'
15
+ import { Record } from '@servicenow/sdk-core/runtime/db'
16
+
17
+ export default Plugin({
18
+ name: 'Test',
19
+ extractors: {
20
+ entity: {
21
+ CallExpression: (node: CallExpression, context: Context) => {
22
+ // This constructs an entity of kind 'test' and returns it. This entity is then parsed by the composer
23
+ const configExtractor = new ConfigurationFunctionExtractor(node, context, ATF)
24
+ const result = configExtractor.extractConfigFunction('test', context.logger)
25
+ if (!result) {
26
+ return { handled: false }
27
+ }
28
+
29
+ return {
30
+ handled: true,
31
+ diagnostics: [],
32
+ data: [result],
33
+ }
34
+ },
35
+ },
36
+ },
37
+ composers: {
38
+ entity: {
39
+ test(entity, context) {
40
+ const node = entity.getNode()
41
+ const data = entity.getValue()
42
+ const dataType: ConfigEntity = data as any
43
+ const stepCreator = new ATF(dataType.inputObject as any, context)
44
+ // Iterate over all steps parsed from the AST and call their corresponding implementation in the ATF Composer
45
+ const configEntries = dataType.configEntries
46
+ for (const index of Object.keys(configEntries)) {
47
+ const step = configEntries[index]
48
+ if (step.name != 'constructor' && stepCreator[step.name]) {
49
+ try {
50
+ stepCreator[step.name](step.info)
51
+ // Step incrementation is handled here so that you do not need to be concerned when adding new step
52
+ stepCreator.incrementOrder()
53
+ } catch (e) {
54
+ context.logger.error(`Caught error composing step '${step.name}': `, e)
55
+ throw e
56
+ }
57
+ }
58
+ }
59
+ // Add test to return data
60
+ const returnData: LinkedDocument[] = context.composeEntities(
61
+ [
62
+ new EntityData(
63
+ 'record',
64
+ context.registerExplicitId('sys_atf_test', stepCreator.test.$id as string),
65
+ ObjectData.fromObjectValue(stepCreator.test, node),
66
+ node
67
+ ),
68
+ ],
69
+ [RecordPlugin]
70
+ )
71
+
72
+ // Add steps and their variables to return data
73
+ stepCreator.stepRecords.forEach((step) => {
74
+ const stepID = getRecordIdIfRecord(step, context)
75
+ returnData.push({
76
+ guid: stepID,
77
+ kind: 'step',
78
+ node,
79
+ data: {
80
+ step,
81
+ data: stepCreator.records[stepID],
82
+ },
83
+ })
84
+ })
85
+ return returnData
86
+ },
87
+ },
88
+ },
89
+ serializers: {
90
+ step(document, context) {
91
+ const data: {
92
+ step: Record<'sys_atf_step'>
93
+ data: (Record<'sys_variable_value'> | Record<'sys_element_mapping'>)[]
94
+ } = document.data as any
95
+ const builder = unloadBuilder(context)
96
+ // First create the step xml
97
+ const stepNode = recordXml(builder.xml, 'sys_atf_step', getRecordIdIfRecord(data.step, context) as string)
98
+ Object.keys(data.step.data).forEach((key) => {
99
+ stepNode.field(key, getRecordIdIfRecord(data.step.data[key], context))
100
+ })
101
+
102
+ // Then add all variables and element mappings associated with the step
103
+ data.data.forEach((varRecord) => {
104
+ const varNode = recordXml(builder.xml, varRecord.table, getRecordIdIfRecord(varRecord, context))
105
+ Object.keys(varRecord.data).forEach((key) => {
106
+ const value = getRecordIdIfRecord(varRecord.data[key], context)
107
+ varNode.field(key, typeof value === 'undefined' ? '' : value)
108
+ })
109
+ })
110
+
111
+ // Return the entire thing as a bundle
112
+ return {
113
+ name: `sys_atf_step_${document.guid}.xml`,
114
+ directory: 'update',
115
+ content: builder.end(),
116
+ }
117
+ },
118
+ },
119
+ })
@@ -0,0 +1 @@
1
+ export { default as TestPlugin } from './TestPlugin'
@@ -0,0 +1,117 @@
1
+ import * as db from '@servicenow/sdk-core/runtime/db'
2
+ import { EntityData, FluentDiagnostic, ObjectData, Plugin, extractCallExpression } from '@servicenow/sdk-build-core'
3
+
4
+ const validFunctions = [
5
+ 'add',
6
+ 'coalesce',
7
+ 'concat',
8
+ 'datediff',
9
+ 'dayofweek',
10
+ 'distance_sphere',
11
+ 'divide',
12
+ 'greatest',
13
+ 'least',
14
+ 'length',
15
+ 'multiply',
16
+ 'position',
17
+ 'substring',
18
+ 'subtract',
19
+ ]
20
+
21
+ const regExp = new RegExp(`^glidefunction:(${validFunctions.join('|')})[(].*?[)]$`, 'g')
22
+
23
+ function ColumnPlugin<
24
+ const A extends unknown[],
25
+ const E extends A extends [infer T extends Record<string, unknown>] ? T : never,
26
+ const K extends string,
27
+ >(fn: (...args: A) => E, entityKind: K) {
28
+ return Plugin({
29
+ name: `${entityKind}Column`,
30
+ extractors: {
31
+ entity: {
32
+ CallExpression(node, context) {
33
+ const result = extractCallExpression(
34
+ fn,
35
+ `${entityKind}Column`,
36
+ node,
37
+ context,
38
+ () => 'NO_GUID_GENERATED' // TODO: We shouldn't need to generate any GUID here but maybe should provide something unique to be on the safe side
39
+ )
40
+ if (!result.handled || !(0 in result.data)) {
41
+ return result
42
+ }
43
+
44
+ const entity = result.data[0]
45
+ const diagnostics = result.diagnostics
46
+ const functionDefinition = entity.getProperty('function_definition')?.getValue()
47
+ if (functionDefinition) {
48
+ if (typeof functionDefinition !== 'string' || !functionDefinition.match(regExp)) {
49
+ diagnostics.push(
50
+ new FluentDiagnostic(
51
+ node,
52
+ `'function_definition' must start with 'glidefunction:' and include a single function call to a predefined function:\n${validFunctions.join(
53
+ '\n'
54
+ )}`
55
+ )
56
+ )
57
+ }
58
+ }
59
+
60
+ return {
61
+ handled: true,
62
+ diagnostics,
63
+ data: [
64
+ new EntityData(
65
+ entity.getKind(),
66
+ entity.getGuid(),
67
+ ObjectData.fromObjectValue(
68
+ {
69
+ ...entity.getValue(),
70
+ entityKind,
71
+ },
72
+ entity.getNode()
73
+ ),
74
+ entity.getNode()
75
+ ),
76
+ ],
77
+ }
78
+ },
79
+ },
80
+ },
81
+ })
82
+ }
83
+
84
+ export const StringColumnPlugin = ColumnPlugin(db.StringColumn, 'string')
85
+ export const BooleanColumnPlugin = ColumnPlugin(db.BooleanColumn, 'boolean')
86
+ export const IntegerColumnPlugin = ColumnPlugin(db.IntegerColumn, 'integer')
87
+ export const BasicImageColumnPlugin = ColumnPlugin(db.BasicImageColumn, 'basicImage')
88
+ export const ConditionsColumnPlugin = ColumnPlugin(db.ConditionsColumn, 'conditions')
89
+ export const ChoiceColumnPlugin = ColumnPlugin(db.ChoiceColumn, 'choice')
90
+ export const DecimalColumnPlugin = ColumnPlugin(db.DecimalColumn, 'decimal')
91
+ export const DocumentIdColumnPlugin = ColumnPlugin(db.DocumentIdColumn, 'documentId')
92
+ export const DomainIdColumnPlugin = ColumnPlugin(db.DomainIdColumn, 'domainId')
93
+ export const DomainPathColumnPlugin = ColumnPlugin(db.DomainPathColumn, 'domainPath')
94
+ export const ListColumnPlugin = ColumnPlugin(db.ListColumn, 'list')
95
+ export const ReferenceColumnPlugin = ColumnPlugin(db.ReferenceColumn, 'reference')
96
+ export const RadioColumnPlugin = ColumnPlugin(db.RadioColumn, 'radio')
97
+ export const ScriptColumnPlugin = ColumnPlugin(db.ScriptColumn, 'script')
98
+ export const SystemClassNameColumnPlugin = ColumnPlugin(db.SystemClassNameColumn, 'systemClassName')
99
+ export const TableNameColumnPlugin = ColumnPlugin(db.TableNameColumn, 'tableName')
100
+ export const TranslatedFieldColumnPlugin = ColumnPlugin(db.TranslatedFieldColumn, 'translatedField')
101
+ export const TranslatedTextColumnPlugin = ColumnPlugin(db.TranslatedTextColumn, 'translatedText')
102
+ export const UserRolesColumnPlugin = ColumnPlugin(db.UserRolesColumn, 'userRoles')
103
+ export const VersionColumnPlugin = ColumnPlugin(db.VersionColumn, 'version')
104
+ export const FieldNameColumnPlugin = ColumnPlugin(db.FieldNameColumn, 'fieldName')
105
+
106
+ // Date columns
107
+ export const DateColumnPlugin = ColumnPlugin(db.DateColumn, 'glide_date')
108
+ export const DateTimeColumnPlugin = ColumnPlugin(db.DateTimeColumn, 'glide_date_time')
109
+ export const CalendarDateTimeColumnPlugin = ColumnPlugin(db.CalendarDateTimeColumn, 'calendar_date_time')
110
+ export const BasicDateTimeColumnPlugin = ColumnPlugin(db.BasicDateTimeColumn, 'datetime')
111
+ export const DueDateColumnPlugin = ColumnPlugin(db.DueDateColumn, 'due_date')
112
+ export const IntegerDateColumnPlugin = ColumnPlugin(db.IntegerDateColumn, 'integer_date')
113
+ export const ScheduleDateTimeColumnPlugin = ColumnPlugin(db.ScheduleDateTimeColumn, 'schedule_date_time')
114
+ export const OtherDateColumnPlugin = ColumnPlugin(db.OtherDateColumn, 'date')
115
+
116
+ // Catch all column type which keys of off 'internal_type'
117
+ export const GenericColumnPlugin = ColumnPlugin(db.GenericColumn, 'generic')
@@ -0,0 +1,391 @@
1
+ import { Record, TableName } from '@servicenow/sdk-core/runtime/db'
2
+ import {
3
+ Context,
4
+ Plugin,
5
+ getCallExpressionName,
6
+ isGUID,
7
+ removeNode,
8
+ transformFunctionArguments,
9
+ unloadBuilder,
10
+ linkDocument,
11
+ generateCallExpressionExportForDocument,
12
+ getOrCreateEntitySourceFile,
13
+ XmlData,
14
+ FluentDiagnostic,
15
+ ExtractionResult,
16
+ EntityData,
17
+ extractCallExpression,
18
+ ObjectData,
19
+ } from '@servicenow/sdk-build-core'
20
+ import { z } from 'zod'
21
+ import { CallExpression, SyntaxKind } from 'ts-morph'
22
+
23
+ // TODO: This schema should live with the Record entity function itself
24
+ export const RecordEntitySchema = z.object({
25
+ $id: z.string().or(z.number()),
26
+ data: z.record(z.any()),
27
+ table: z.string(),
28
+ })
29
+
30
+ type RecordData = z.output<typeof RecordEntitySchema>
31
+ type Data = { [key: string]: string | number | boolean }
32
+
33
+ const RecordUpdate = z.object({
34
+ record_update: z.record(z.any()),
35
+ })
36
+
37
+ const RecordUpdateTableSchema = z.object({
38
+ '@_table': z.string(),
39
+ })
40
+
41
+ export const TextStringSchema = z.object({ '#text': z.string() }).transform((val) => val['#text'])
42
+ export const TextBooleanSchema = z.object({ '#text': z.boolean() }).transform((val) => val['#text'])
43
+ export const TextNumberSchema = z.object({ '#text': z.number().or(z.literal('')) }).transform((val) => val['#text'])
44
+
45
+ const SysJournalFieldSchema = z.object({
46
+ element: TextStringSchema,
47
+ element_id: TextStringSchema,
48
+ name: TextStringSchema,
49
+ sys_id: TextStringSchema,
50
+ value: TextStringSchema,
51
+ })
52
+
53
+ export type SysJournalField = z.infer<typeof SysJournalFieldSchema>
54
+
55
+ const ignoreTables = ['sys_metadata_link', 'sys_module', 'sys_ux_lib_asset', 'sys_app', 'sys_store_app']
56
+ const ignoreFields = [
57
+ 'sys_created_by',
58
+ 'sys_created_on',
59
+ 'sys_updated_by',
60
+ 'sys_updated_on',
61
+ 'sys_mod_count',
62
+ 'sys_class_name',
63
+ 'sys_id',
64
+ 'sys_package',
65
+ 'sys_policy',
66
+ 'sys_scope',
67
+ 'sys_update_name',
68
+ ]
69
+ const ignoreXMLElement = ['sys_translated_text', 'sys_es_latest_script']
70
+
71
+ export const RecordXmlSchema = z
72
+ .object({
73
+ '@_action': z.union([z.literal('INSERT_OR_UPDATE'), z.literal('DELETE'), z.literal('delete_multiple')]),
74
+ sys_id: z.object({
75
+ '#text': z.string().min(32).max(32),
76
+ }),
77
+ })
78
+ .catchall(
79
+ z.intersection(
80
+ z.object({
81
+ '#text': z.union([z.string(), z.boolean(), z.number(), z.undefined()]),
82
+ }),
83
+ z.record(z.any()) // capture all attributes
84
+ )
85
+ )
86
+
87
+ export const RecordDocumentSchema = z.object({
88
+ data: z.record(z.union([z.string(), z.number(), z.boolean(), z.undefined(), z.null()])),
89
+ table: z.string(),
90
+ })
91
+
92
+ function cleanRecordUpdate(recordUpdate: globalThis.Record<string, unknown>) {
93
+ ignoreXMLElement.forEach((element) => delete recordUpdate[element])
94
+ return recordUpdate
95
+ }
96
+
97
+ export const RecordPlugin = Plugin({
98
+ name: 'Record',
99
+ extractors: {
100
+ entity: {
101
+ CallExpression: (node, context) => {
102
+ const result = extractCallExpression(
103
+ Record,
104
+ 'record',
105
+ node,
106
+ context,
107
+ (record) => {
108
+ // Take the guid from the id property, a sys_id property, or generate one
109
+ if ('sys_id' in record.data) {
110
+ return record.data['sys_id'] as string
111
+ } else if (isGUID(record.$id as string)) {
112
+ return record.$id as string
113
+ } else {
114
+ return context.keys.registerExplicitId(record.table, record.$id as string)
115
+ }
116
+ },
117
+ (record): record is Record => RecordEntitySchema.safeParse(record).success
118
+ )
119
+
120
+ if (!result.handled || !(0 in result.data)) {
121
+ return result
122
+ }
123
+
124
+ const record = result.data[0]
125
+ const diagnostics = result.diagnostics
126
+ const tableName = record.getProperty('table').getValue()
127
+ const pluginInfo = context.getPluginForTable(tableName)
128
+
129
+ if (pluginInfo) {
130
+ const { logLevel, api } = pluginInfo
131
+ const tableNode = node
132
+ .getFirstChildByKindOrThrow(SyntaxKind.ObjectLiteralExpression)
133
+ .getPropertyOrThrow('table')
134
+
135
+ diagnostics.push(
136
+ new FluentDiagnostic(
137
+ tableNode,
138
+ `${tableName} should not be created by the Record API. Please use the ${api} API.`,
139
+ { level: logLevel }
140
+ )
141
+ )
142
+ }
143
+
144
+ // TODO: Get this out of the RecordPlugin somehow. Maybe need to provide a new mechanism for
145
+ // plugins to add diagnostics for entities extracted by other plugins?
146
+ if (tableName === 'sys_ui_view') {
147
+ const viewName = record.getProperty('data').getProperty('name')?.getValue()
148
+ const viewDataNode = node
149
+ .getFirstChildByKindOrThrow(SyntaxKind.ObjectLiteralExpression)
150
+ .getPropertyOrThrow('data')
151
+ .asKindOrThrow(SyntaxKind.PropertyAssignment)
152
+ .getInitializerIfKind(SyntaxKind.ObjectLiteralExpression)
153
+
154
+ const viewNameNode = viewDataNode?.getPropertyOrThrow('name')
155
+ const nameRegex = new RegExp(`^[a-zA-Z0-9_]+$`)
156
+
157
+ if (typeof viewName !== 'string' || !viewName.match(nameRegex)) {
158
+ diagnostics.push(
159
+ new FluentDiagnostic(viewNameNode!, `View name can only contain alphanumeric characters`)
160
+ )
161
+ }
162
+ }
163
+
164
+ return {
165
+ handled: true,
166
+ diagnostics,
167
+ data: [record],
168
+ }
169
+ },
170
+ },
171
+ xml(xml, context) {
172
+ // Parse out record_Update info
173
+ const recordUpdateResult = RecordUpdate.safeParse(xml.data)
174
+ if (!recordUpdateResult.success) {
175
+ return undefined
176
+ }
177
+
178
+ // Parse root level @_table from record_update
179
+ const recordUpdate = cleanRecordUpdate(recordUpdateResult.data.record_update)
180
+ let tableRecord, recordTable
181
+ const table = RecordUpdateTableSchema.safeParse(recordUpdate)
182
+ if (!table.success) {
183
+ // TODO A record plugin should only handle 1 table record?
184
+ // Taking the first one for now. Shouldn't assume this is the right one
185
+ recordTable = Object.keys(recordUpdate)[0] as string
186
+ tableRecord = recordUpdate[recordTable]
187
+ } else {
188
+ recordTable = table.data['@_table']
189
+ tableRecord = recordUpdate[table.data['@_table']]
190
+ }
191
+ const ignoreTableList = [...context.app.config.ignoreTransformTableList, ...ignoreTables]
192
+ if (!tableRecord || ignoreTableList.includes(recordTable)) {
193
+ return undefined
194
+ }
195
+ const recordParse = RecordXmlSchema.safeParse(tableRecord)
196
+ if (!recordParse.success) {
197
+ return undefined
198
+ }
199
+
200
+ if (
201
+ tableRecord['@_action'] !== 'DELETE' &&
202
+ Object.keys(recordUpdate).some((k) => k !== recordTable && !k.startsWith('@_'))
203
+ ) {
204
+ // Record contains other entries in the file that cannot be processed
205
+ return undefined
206
+ }
207
+
208
+ try {
209
+ const {
210
+ '@_action': action,
211
+ sys_id: { '#text': sysId },
212
+ ...rest
213
+ } = tableRecord
214
+
215
+ const record = Object.entries(rest).reduce((out, [k, v]) => {
216
+ out[k] = v ? v['#text'] : undefined
217
+ return out
218
+ }, {})
219
+
220
+ return new XmlData(
221
+ {
222
+ id: sysId,
223
+ table: recordTable,
224
+ data: record,
225
+ },
226
+ xml.filePath,
227
+ 'record',
228
+ action
229
+ )
230
+ } catch (err) {
231
+ return undefined // TODO: What are we catching here?
232
+ }
233
+ },
234
+ },
235
+ composers: {
236
+ entity: {
237
+ record(entity, context) {
238
+ const { $id, ...record } = RecordEntitySchema.parse(entity.getValue())
239
+ const fixedData = Object.entries(record.data).reduce((out, [c, v]) => {
240
+ if (!v || typeof v !== 'object') {
241
+ out[c] = v
242
+ return out
243
+ }
244
+
245
+ const record = RecordEntitySchema.safeParse(v)
246
+ if (record.success) {
247
+ // User can override hashing w/ explicit sys_id
248
+ if ('sys_id' in record.data.data) {
249
+ out[c] = record.data.data['sys_id']
250
+ } else if (isGUID(record.data.$id)) {
251
+ out[c] = record.data.$id
252
+ } else {
253
+ out[c] = context.keys.registerExplicitId(record.data.table, record.data.$id)
254
+ }
255
+ return out
256
+ }
257
+
258
+ throw Error(`Unknown object assigned to '${c}' column: ${JSON.stringify(v, undefined, 4)}`)
259
+ }, {})
260
+
261
+ const data: RecordData = {
262
+ table: record.table,
263
+ $id, //preserve the original id if set
264
+ data: fixedData,
265
+ }
266
+
267
+ return {
268
+ kind: 'record',
269
+ node: entity.getNode(),
270
+ guid: entity.getGuid(),
271
+ data,
272
+ }
273
+ },
274
+ },
275
+ xml: {
276
+ record(xml) {
277
+ const table = xml.data['table'] as string
278
+ const sysId = xml.data['id'] as string
279
+
280
+ return {
281
+ action: xml.action,
282
+ guid: sysId,
283
+ kind: 'record',
284
+ data: { table, data: xml.data['data'] },
285
+ }
286
+ },
287
+ },
288
+ },
289
+ serializers: {
290
+ record(document, context) {
291
+ const { table, data } = RecordDocumentSchema.parse(document.data)
292
+ const recordBuilder = unloadBuilder(context)
293
+ const builder = recordBuilder.record(table, document.guid, document.action)
294
+
295
+ Object.entries(data)
296
+ .filter(([c, v]) => typeof v !== 'undefined' && c !== 'sys_id')
297
+ .forEach(([c, v]) => builder.field(c, v))
298
+
299
+ return {
300
+ name: `${table}_${document.guid}.xml`,
301
+ directory: 'update',
302
+ content: recordBuilder.end(),
303
+ }
304
+ },
305
+ },
306
+ generators: {
307
+ record(document, context) {
308
+ return linkDocument(
309
+ document,
310
+ generateCallExpressionExportForDocument(
311
+ context,
312
+ {
313
+ sourceFile: getOrCreateEntitySourceFile(
314
+ context,
315
+ `${document.data?.['table']}_${document.guid}`
316
+ ),
317
+ moduleSpecifier: '@servicenow/sdk/core',
318
+ },
319
+ Record,
320
+ { $id: document.guid }
321
+ ).getExpressionIfKindOrThrow(SyntaxKind.CallExpression)
322
+ )
323
+ },
324
+ },
325
+ transformers: {
326
+ record: {
327
+ CallExpression(document) {
328
+ if (getCallExpressionName(document.node) !== Record.name) {
329
+ return false
330
+ }
331
+
332
+ if (document.action === 'DELETE') {
333
+ removeNode(document.node)
334
+ return true
335
+ }
336
+
337
+ if (!document.changedData) {
338
+ return false
339
+ }
340
+
341
+ const { table } = document.data as RecordData
342
+ const data = (document.changedData as any).data as Data
343
+
344
+ const properties = Object.keys(data).reduce((out, key) => {
345
+ if (ignoreFields.includes(key)) {
346
+ return out
347
+ }
348
+
349
+ if (data[key] !== '' && data[key] !== undefined) {
350
+ out[key] = data[key]
351
+ }
352
+
353
+ return out
354
+ }, {} as Record)
355
+
356
+ transformFunctionArguments(document.node, Record, {
357
+ table,
358
+ data: properties,
359
+ })
360
+
361
+ return true
362
+ },
363
+ },
364
+ },
365
+ })
366
+
367
+ export function extractCallExpressionAsRecord<
368
+ const A extends unknown[],
369
+ const E extends A extends [infer T extends globalThis.Record<string, unknown>] ? T : never,
370
+ const T extends TableName,
371
+ >(
372
+ fn: (...args: A) => E,
373
+ asRecord: (entity: E) => Record<T>,
374
+ guid: (entity: E) => string,
375
+ node: CallExpression,
376
+ context: Context
377
+ ): ExtractionResult<EntityData<Record<T>>> {
378
+ const result = extractCallExpression(fn, 'record', node, context, guid)
379
+ if (!result.handled) {
380
+ return result
381
+ }
382
+
383
+ return {
384
+ handled: true,
385
+ diagnostics: result.diagnostics,
386
+ // data: result.data.map((d) => new EntityData(context, node, 'record', d.guid, asRecord(d.data))),
387
+ data: result.data.map(
388
+ (d) => new EntityData('record', d.getGuid(), ObjectData.fromObjectValue(asRecord(d.getValue()), node), node)
389
+ ),
390
+ }
391
+ }