@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.
- package/dist/AttachmentPlugin.d.ts +253 -0
- package/dist/AttachmentPlugin.js +216 -0
- package/dist/AttachmentPlugin.js.map +1 -0
- package/dist/BusinessRulePlugin.d.ts +56 -0
- package/dist/BusinessRulePlugin.js +171 -0
- package/dist/BusinessRulePlugin.js.map +1 -0
- package/dist/CrossScopePrivilegePlugin.d.ts +22 -0
- package/dist/CrossScopePrivilegePlugin.js +42 -0
- package/dist/CrossScopePrivilegePlugin.js.map +1 -0
- package/dist/DefaultPlugin.d.ts +71 -0
- package/dist/DefaultPlugin.js +238 -0
- package/dist/DefaultPlugin.js.map +1 -0
- package/dist/IdPlugin.d.ts +17 -0
- package/dist/IdPlugin.js +45 -0
- package/dist/IdPlugin.js.map +1 -0
- package/dist/ListPlugin.d.ts +91 -0
- package/dist/ListPlugin.js +398 -0
- package/dist/ListPlugin.js.map +1 -0
- package/dist/PropertyPlugin.d.ts +122 -0
- package/dist/PropertyPlugin.js +165 -0
- package/dist/PropertyPlugin.js.map +1 -0
- package/dist/ScriptTemplatePlugin.d.ts +31 -0
- package/dist/ScriptTemplatePlugin.js +208 -0
- package/dist/ScriptTemplatePlugin.js.map +1 -0
- package/dist/UserPreferencePlugin.d.ts +16 -0
- package/dist/UserPreferencePlugin.js +30 -0
- package/dist/UserPreferencePlugin.js.map +1 -0
- package/dist/aclAndRole/AclPlugin.d.ts +117 -0
- package/dist/aclAndRole/AclPlugin.js +285 -0
- package/dist/aclAndRole/AclPlugin.js.map +1 -0
- package/dist/aclAndRole/RolePlugin.d.ts +58 -0
- package/dist/aclAndRole/RolePlugin.js +152 -0
- package/dist/aclAndRole/RolePlugin.js.map +1 -0
- package/dist/aclAndRole/Util.d.ts +3 -0
- package/dist/aclAndRole/Util.js +106 -0
- package/dist/aclAndRole/Util.js.map +1 -0
- package/dist/app/ApplicationMenuPlugin.d.ts +32 -0
- package/dist/app/ApplicationMenuPlugin.js +106 -0
- package/dist/app/ApplicationMenuPlugin.js.map +1 -0
- package/dist/atf/ATFComposer.d.ts +492 -0
- package/dist/atf/ATFComposer.js +2717 -0
- package/dist/atf/ATFComposer.js.map +1 -0
- package/dist/atf/TestPlugin.d.ts +31 -0
- package/dist/atf/TestPlugin.js +95 -0
- package/dist/atf/TestPlugin.js.map +1 -0
- package/dist/atf/index.d.ts +1 -0
- package/dist/atf/index.js +9 -0
- package/dist/atf/index.js.map +1 -0
- package/dist/db/ColumnPlugins.d.ts +278 -0
- package/dist/db/ColumnPlugins.js +112 -0
- package/dist/db/ColumnPlugins.js.map +1 -0
- package/dist/db/RecordPlugin.d.ts +208 -0
- package/dist/db/RecordPlugin.js +287 -0
- package/dist/db/RecordPlugin.js.map +1 -0
- package/dist/db/TablePlugin.d.ts +742 -0
- package/dist/db/TablePlugin.js +1249 -0
- package/dist/db/TablePlugin.js.map +1 -0
- package/dist/db/index.d.ts +3 -0
- package/dist/db/index.js +27 -0
- package/dist/db/index.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +51 -0
- package/dist/index.js.map +1 -0
- package/dist/scriptedRESTAPI/RESTDeserializationUtils.d.ts +12 -0
- package/dist/scriptedRESTAPI/RESTDeserializationUtils.js +371 -0
- package/dist/scriptedRESTAPI/RESTDeserializationUtils.js.map +1 -0
- package/dist/scriptedRESTAPI/RESTSerializationUtils.d.ts +15 -0
- package/dist/scriptedRESTAPI/RESTSerializationUtils.js +177 -0
- package/dist/scriptedRESTAPI/RESTSerializationUtils.js.map +1 -0
- package/dist/scriptedRESTAPI/RestApiPlugin.d.ts +144 -0
- package/dist/scriptedRESTAPI/RestApiPlugin.js +318 -0
- package/dist/scriptedRESTAPI/RestApiPlugin.js.map +1 -0
- package/dist/scriptedRESTAPI/RestSchemaUtils.d.ts +190 -0
- package/dist/scriptedRESTAPI/RestSchemaUtils.js +53 -0
- package/dist/scriptedRESTAPI/RestSchemaUtils.js.map +1 -0
- package/dist/scriptedRESTAPI/RestUtils.d.ts +75 -0
- package/dist/scriptedRESTAPI/RestUtils.js +469 -0
- package/dist/scriptedRESTAPI/RestUtils.js.map +1 -0
- package/dist/scripts/ClientScriptPlugin.d.ts +43 -0
- package/dist/scripts/ClientScriptPlugin.js +190 -0
- package/dist/scripts/ClientScriptPlugin.js.map +1 -0
- package/dist/scripts/scriptUtils.d.ts +15 -0
- package/dist/scripts/scriptUtils.js +83 -0
- package/dist/scripts/scriptUtils.js.map +1 -0
- package/dist/uxf/ExperiencePlugin.d.ts +22 -0
- package/dist/uxf/ExperiencePlugin.js +55 -0
- package/dist/uxf/ExperiencePlugin.js.map +1 -0
- package/dist/uxf/RoutesPlugin.d.ts +22 -0
- package/dist/uxf/RoutesPlugin.js +176 -0
- package/dist/uxf/RoutesPlugin.js.map +1 -0
- package/dist/uxf/UxfFormulaParser/cleanUxValue.d.ts +4 -0
- package/dist/uxf/UxfFormulaParser/cleanUxValue.js +65 -0
- package/dist/uxf/UxfFormulaParser/cleanUxValue.js.map +1 -0
- package/dist/uxf/UxfFormulaParser/grammerParser/api.d.ts +189 -0
- package/dist/uxf/UxfFormulaParser/grammerParser/api.js +158 -0
- package/dist/uxf/UxfFormulaParser/grammerParser/api.js.map +1 -0
- package/dist/uxf/UxfFormulaParser/grammerParser/clientTransformMap.d.ts +13 -0
- package/dist/uxf/UxfFormulaParser/grammerParser/clientTransformMap.js +604 -0
- package/dist/uxf/UxfFormulaParser/grammerParser/clientTransformMap.js.map +1 -0
- package/dist/uxf/UxfFormulaParser/grammerParser/grammarParser.d.ts +12 -0
- package/dist/uxf/UxfFormulaParser/grammerParser/grammarParser.js +551 -0
- package/dist/uxf/UxfFormulaParser/grammerParser/grammarParser.js.map +1 -0
- package/dist/uxf/UxfFormulaParser/grammerParser/spanHelpers.d.ts +31 -0
- package/dist/uxf/UxfFormulaParser/grammerParser/spanHelpers.js +64 -0
- package/dist/uxf/UxfFormulaParser/grammerParser/spanHelpers.js.map +1 -0
- package/dist/uxf/UxfFormulaParser/index.d.ts +3 -0
- package/dist/uxf/UxfFormulaParser/index.js +11 -0
- package/dist/uxf/UxfFormulaParser/index.js.map +1 -0
- package/dist/uxf/UxfFormulaParser/parser.d.ts +8 -0
- package/dist/uxf/UxfFormulaParser/parser.js +87 -0
- package/dist/uxf/UxfFormulaParser/parser.js.map +1 -0
- package/dist/uxf/UxfFormulaParser/utils/getErrorMsg.d.ts +8 -0
- package/dist/uxf/UxfFormulaParser/utils/getErrorMsg.js +17 -0
- package/dist/uxf/UxfFormulaParser/utils/getErrorMsg.js.map +1 -0
- package/dist/uxf/constants.d.ts +2 -0
- package/dist/uxf/constants.js +8 -0
- package/dist/uxf/constants.js.map +1 -0
- package/dist/uxf/index.d.ts +2 -0
- package/dist/uxf/index.js +11 -0
- package/dist/uxf/index.js.map +1 -0
- package/dist/uxf/tectonicIdGenerator.d.ts +12 -0
- package/dist/uxf/tectonicIdGenerator.js +102 -0
- package/dist/uxf/tectonicIdGenerator.js.map +1 -0
- package/license +9 -0
- package/package.json +42 -0
- package/src/AttachmentPlugin.ts +262 -0
- package/src/BusinessRulePlugin.ts +251 -0
- package/src/CrossScopePrivilegePlugin.ts +54 -0
- package/src/DefaultPlugin.ts +272 -0
- package/src/IdPlugin.ts +47 -0
- package/src/ListPlugin.ts +497 -0
- package/src/PropertyPlugin.ts +218 -0
- package/src/ScriptTemplatePlugin.ts +223 -0
- package/src/UserPreferencePlugin.ts +36 -0
- package/src/aclAndRole/AclPlugin.ts +410 -0
- package/src/aclAndRole/RolePlugin.ts +225 -0
- package/src/aclAndRole/Util.ts +104 -0
- package/src/app/ApplicationMenuPlugin.ts +158 -0
- package/src/atf/ATFComposer.ts +3356 -0
- package/src/atf/TestPlugin.ts +119 -0
- package/src/atf/index.ts +1 -0
- package/src/db/ColumnPlugins.ts +117 -0
- package/src/db/RecordPlugin.ts +391 -0
- package/src/db/TablePlugin.ts +1581 -0
- package/src/db/index.ts +3 -0
- package/src/index.ts +16 -0
- package/src/scriptedRESTAPI/RESTDeserializationUtils.ts +410 -0
- package/src/scriptedRESTAPI/RESTSerializationUtils.ts +227 -0
- package/src/scriptedRESTAPI/RestApiPlugin.ts +438 -0
- package/src/scriptedRESTAPI/RestSchemaUtils.ts +72 -0
- package/src/scriptedRESTAPI/RestUtils.ts +507 -0
- package/src/scripts/ClientScriptPlugin.ts +251 -0
- package/src/scripts/scriptUtils.ts +81 -0
- package/src/uxf/ExperiencePlugin.ts +64 -0
- package/src/uxf/RoutesPlugin.ts +215 -0
- package/src/uxf/UxfFormulaParser/cleanUxValue.ts +73 -0
- package/src/uxf/UxfFormulaParser/grammerParser/api.js +166 -0
- package/src/uxf/UxfFormulaParser/grammerParser/clientTransformMap.js +606 -0
- package/src/uxf/UxfFormulaParser/grammerParser/grammarParser.js +551 -0
- package/src/uxf/UxfFormulaParser/grammerParser/spanHelpers.js +65 -0
- package/src/uxf/UxfFormulaParser/index.ts +4 -0
- package/src/uxf/UxfFormulaParser/parser.ts +64 -0
- package/src/uxf/UxfFormulaParser/utils/getErrorMsg.ts +13 -0
- package/src/uxf/constants.ts +4 -0
- package/src/uxf/index.ts +2 -0
- 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
|
+
})
|
package/src/atf/index.ts
ADDED
|
@@ -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
|
+
}
|