@servicenow/sdk-build-plugins 4.7.1 → 4.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/alias/alias-plugin.d.ts +2 -0
- package/dist/alias/alias-plugin.js +183 -0
- package/dist/alias/alias-plugin.js.map +1 -0
- package/dist/alias/alias-template-plugin.d.ts +2 -0
- package/dist/alias/alias-template-plugin.js +232 -0
- package/dist/alias/alias-template-plugin.js.map +1 -0
- package/dist/alias/index.d.ts +3 -0
- package/dist/alias/index.js +20 -0
- package/dist/alias/index.js.map +1 -0
- package/dist/alias/retry-policy-plugin.d.ts +2 -0
- package/dist/alias/retry-policy-plugin.js +119 -0
- package/dist/alias/retry-policy-plugin.js.map +1 -0
- package/dist/arrow-function-plugin.d.ts +1 -0
- package/dist/arrow-function-plugin.js +60 -21
- package/dist/arrow-function-plugin.js.map +1 -1
- package/dist/atf/test-plugin.js +1 -1
- package/dist/atf/test-plugin.js.map +1 -1
- package/dist/basic-syntax-plugin.js +7 -7
- package/dist/basic-syntax-plugin.js.map +1 -1
- package/dist/column/index.d.ts +2 -0
- package/dist/column/index.js +13 -0
- package/dist/column/index.js.map +1 -0
- package/dist/dashboard/dashboard-plugin.js +4 -0
- package/dist/dashboard/dashboard-plugin.js.map +1 -1
- package/dist/data-lookup-plugin.d.ts +2 -0
- package/dist/data-lookup-plugin.js +159 -0
- package/dist/data-lookup-plugin.js.map +1 -0
- package/dist/flow/plugins/flow-action-definition-plugin.js +38 -2
- package/dist/flow/plugins/flow-action-definition-plugin.js.map +1 -1
- package/dist/flow/plugins/flow-instance-plugin.js +1 -1
- package/dist/flow/plugins/flow-instance-plugin.js.map +1 -1
- package/dist/flow/plugins/step-instance-plugin.js +1 -1
- package/dist/flow/plugins/step-instance-plugin.js.map +1 -1
- package/dist/flow/utils/flow-constants.d.ts +7 -0
- package/dist/flow/utils/flow-constants.js +6 -1
- package/dist/flow/utils/flow-constants.js.map +1 -1
- package/dist/flow/utils/flow-shapes.d.ts +1 -1
- package/dist/form-plugin.js +35 -24
- package/dist/form-plugin.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/now-attach-plugin.d.ts +1 -1
- package/dist/now-config-plugin.js +2 -1
- package/dist/now-config-plugin.js.map +1 -1
- package/dist/now-delete-plugin.d.ts +2 -0
- package/dist/now-delete-plugin.js +64 -0
- package/dist/now-delete-plugin.js.map +1 -0
- package/dist/record-plugin.d.ts +10 -0
- package/dist/record-plugin.js +15 -1
- package/dist/record-plugin.js.map +1 -1
- package/dist/repack/lint/Rules.js +17 -7
- package/dist/repack/lint/Rules.js.map +1 -1
- package/dist/rest-message-plugin.d.ts +2 -0
- package/dist/rest-message-plugin.js +331 -0
- package/dist/rest-message-plugin.js.map +1 -0
- package/dist/script-include-plugin.js +1 -1
- package/dist/script-include-plugin.js.map +1 -1
- package/dist/server-module-plugin/sbom-builder.js +17 -7
- package/dist/server-module-plugin/sbom-builder.js.map +1 -1
- package/dist/static-content-plugin.js +17 -7
- package/dist/static-content-plugin.js.map +1 -1
- package/dist/table-plugin.js +5 -2
- package/dist/table-plugin.js.map +1 -1
- package/package.json +6 -5
- package/src/alias/alias-plugin.ts +221 -0
- package/src/alias/alias-template-plugin.ts +271 -0
- package/src/alias/index.ts +3 -0
- package/src/alias/retry-policy-plugin.ts +138 -0
- package/src/arrow-function-plugin.ts +67 -23
- package/src/atf/test-plugin.ts +1 -1
- package/src/basic-syntax-plugin.ts +7 -7
- package/src/column/index.ts +7 -0
- package/src/dashboard/dashboard-plugin.ts +4 -0
- package/src/data-lookup-plugin.ts +191 -0
- package/src/flow/plugins/flow-action-definition-plugin.ts +49 -3
- package/src/flow/plugins/flow-instance-plugin.ts +2 -1
- package/src/flow/plugins/step-instance-plugin.ts +1 -1
- package/src/flow/utils/flow-constants.ts +8 -0
- package/src/form-plugin.ts +47 -26
- package/src/index.ts +4 -0
- package/src/now-config-plugin.ts +2 -1
- package/src/now-delete-plugin.ts +82 -0
- package/src/record-plugin.ts +17 -2
- package/src/rest-message-plugin.ts +391 -0
- package/src/script-include-plugin.ts +4 -1
- package/src/table-plugin.ts +7 -2
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { CallExpressionShape, Plugin, type Diagnostics, type Shape, type ObjectShape } from '@servicenow/sdk-build-core'
|
|
2
|
+
import { NowIdShape } from '../now-id-plugin'
|
|
3
|
+
|
|
4
|
+
const SYS_ALIAS = 'sys_alias'
|
|
5
|
+
|
|
6
|
+
// Maps developer-facing camelCase connection type labels to ServiceNow platform values
|
|
7
|
+
const CONNECTION_TYPE_MAP: Record<string, string> = {
|
|
8
|
+
httpConnection: 'http_connection',
|
|
9
|
+
jdbcConnection: 'jdbc_connection',
|
|
10
|
+
basicConnection: 'sys_connection',
|
|
11
|
+
jmsConnection: 'orch_jms_ds',
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Reverse map: platform values to camelCase labels (used in toShape)
|
|
15
|
+
const CONNECTION_TYPE_REVERSE_MAP: Record<string, string> = Object.fromEntries(
|
|
16
|
+
Object.entries(CONNECTION_TYPE_MAP).map(([label, platform]) => [platform, label])
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
// Default retry policy sys_ids by platform connection type
|
|
20
|
+
// Mirrors ServiceNow client scripts: "Default RetryPolicy onLoad new form" / "Default RetryPolicy onChange Conn-type"
|
|
21
|
+
const DEFAULT_RETRY_POLICY: Record<string, string> = {
|
|
22
|
+
http_connection: 'ef751ff07301330025d71afe2ff6a7f9', // Default HTTP Retry Policy
|
|
23
|
+
jdbc_connection: 'e8ddcb7573331010cbfec9d234f6a7fd', // Default JDBC Retry Policy
|
|
24
|
+
sys_connection: 'e385484773100010e3a71afe2ff6a709', // Default Basic Connection Retry Policy
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Hints when `connectionType` is set on a credential-only alias.
|
|
29
|
+
*
|
|
30
|
+
* Based on sys_alias UI policies:
|
|
31
|
+
* - "Show / hide connection type" — connectionType is only shown when type='connection'
|
|
32
|
+
*/
|
|
33
|
+
function hintConnectionTypeForCredential(
|
|
34
|
+
connectionTypeShape: Shape,
|
|
35
|
+
aliasType: string | undefined,
|
|
36
|
+
diagnostics: Diagnostics
|
|
37
|
+
): void {
|
|
38
|
+
if (aliasType === 'credential' && connectionTypeShape.isDefined()) {
|
|
39
|
+
diagnostics.warn(
|
|
40
|
+
connectionTypeShape,
|
|
41
|
+
`'connectionType' is ignored when type is 'credential'. It only applies to 'connection' type aliases.`
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Resolves a developer-facing camelCase connection type to the ServiceNow platform value.
|
|
48
|
+
* Returns the platform default ('http_connection') if the value is not provided.
|
|
49
|
+
*/
|
|
50
|
+
function resolveConnectionType(connectionTypeShape: Shape): string {
|
|
51
|
+
const value = connectionTypeShape.ifString()?.getValue()
|
|
52
|
+
if (!value) {
|
|
53
|
+
return 'http_connection'
|
|
54
|
+
}
|
|
55
|
+
return CONNECTION_TYPE_MAP[value] ?? 'http_connection'
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Computes the `id` field value for a sys_alias record.
|
|
60
|
+
* ServiceNow's virtual field calculation: scope + '.' + name (global scope omits prefix).
|
|
61
|
+
* Without this, the stored 'id' column is empty and Flow Designer's
|
|
62
|
+
* idISNOTEMPTY filter hides the alias from dropdowns.
|
|
63
|
+
*/
|
|
64
|
+
function computeAliasId(name: string, scope: string): string {
|
|
65
|
+
const sanitizedName = name.replace(/[^A-Za-z0-9_]/g, '_')
|
|
66
|
+
return scope !== 'global' ? `${scope}.${sanitizedName}` : sanitizedName
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Validates fields related to child aliases.
|
|
71
|
+
*
|
|
72
|
+
* Based on sys_alias UI policy "HideEmptyParentField" — the parent field is hidden
|
|
73
|
+
* in the UI when type='credential'.
|
|
74
|
+
*/
|
|
75
|
+
function validateChildAliasFields(arg: ObjectShape, diagnostics: Diagnostics): void {
|
|
76
|
+
const parent = arg.get('parent')
|
|
77
|
+
const type = arg.get('type')
|
|
78
|
+
|
|
79
|
+
// Warn if parent is set for credential-only alias — UI hides the field so it has no effect.
|
|
80
|
+
if (type?.isString() && type.getValue() === 'credential' && parent?.isDefined()) {
|
|
81
|
+
diagnostics.warn(
|
|
82
|
+
parent,
|
|
83
|
+
`'parent' field is not visible in the UI when type is 'credential'. ` +
|
|
84
|
+
`Parent aliases are only supported for 'connection' type aliases.`
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Validates fields related to 'connection' type alias.
|
|
91
|
+
*
|
|
92
|
+
* Based on sys_alias UI policies:
|
|
93
|
+
* - "Show/Hide Multiple connection flag" — multipleConnections is only shown when type='connection'
|
|
94
|
+
* - "Show / hide Default Retry Policy field" — retryPolicy is only shown when type='connection'
|
|
95
|
+
*/
|
|
96
|
+
function validateConnectionOnlyFields(arg: ObjectShape, aliasType: string | undefined, diagnostics: Diagnostics): void {
|
|
97
|
+
if (aliasType === 'credential') {
|
|
98
|
+
const multipleConnections = arg.get('multipleConnections')
|
|
99
|
+
if (multipleConnections?.isDefined()) {
|
|
100
|
+
diagnostics.warn(
|
|
101
|
+
multipleConnections,
|
|
102
|
+
`'multipleConnections' is ignored when type is 'credential'. It only applies to 'connection' type aliases.`
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const retryPolicy = arg.get('retryPolicy')
|
|
107
|
+
if (retryPolicy?.isDefined()) {
|
|
108
|
+
diagnostics.warn(
|
|
109
|
+
retryPolicy,
|
|
110
|
+
`'retryPolicy' is ignored when type is 'credential'. It only applies to 'connection' type aliases.`
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export const AliasPlugin = Plugin.create({
|
|
117
|
+
name: 'AliasPlugin',
|
|
118
|
+
records: {
|
|
119
|
+
[SYS_ALIAS]: {
|
|
120
|
+
toShape(record) {
|
|
121
|
+
// Resolve connection_type and alias type to determine default retry policy
|
|
122
|
+
const aliasType = record.get('type').toString().getValue() || 'connection'
|
|
123
|
+
const connType = record.get('connection_type').toString().getValue() || 'http_connection'
|
|
124
|
+
// For credential aliases, retry_policy is hidden in the UI and has no effect —
|
|
125
|
+
// treat whatever value is present as the default so it gets suppressed in Fluent output.
|
|
126
|
+
const retryPolicyValue = record.get('retry_policy').toString().getValue()
|
|
127
|
+
const defaultRetryPolicy =
|
|
128
|
+
aliasType === 'connection' ? (DEFAULT_RETRY_POLICY[connType] ?? '') : retryPolicyValue
|
|
129
|
+
const camelCaseConnType = CONNECTION_TYPE_REVERSE_MAP[connType] ?? 'httpConnection'
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
success: true,
|
|
133
|
+
value: new CallExpressionShape({
|
|
134
|
+
source: record,
|
|
135
|
+
callee: 'Alias',
|
|
136
|
+
args: [
|
|
137
|
+
record.transform(({ $ }) => ({
|
|
138
|
+
$id: $.val(NowIdShape.from(record)),
|
|
139
|
+
name: $,
|
|
140
|
+
type: $.def('connection'),
|
|
141
|
+
connectionType: $.from('connection_type').val(camelCaseConnType).def('httpConnection'),
|
|
142
|
+
description: $.def(''),
|
|
143
|
+
parent: $.def(''),
|
|
144
|
+
configurationTemplate: $.from('configuration_template').def(''),
|
|
145
|
+
retryPolicy: $.from('retry_policy').def(defaultRetryPolicy),
|
|
146
|
+
multipleConnections: $.from('multiple_connections').toBoolean().def(false),
|
|
147
|
+
protectionPolicy: $.from('sys_policy').def(''),
|
|
148
|
+
})),
|
|
149
|
+
],
|
|
150
|
+
}),
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
shapes: [
|
|
156
|
+
{
|
|
157
|
+
shape: CallExpressionShape,
|
|
158
|
+
fileTypes: ['fluent'],
|
|
159
|
+
async toRecord(callExpression, { factory, diagnostics, config }) {
|
|
160
|
+
if (callExpression.getCallee() !== 'Alias') {
|
|
161
|
+
return { success: false }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const argRaw = callExpression.getArgument(0)
|
|
165
|
+
if (!argRaw?.isObject()) {
|
|
166
|
+
return { success: false }
|
|
167
|
+
}
|
|
168
|
+
const arg = argRaw.asObject()
|
|
169
|
+
|
|
170
|
+
// Validate required 'name' property.
|
|
171
|
+
// TypeScript enforces `name: string`, but the empty-string check is semantic and
|
|
172
|
+
// cannot be expressed in the type system.
|
|
173
|
+
const name = arg.get('name')
|
|
174
|
+
const nameValue = name.ifString()?.getValue().trim() ?? ''
|
|
175
|
+
if (nameValue === '') {
|
|
176
|
+
diagnostics.error(name.isDefined() ? name : arg, `'name' is required and cannot be empty.`)
|
|
177
|
+
return { success: false }
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Resolve type and connectionType
|
|
181
|
+
const typeShape = arg.get('type')
|
|
182
|
+
const aliasType = typeShape.ifString()?.getValue() ?? 'connection'
|
|
183
|
+
|
|
184
|
+
const connectionTypeShape = arg.get('connectionType')
|
|
185
|
+
const resolvedConnectionType = resolveConnectionType(connectionTypeShape)
|
|
186
|
+
|
|
187
|
+
// Cross-field hints
|
|
188
|
+
hintConnectionTypeForCredential(connectionTypeShape, aliasType, diagnostics)
|
|
189
|
+
validateChildAliasFields(arg, diagnostics)
|
|
190
|
+
validateConnectionOnlyFields(arg, aliasType, diagnostics)
|
|
191
|
+
|
|
192
|
+
// Resolve default retry policy (only for connection type)
|
|
193
|
+
const defaultRetryPolicy =
|
|
194
|
+
aliasType === 'connection' ? (DEFAULT_RETRY_POLICY[resolvedConnectionType] ?? '') : ''
|
|
195
|
+
|
|
196
|
+
const aliasId = computeAliasId(nameValue, config.scope)
|
|
197
|
+
|
|
198
|
+
// Create the sys_alias record
|
|
199
|
+
const record = await factory.createRecord({
|
|
200
|
+
source: callExpression,
|
|
201
|
+
table: SYS_ALIAS,
|
|
202
|
+
explicitId: arg.get('$id'),
|
|
203
|
+
properties: arg.transform(({ $ }) => ({
|
|
204
|
+
name: $,
|
|
205
|
+
id: $.val(aliasId),
|
|
206
|
+
type: $.def('connection'),
|
|
207
|
+
connection_type: $.from('connectionType').val(resolvedConnectionType).def('http_connection'),
|
|
208
|
+
description: $.def(''),
|
|
209
|
+
parent: $,
|
|
210
|
+
configuration_template: $.from('configurationTemplate'),
|
|
211
|
+
retry_policy: $.from('retryPolicy').def(defaultRetryPolicy),
|
|
212
|
+
multiple_connections: $.from('multipleConnections').def(false),
|
|
213
|
+
sys_policy: $.from('protectionPolicy'),
|
|
214
|
+
})),
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
return { success: true, value: record }
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
],
|
|
221
|
+
})
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { CallExpressionShape, Plugin, type Shape, type StringShape } from '@servicenow/sdk-build-core'
|
|
2
|
+
import { NowIdShape } from '../now-id-plugin'
|
|
3
|
+
import { NowIncludeShape } from '../now-include-plugin'
|
|
4
|
+
import { ModuleFunctionShape } from '../server-module-plugin'
|
|
5
|
+
import { toReference } from '../utils'
|
|
6
|
+
|
|
7
|
+
const DEFAULT_POST_PROCESS_SCRIPT = `(function execute(aliasId, connectionSysId, jsonDefaultData, jsonDynamicData) {
|
|
8
|
+
// aliasId: sys_id of the alias record
|
|
9
|
+
// connectionSysId: sys_id of the connection record created by using the default and dynamic data
|
|
10
|
+
// jsonDefaultData (String): The default JSON template data from template table
|
|
11
|
+
// e.g. var jsonDefaultParsed = JSON.parse(jsonDefaultData);
|
|
12
|
+
// gs.info("default connection url = " + jsonDefaultParsed["connection"]["connection_url"]);
|
|
13
|
+
// jsonDynamicData (String): User input based on the key/values defined in "Dynamic Data"
|
|
14
|
+
// e.g. var jsonDynamicParsed = JSON.parse(jsonDynamicData);
|
|
15
|
+
// gs.info("dynamic connection url = " + jsonDynamicParsed["connection.connection_url"]);
|
|
16
|
+
})(aliasId, connectionSysId, jsonDefaultData, jsonDynamicData);
|
|
17
|
+
`
|
|
18
|
+
const DEFAULT_PRE_EDIT_SCRIPT = `(function execute(aliasId, connectionSysId, jsonDefaultData, jsonDynamicData) {
|
|
19
|
+
// aliasId: sys_id of the alias record.
|
|
20
|
+
// connectionSysId: sys_id of the connection record created by using the default and dynamic data.
|
|
21
|
+
// jsonDefaultData (String): The default JSON template data from template table.
|
|
22
|
+
// e.g. var jsonDefaultParsed = JSON.parse(jsonDefaultData);
|
|
23
|
+
// gs.info("default connection url = " + jsonDefaultParsed["connection"]["connection_url"]);
|
|
24
|
+
// jsonDynamicData (String): Current values of Connection and Credential fields (except Password fields) based on the key-value pairs defined in "Dynamic Data".
|
|
25
|
+
// e.g. var jsonDynamicParsed = JSON.parse(jsonDynamicData);
|
|
26
|
+
// gs.info("dynamic connection url = " + jsonDynamicParsed["connection.connection_url"]);
|
|
27
|
+
|
|
28
|
+
//returns array of objects. Each object has name-value pairs for populating additional fields values in connections dashboard in the edit view.
|
|
29
|
+
//In this example script, additionalField1, additionalField2 are additional fields.
|
|
30
|
+
//Here we define calcAdditionalField1(), calcAdditionalField2() functions in the script
|
|
31
|
+
//to calculate current values for additionalField1, additionalField2.
|
|
32
|
+
|
|
33
|
+
//return [{
|
|
34
|
+
// name: "additionalField1"
|
|
35
|
+
// value: calcAdditionalField1(),
|
|
36
|
+
// },
|
|
37
|
+
// {
|
|
38
|
+
// name: "additionalField2"
|
|
39
|
+
// value: calcAdditionalField2(),
|
|
40
|
+
// }
|
|
41
|
+
//];
|
|
42
|
+
|
|
43
|
+
return [];
|
|
44
|
+
})(aliasId, connectionSysId, jsonDefaultData, jsonDynamicData);
|
|
45
|
+
`
|
|
46
|
+
const DEFAULT_ON_EDIT_SCRIPT = `(function execute(aliasId, connectionSysId, jsonDefaultData, jsonDynamicData) {
|
|
47
|
+
// aliasId: sys_id of the alias record
|
|
48
|
+
// connectionSysId: sys_id of the connection record created by using the default and dynamic data
|
|
49
|
+
// jsonDefaultData (String): The default JSON template data from template table
|
|
50
|
+
// e.g. var jsonDefaultParsed = JSON.parse(jsonDefaultData);
|
|
51
|
+
// gs.info("default connection url = " + jsonDefaultParsed["connection"]["connection_url"]);
|
|
52
|
+
// jsonDynamicData (String): User input based on the key/values defined in "Dynamic Data"
|
|
53
|
+
// e.g. var jsonDynamicParsed = JSON.parse(jsonDynamicData);
|
|
54
|
+
// gs.info("dynamic connection url = " + jsonDynamicParsed["connection.connection_url"]);
|
|
55
|
+
})(aliasId, connectionSysId, jsonDefaultData, jsonDynamicData);
|
|
56
|
+
`
|
|
57
|
+
|
|
58
|
+
function camelToSnake(key: string): string {
|
|
59
|
+
return key.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function snakeToCamel(key: string): string {
|
|
63
|
+
return key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase())
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Only these keys differ between DB (snake_case) and Fluent API (camelCase).
|
|
67
|
+
// All other keys — including arbitrary user-defined values in open [key: string]: unknown
|
|
68
|
+
// sections — are left unchanged to avoid corrupting non-SDK field names.
|
|
69
|
+
const DB_SNAKE_KEYS = new Set([
|
|
70
|
+
'connection_fields',
|
|
71
|
+
'credential_fields',
|
|
72
|
+
'additional_fields',
|
|
73
|
+
'use_mid',
|
|
74
|
+
'connection_url',
|
|
75
|
+
'default_group',
|
|
76
|
+
])
|
|
77
|
+
const FLUENT_CAMEL_KEYS = new Set([
|
|
78
|
+
'connectionFields',
|
|
79
|
+
'credentialFields',
|
|
80
|
+
'additionalFields',
|
|
81
|
+
'useMid',
|
|
82
|
+
'connectionUrl',
|
|
83
|
+
'defaultGroup',
|
|
84
|
+
])
|
|
85
|
+
|
|
86
|
+
function convertKeys(obj: unknown, converter: (key: string) => string, definedKeys: Set<string>): unknown {
|
|
87
|
+
if (obj === null || typeof obj !== 'object') {
|
|
88
|
+
return obj
|
|
89
|
+
}
|
|
90
|
+
if (Array.isArray(obj)) {
|
|
91
|
+
return obj.map((item) => convertKeys(item, converter, definedKeys))
|
|
92
|
+
}
|
|
93
|
+
const result: Record<string, unknown> = {}
|
|
94
|
+
for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
|
|
95
|
+
const newKey = definedKeys.has(key) ? converter(key) : key
|
|
96
|
+
result[newKey] = convertKeys(value, converter, definedKeys)
|
|
97
|
+
}
|
|
98
|
+
return result
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Parse JSON string from DB and convert snake_case keys to camelCase (DB → Fluent)
|
|
103
|
+
* Used in toShape for dynamicDataSchema and defaultDataTemplate
|
|
104
|
+
*/
|
|
105
|
+
function parseDbJson(v: StringShape | undefined): unknown {
|
|
106
|
+
const parsed = v?.ifNotEmpty()?.parseJson()
|
|
107
|
+
if (!parsed) {
|
|
108
|
+
return undefined
|
|
109
|
+
}
|
|
110
|
+
const db = parsed.getValue()
|
|
111
|
+
if (db === undefined || db === null) {
|
|
112
|
+
return undefined
|
|
113
|
+
}
|
|
114
|
+
return convertKeys(db, snakeToCamel, DB_SNAKE_KEYS)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Convert camelCase keys to snake_case and stringify to JSON (Fluent → DB)
|
|
119
|
+
* Used in toRecord for dynamicDataSchema and defaultDataTemplate
|
|
120
|
+
*/
|
|
121
|
+
function serializeFluentJson(v: unknown): string | undefined {
|
|
122
|
+
if (v === undefined || v === null) {
|
|
123
|
+
return undefined
|
|
124
|
+
}
|
|
125
|
+
return JSON.stringify(convertKeys(v, camelToSnake, FLUENT_CAMEL_KEYS), null, 2)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Helper function to handle ModuleFunctionShape transformation for alias template scripts
|
|
130
|
+
* Converts module function references to function call strings with proper parameters
|
|
131
|
+
*/
|
|
132
|
+
function mapScriptWithModuleSupport(v: Shape) {
|
|
133
|
+
return (
|
|
134
|
+
v
|
|
135
|
+
.if(ModuleFunctionShape)
|
|
136
|
+
?.toString(
|
|
137
|
+
(n: string) => `${n}({{PARAMS}})`,
|
|
138
|
+
['aliasId', 'connectionSysId', 'jsonDefaultData', 'jsonDynamicData']
|
|
139
|
+
) ?? v
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export const AliasTemplatePlugin = Plugin.create({
|
|
144
|
+
name: 'AliasTemplatePlugin',
|
|
145
|
+
records: {
|
|
146
|
+
sys_alias_templates: {
|
|
147
|
+
async toShape(record, { transform }) {
|
|
148
|
+
// Only call NowIncludeShape.fromRecord for non-empty, non-default script fields.
|
|
149
|
+
// Scripts that match the default are omitted so round-trips stay clean.
|
|
150
|
+
const preEditScriptField = record.get('pre_edit_script').ifString()?.ifNotEmpty()
|
|
151
|
+
const onEditScriptField = record.get('on_edit_script').ifString()?.ifNotEmpty()
|
|
152
|
+
const postProcessScriptField = record.get('post_process_script').ifString()?.ifNotEmpty()
|
|
153
|
+
|
|
154
|
+
const preEditScript =
|
|
155
|
+
preEditScriptField && preEditScriptField.getValue().trim() !== DEFAULT_PRE_EDIT_SCRIPT.trim()
|
|
156
|
+
? await NowIncludeShape.fromRecord(record, preEditScriptField, transform)
|
|
157
|
+
: undefined
|
|
158
|
+
const onEditScript =
|
|
159
|
+
onEditScriptField && onEditScriptField.getValue().trim() !== DEFAULT_ON_EDIT_SCRIPT.trim()
|
|
160
|
+
? await NowIncludeShape.fromRecord(record, onEditScriptField, transform)
|
|
161
|
+
: undefined
|
|
162
|
+
const postProcessScript =
|
|
163
|
+
postProcessScriptField &&
|
|
164
|
+
postProcessScriptField.getValue().trim() !== DEFAULT_POST_PROCESS_SCRIPT.trim()
|
|
165
|
+
? await NowIncludeShape.fromRecord(record, postProcessScriptField, transform)
|
|
166
|
+
: undefined
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
success: true,
|
|
170
|
+
value: new CallExpressionShape({
|
|
171
|
+
source: record,
|
|
172
|
+
callee: 'AliasTemplate',
|
|
173
|
+
args: [
|
|
174
|
+
record.transform(({ $ }) => ({
|
|
175
|
+
$id: $.val(NowIdShape.from(record)),
|
|
176
|
+
name: $,
|
|
177
|
+
|
|
178
|
+
// JSON strings stored in DB → typed objects in Fluent code
|
|
179
|
+
// Only defined SDK keys are converted from snake_case to camelCase
|
|
180
|
+
dynamicDataSchema: $.from('dynamic_data_schema').map((v) => parseDbJson(v.ifString())),
|
|
181
|
+
defaultDataTemplate: $.from('default_data_template').map((v) =>
|
|
182
|
+
parseDbJson(v.ifString())
|
|
183
|
+
),
|
|
184
|
+
|
|
185
|
+
// Script fields via NowIncludeShape for IDE file support
|
|
186
|
+
preEditScript: $.val(preEditScript),
|
|
187
|
+
onEditScript: $.val(onEditScript),
|
|
188
|
+
postProcessScript: $.val(postProcessScript),
|
|
189
|
+
|
|
190
|
+
// Reference field — omit when empty
|
|
191
|
+
testAction: $.from('test_action').def(''),
|
|
192
|
+
})),
|
|
193
|
+
],
|
|
194
|
+
}),
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
shapes: [
|
|
200
|
+
{
|
|
201
|
+
shape: CallExpressionShape,
|
|
202
|
+
fileTypes: ['fluent'],
|
|
203
|
+
async toRecord(callExpression, { factory, config, diagnostics }) {
|
|
204
|
+
if (callExpression.getCallee() !== 'AliasTemplate') {
|
|
205
|
+
return { success: false }
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const arg = callExpression.getArgument(0).asObject()
|
|
209
|
+
|
|
210
|
+
// Check for unresolved script references
|
|
211
|
+
if (arg.get('preEditScript').isUnresolved()) {
|
|
212
|
+
diagnostics.error(
|
|
213
|
+
arg.get('preEditScript').getOriginalNode(),
|
|
214
|
+
`Unable to resolve the preEditScript reference, ensure the imported module is within the ${config.serverModulesDir} directory.`
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
if (arg.get('onEditScript').isUnresolved()) {
|
|
218
|
+
diagnostics.error(
|
|
219
|
+
arg.get('onEditScript').getOriginalNode(),
|
|
220
|
+
`Unable to resolve the onEditScript reference, ensure the imported module is within the ${config.serverModulesDir} directory.`
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
if (arg.get('postProcessScript').isUnresolved()) {
|
|
224
|
+
diagnostics.error(
|
|
225
|
+
arg.get('postProcessScript').getOriginalNode(),
|
|
226
|
+
`Unable to resolve the postProcessScript reference, ensure the imported module is within the ${config.serverModulesDir} directory.`
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
success: true,
|
|
232
|
+
value: await factory.createRecord({
|
|
233
|
+
source: callExpression,
|
|
234
|
+
table: 'sys_alias_templates',
|
|
235
|
+
explicitId: arg.get('$id'),
|
|
236
|
+
properties: arg.transform(({ $ }) => ({
|
|
237
|
+
name: $,
|
|
238
|
+
|
|
239
|
+
// Typed objects in Fluent code → JSON strings for DB
|
|
240
|
+
// Only defined SDK keys are converted from camelCase to snake_case
|
|
241
|
+
dynamic_data_schema: $.from('dynamicDataSchema').map((v) =>
|
|
242
|
+
serializeFluentJson(v.ifDefined() ? v.getValue() : undefined)
|
|
243
|
+
),
|
|
244
|
+
default_data_template: $.from('defaultDataTemplate').map((v) =>
|
|
245
|
+
serializeFluentJson(v.ifDefined() ? v.getValue() : undefined)
|
|
246
|
+
),
|
|
247
|
+
|
|
248
|
+
// Script fields — CDATA wraps both inline strings and Now.include() content
|
|
249
|
+
// Support for app_modules via ModuleFunctionShape
|
|
250
|
+
pre_edit_script: $.from('preEditScript')
|
|
251
|
+
.map(mapScriptWithModuleSupport)
|
|
252
|
+
.toCdata()
|
|
253
|
+
.def(DEFAULT_PRE_EDIT_SCRIPT),
|
|
254
|
+
on_edit_script: $.from('onEditScript')
|
|
255
|
+
.map(mapScriptWithModuleSupport)
|
|
256
|
+
.toCdata()
|
|
257
|
+
.def(DEFAULT_ON_EDIT_SCRIPT),
|
|
258
|
+
post_process_script: $.from('postProcessScript')
|
|
259
|
+
.map(mapScriptWithModuleSupport)
|
|
260
|
+
.toCdata()
|
|
261
|
+
.def(DEFAULT_POST_PROCESS_SCRIPT),
|
|
262
|
+
|
|
263
|
+
// Reference field — extract sys_id from Record ref or plain string
|
|
264
|
+
test_action: $.from('testAction').map((v) => toReference(v)),
|
|
265
|
+
})),
|
|
266
|
+
}),
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
],
|
|
271
|
+
})
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { CallExpressionShape, Plugin } from '@servicenow/sdk-build-core'
|
|
2
|
+
import { NowIdShape } from '../now-id-plugin'
|
|
3
|
+
|
|
4
|
+
const SYS_RETRY_POLICY = 'sys_retry_policy'
|
|
5
|
+
|
|
6
|
+
const DEFAULT_CONNECTION_TYPE = 'http_retry_conditions'
|
|
7
|
+
const DEFAULT_RETRY_STRATEGY = 'fixed_time_interval'
|
|
8
|
+
const MAX_ELAPSED_TIME_SECONDS = 86400
|
|
9
|
+
const DEFAULT_RESTRICT_TO = 'http_method,status_code,error,response_body,response_headers'
|
|
10
|
+
|
|
11
|
+
export const RetryPolicyPlugin = Plugin.create({
|
|
12
|
+
name: 'RetryPolicyPlugin',
|
|
13
|
+
|
|
14
|
+
records: {
|
|
15
|
+
[SYS_RETRY_POLICY]: {
|
|
16
|
+
async toShape(record) {
|
|
17
|
+
const restrictToRaw = record.get('restrict_to')?.ifString()?.getValue()
|
|
18
|
+
const restrictToArray =
|
|
19
|
+
restrictToRaw === undefined || restrictToRaw === DEFAULT_RESTRICT_TO
|
|
20
|
+
? undefined
|
|
21
|
+
: restrictToRaw === ''
|
|
22
|
+
? []
|
|
23
|
+
: restrictToRaw
|
|
24
|
+
.split(',')
|
|
25
|
+
.map((s) => s.trim())
|
|
26
|
+
.filter(Boolean)
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
success: true,
|
|
30
|
+
value: new CallExpressionShape({
|
|
31
|
+
source: record,
|
|
32
|
+
callee: 'RetryPolicy',
|
|
33
|
+
args: [
|
|
34
|
+
record.transform(({ $ }) => ({
|
|
35
|
+
$id: $.val(NowIdShape.from(record)),
|
|
36
|
+
name: $.def(''),
|
|
37
|
+
connectionType: $.from('connection_type').def(DEFAULT_CONNECTION_TYPE),
|
|
38
|
+
retryStrategy: $.from('retry_strategy').def(DEFAULT_RETRY_STRATEGY),
|
|
39
|
+
count: $.map((v) => v.ifString()?.ifNotEmpty()?.toNumber()),
|
|
40
|
+
interval: $.map((v) => v.ifString()?.ifNotEmpty()?.toNumber()),
|
|
41
|
+
maxElapsedTime: $.from('max_elapsed_time').map((v) =>
|
|
42
|
+
v.ifString()?.ifNotEmpty()?.toNumber()
|
|
43
|
+
),
|
|
44
|
+
condition: $.def(''),
|
|
45
|
+
restrictTo: $.val(restrictToArray),
|
|
46
|
+
protectionPolicy: $.from('sys_policy').def(''),
|
|
47
|
+
})),
|
|
48
|
+
],
|
|
49
|
+
}),
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
shapes: [
|
|
56
|
+
{
|
|
57
|
+
shape: CallExpressionShape,
|
|
58
|
+
fileTypes: ['fluent'],
|
|
59
|
+
async toRecord(callExpression, { factory, diagnostics }) {
|
|
60
|
+
if (callExpression.getCallee() !== 'RetryPolicy') {
|
|
61
|
+
return { success: false }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const arg = callExpression.getArgument(0).asObject()
|
|
65
|
+
|
|
66
|
+
// Runtime validation for maxElapsedTime upper bound (TypeScript cannot enforce numeric ranges)
|
|
67
|
+
const maxElapsedTimeShape = arg.get('maxElapsedTime')
|
|
68
|
+
const maxElapsedTime = maxElapsedTimeShape.ifNumber()?.getValue()
|
|
69
|
+
if (maxElapsedTime !== undefined && maxElapsedTime > MAX_ELAPSED_TIME_SECONDS) {
|
|
70
|
+
diagnostics.error(
|
|
71
|
+
maxElapsedTimeShape,
|
|
72
|
+
`maxElapsedTime must not exceed ${MAX_ELAPSED_TIME_SECONDS} seconds (24 hours). Received: ${maxElapsedTime}.`
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const countShape = arg.get('count')
|
|
77
|
+
const count = countShape.ifNumber()?.getValue()
|
|
78
|
+
if (count !== undefined && !Number.isInteger(count)) {
|
|
79
|
+
diagnostics.error(countShape, `count must be an integer. Received: ${count}.`)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const intervalShape = arg.get('interval')
|
|
83
|
+
const interval = intervalShape.ifNumber()?.getValue()
|
|
84
|
+
if (interval !== undefined && !Number.isInteger(interval)) {
|
|
85
|
+
diagnostics.error(intervalShape, `interval must be an integer. Received: ${interval}.`)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Convert restrictTo string[] → comma-separated string for the DB
|
|
89
|
+
const restrictToShape = arg.get('restrictTo')
|
|
90
|
+
const restrictToArrayShape = restrictToShape.ifArray()
|
|
91
|
+
const restrictToCsv = restrictToArrayShape
|
|
92
|
+
? restrictToArrayShape
|
|
93
|
+
.getElements()
|
|
94
|
+
.map((el) => el.ifString()?.getValue() ?? '')
|
|
95
|
+
.filter(Boolean)
|
|
96
|
+
.join(',')
|
|
97
|
+
: DEFAULT_RESTRICT_TO
|
|
98
|
+
|
|
99
|
+
// Validate condition only references fields in restrictTo
|
|
100
|
+
const allowedFields = (restrictToCsv || DEFAULT_RESTRICT_TO).split(',')
|
|
101
|
+
const conditionShape = arg.get('condition')
|
|
102
|
+
const conditionValue = conditionShape.ifString()?.ifNotEmpty()?.getValue()
|
|
103
|
+
if (conditionValue) {
|
|
104
|
+
for (const part of conditionValue.split('^')) {
|
|
105
|
+
const fieldPart = part.startsWith('OR') ? part.slice(2) : part
|
|
106
|
+
if (!allowedFields.some((field) => fieldPart.startsWith(field))) {
|
|
107
|
+
const fieldName = fieldPart.match(/^[a-z_]+/)?.[0] ?? fieldPart
|
|
108
|
+
diagnostics.error(
|
|
109
|
+
conditionShape,
|
|
110
|
+
`Condition references field '${fieldName}' which is not in restrictTo. Allowed fields: ${allowedFields.join(', ')}.`
|
|
111
|
+
)
|
|
112
|
+
break
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const record = await factory.createRecord({
|
|
118
|
+
source: callExpression,
|
|
119
|
+
table: SYS_RETRY_POLICY,
|
|
120
|
+
explicitId: arg.get('$id'),
|
|
121
|
+
properties: arg.transform(({ $ }) => ({
|
|
122
|
+
name: $.def(''),
|
|
123
|
+
connection_type: $.from('connectionType').def(DEFAULT_CONNECTION_TYPE),
|
|
124
|
+
retry_strategy: $.from('retryStrategy').def(DEFAULT_RETRY_STRATEGY),
|
|
125
|
+
count: $,
|
|
126
|
+
interval: $,
|
|
127
|
+
max_elapsed_time: $.from('maxElapsedTime'),
|
|
128
|
+
condition: $.def(''),
|
|
129
|
+
restrict_to: $.val(restrictToCsv),
|
|
130
|
+
sys_policy: $.from('protectionPolicy').def(''),
|
|
131
|
+
})),
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
return { success: true, value: record }
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
})
|