isc-transforms-mcp 1.0.17 → 1.0.19
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.
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "sailpoint.isc.transforms.replace.schema.json",
|
|
4
4
|
"title": "SailPoint ISC Transform Schema - replace",
|
|
5
|
-
"description": "Strict schema
|
|
5
|
+
"description": "Strict schema for the SailPoint ISC replace transform. Finds all occurrences of a single Java regular expression pattern in the input string and replaces every match with the replacement string. Unlike replaceAll, this operates on a single regex+replacement pair. Key behaviors: (1) ALL occurrences are replaced, not just the first. (2) Use an empty replacement string to delete all matched text. (3) Capture groups in the regex can be referenced as $1, $2, etc. in the replacement. (4) Use bracket notation for literal special characters (e.g., '[.]' for a literal dot). (5) input is optional — if omitted the transform uses the source+attribute configured in the identity profile UI.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"additionalProperties": false,
|
|
8
8
|
"required": [
|
|
@@ -12,16 +12,18 @@
|
|
|
12
12
|
],
|
|
13
13
|
"properties": {
|
|
14
14
|
"type": {
|
|
15
|
-
"const": "replace"
|
|
15
|
+
"const": "replace",
|
|
16
|
+
"description": "Transform operation type. Must be exactly 'replace'."
|
|
16
17
|
},
|
|
17
18
|
"name": {
|
|
18
19
|
"type": "string",
|
|
19
|
-
"minLength": 1
|
|
20
|
+
"minLength": 1,
|
|
21
|
+
"description": "Display name for this transform, shown in UI dropdowns and identity profile mappings."
|
|
20
22
|
},
|
|
21
23
|
"requiresPeriodicRefresh": {
|
|
22
24
|
"type": "boolean",
|
|
23
25
|
"default": false,
|
|
24
|
-
"description": "
|
|
26
|
+
"description": "If true, re-evaluates this transform during the nightly identity refresh cycle. Default is false."
|
|
25
27
|
},
|
|
26
28
|
"attributes": {
|
|
27
29
|
"type": "object",
|
|
@@ -30,19 +32,24 @@
|
|
|
30
32
|
"regex",
|
|
31
33
|
"replacement"
|
|
32
34
|
],
|
|
35
|
+
"description": "Required container for replace operation attributes.",
|
|
33
36
|
"properties": {
|
|
34
37
|
"regex": {
|
|
35
38
|
"type": "string",
|
|
36
39
|
"minLength": 1,
|
|
37
|
-
"description": "
|
|
40
|
+
"description": "Required. The Java regular expression pattern to search for. ALL occurrences of the pattern in the input are replaced — not just the first match. Pattern syntax notes: (1) Standard Java regex — character classes, quantifiers, anchors, and capture groups are all supported. (2) Use bracket notation to match literal special regex characters: '[.]' for a literal dot, '[+]' for a literal plus, '[-]' for a literal hyphen. (3) Capture groups with parentheses (e.g., '([A-Za-z]+)') can be back-referenced in the replacement field as $1, $2, etc. (4) The pattern is case-sensitive by default — use character classes (e.g., '[Aa]') or inline flag '(?i)' to match case-insensitively."
|
|
38
41
|
},
|
|
39
42
|
"replacement": {
|
|
40
43
|
"type": "string",
|
|
41
|
-
"description": "Replacement string."
|
|
44
|
+
"description": "Required. The string that replaces every occurrence of the matched pattern. Replacement notes: (1) Use an empty string \"\" to delete all matched text. (2) Use $0 to insert the entire matched text, $1 for the first capture group, $2 for the second, etc. (3) To include a literal dollar sign, use \\$."
|
|
42
45
|
},
|
|
43
46
|
"input": {
|
|
44
|
-
"description": "Optional explicit input
|
|
45
|
-
"
|
|
47
|
+
"description": "Optional explicit input providing the string to apply the regex replacement to. If omitted, the transform uses the source and attribute combination configured in the identity profile UI. When provided, must be a nested transform object or a static string.",
|
|
48
|
+
"anyOf": [
|
|
49
|
+
{
|
|
50
|
+
"type": "string",
|
|
51
|
+
"description": "Static string value to apply the regex replacement to."
|
|
52
|
+
},
|
|
46
53
|
{
|
|
47
54
|
"$ref": "#/$defs/NestedTransform"
|
|
48
55
|
}
|
|
@@ -54,7 +61,7 @@
|
|
|
54
61
|
"$defs": {
|
|
55
62
|
"NestedTransform": {
|
|
56
63
|
"type": "object",
|
|
57
|
-
"description": "
|
|
64
|
+
"description": "A nested transform object that provides the string input for the replace operation. Its output string is the value the regex replacement is applied to. The 'name' field is optional in nested transforms per SailPoint docs.",
|
|
58
65
|
"additionalProperties": false,
|
|
59
66
|
"required": [
|
|
60
67
|
"type",
|
|
@@ -62,18 +69,22 @@
|
|
|
62
69
|
],
|
|
63
70
|
"properties": {
|
|
64
71
|
"id": {
|
|
65
|
-
"type": "string"
|
|
72
|
+
"type": "string",
|
|
73
|
+
"description": "Optional ID when referencing an existing saved transform."
|
|
66
74
|
},
|
|
67
75
|
"name": {
|
|
68
76
|
"type": "string",
|
|
69
|
-
"minLength": 1
|
|
77
|
+
"minLength": 1,
|
|
78
|
+
"description": "Optional display name for the nested transform."
|
|
70
79
|
},
|
|
71
80
|
"type": {
|
|
72
81
|
"type": "string",
|
|
73
|
-
"minLength": 1
|
|
82
|
+
"minLength": 1,
|
|
83
|
+
"description": "The operation type of the nested transform (e.g., 'accountAttribute', 'identityAttribute', 'trim')."
|
|
74
84
|
},
|
|
75
85
|
"requiresPeriodicRefresh": {
|
|
76
|
-
"type": "boolean"
|
|
86
|
+
"type": "boolean",
|
|
87
|
+
"description": "Whether this nested transform re-evaluates during nightly refresh."
|
|
77
88
|
},
|
|
78
89
|
"attributes": {
|
|
79
90
|
"type": "object",
|
|
@@ -85,12 +96,49 @@
|
|
|
85
96
|
},
|
|
86
97
|
"examples": [
|
|
87
98
|
{
|
|
99
|
+
"name": "Replace Whitespace with Underscore",
|
|
88
100
|
"type": "replace",
|
|
89
|
-
"name": "Replace Transform",
|
|
90
101
|
"attributes": {
|
|
91
102
|
"regex": "\\s+",
|
|
92
103
|
"replacement": "_"
|
|
93
104
|
}
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"name": "Remove Non-Alphanumeric Characters",
|
|
108
|
+
"type": "replace",
|
|
109
|
+
"attributes": {
|
|
110
|
+
"regex": "[^A-Za-z0-9]",
|
|
111
|
+
"replacement": ""
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"name": "Extract Domain from Email (Capture Group)",
|
|
116
|
+
"type": "replace",
|
|
117
|
+
"attributes": {
|
|
118
|
+
"regex": "^[^@]+@(.+)$",
|
|
119
|
+
"replacement": "$1",
|
|
120
|
+
"input": {
|
|
121
|
+
"type": "identityAttribute",
|
|
122
|
+
"attributes": {
|
|
123
|
+
"name": "email"
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"name": "Replace Literal Dots with Hyphens",
|
|
130
|
+
"type": "replace",
|
|
131
|
+
"attributes": {
|
|
132
|
+
"regex": "[.]",
|
|
133
|
+
"replacement": "-",
|
|
134
|
+
"input": {
|
|
135
|
+
"type": "accountAttribute",
|
|
136
|
+
"attributes": {
|
|
137
|
+
"sourceName": "HR Source",
|
|
138
|
+
"attributeName": "displayName"
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
94
142
|
}
|
|
95
143
|
]
|
|
96
144
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "sailpoint.isc.transforms.substring.schema.json",
|
|
4
4
|
"title": "SailPoint ISC Transform Schema - substring",
|
|
5
|
-
"description": "Strict schema
|
|
5
|
+
"description": "Strict schema for the SailPoint ISC substring transform. Returns a portion of the input string using zero-based indexing. begin is the inclusive start position; end is the exclusive end position. Offset fields add characters to their respective boundary. Key sentinel values: begin=-1 means start at character 0 (beginOffset is ignored); end=-1 or omitted means return through the end of the string (endOffset is ignored). LIMITATION: The substring transform does not provide an easy way to get the last N characters of a string — use the getEndOfString transform instead.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"additionalProperties": false,
|
|
8
8
|
"required": [
|
|
@@ -12,16 +12,18 @@
|
|
|
12
12
|
],
|
|
13
13
|
"properties": {
|
|
14
14
|
"type": {
|
|
15
|
-
"const": "substring"
|
|
15
|
+
"const": "substring",
|
|
16
|
+
"description": "Transform operation type. Must be exactly 'substring'."
|
|
16
17
|
},
|
|
17
18
|
"name": {
|
|
18
19
|
"type": "string",
|
|
19
|
-
"minLength": 1
|
|
20
|
+
"minLength": 1,
|
|
21
|
+
"description": "Display name for this transform, shown in UI dropdowns and identity profile mappings."
|
|
20
22
|
},
|
|
21
23
|
"requiresPeriodicRefresh": {
|
|
22
24
|
"type": "boolean",
|
|
23
25
|
"default": false,
|
|
24
|
-
"description": "
|
|
26
|
+
"description": "If true, re-evaluates this transform during the nightly identity refresh cycle. Default is false."
|
|
25
27
|
},
|
|
26
28
|
"attributes": {
|
|
27
29
|
"type": "object",
|
|
@@ -29,26 +31,31 @@
|
|
|
29
31
|
"required": [
|
|
30
32
|
"begin"
|
|
31
33
|
],
|
|
34
|
+
"description": "Required container for substring operation attributes.",
|
|
32
35
|
"properties": {
|
|
33
36
|
"begin": {
|
|
34
37
|
"type": "integer",
|
|
35
|
-
"description": "
|
|
38
|
+
"description": "Required. Zero-based inclusive start index of the substring. The character at this position is included in the result. Special value: -1 means start at character 0 (the very beginning of the string), and beginOffset is ignored when begin is -1. Must be -1 or a non-negative integer."
|
|
36
39
|
},
|
|
37
40
|
"beginOffset": {
|
|
38
41
|
"type": "integer",
|
|
39
|
-
"description": "
|
|
42
|
+
"description": "Optional. Integer added to the begin value to shift the start position. Only applied when begin is not -1. Example: begin=1, beginOffset=1 results in an effective start of character 2. Ignored when begin is -1."
|
|
40
43
|
},
|
|
41
44
|
"end": {
|
|
42
45
|
"type": "integer",
|
|
43
|
-
"description": "
|
|
46
|
+
"description": "Optional. Zero-based exclusive end index of the substring. The character at this position is NOT included in the result. If omitted or set to -1, the substring extends through the end of the string. Must be -1 or a non-negative integer greater than the effective begin position."
|
|
44
47
|
},
|
|
45
48
|
"endOffset": {
|
|
46
49
|
"type": "integer",
|
|
47
|
-
"description": "
|
|
50
|
+
"description": "Optional. Integer added to the end value to shift the end position. Only applied when end is provided and is not -1. Example: end=3, endOffset=2 results in an effective end of character 5. Ignored when end is -1 or omitted."
|
|
48
51
|
},
|
|
49
52
|
"input": {
|
|
50
|
-
"description": "Optional explicit input
|
|
51
|
-
"
|
|
53
|
+
"description": "Optional explicit input providing the string to extract from. If omitted, the transform uses the source and attribute combination configured in the identity profile UI. When provided, must be a nested transform object or a static string.",
|
|
54
|
+
"anyOf": [
|
|
55
|
+
{
|
|
56
|
+
"type": "string",
|
|
57
|
+
"description": "Static string value to extract from."
|
|
58
|
+
},
|
|
52
59
|
{
|
|
53
60
|
"$ref": "#/$defs/NestedTransform"
|
|
54
61
|
}
|
|
@@ -57,54 +64,34 @@
|
|
|
57
64
|
},
|
|
58
65
|
"allOf": [
|
|
59
66
|
{
|
|
60
|
-
"description": "beginOffset is only meaningful when begin != -1.
|
|
67
|
+
"description": "beginOffset is only meaningful when begin != -1. When begin is -1, beginOffset must not be present.",
|
|
61
68
|
"if": {
|
|
62
69
|
"properties": {
|
|
63
|
-
"begin": {
|
|
64
|
-
"const": -1
|
|
65
|
-
}
|
|
70
|
+
"begin": { "const": -1 }
|
|
66
71
|
},
|
|
67
|
-
"required": [
|
|
68
|
-
"begin"
|
|
69
|
-
]
|
|
72
|
+
"required": ["begin"]
|
|
70
73
|
},
|
|
71
74
|
"then": {
|
|
72
|
-
"not": {
|
|
73
|
-
"required": [
|
|
74
|
-
"beginOffset"
|
|
75
|
-
]
|
|
76
|
-
}
|
|
75
|
+
"not": { "required": ["beginOffset"] }
|
|
77
76
|
}
|
|
78
77
|
},
|
|
79
78
|
{
|
|
80
|
-
"description": "endOffset is only meaningful when end is provided and end != -1.
|
|
79
|
+
"description": "endOffset is only meaningful when end is provided and end != -1. When end is absent or -1, endOffset must not be present.",
|
|
81
80
|
"if": {
|
|
82
81
|
"anyOf": [
|
|
83
82
|
{
|
|
84
|
-
"not": {
|
|
85
|
-
"required": [
|
|
86
|
-
"end"
|
|
87
|
-
]
|
|
88
|
-
}
|
|
83
|
+
"not": { "required": ["end"] }
|
|
89
84
|
},
|
|
90
85
|
{
|
|
91
86
|
"properties": {
|
|
92
|
-
"end": {
|
|
93
|
-
"const": -1
|
|
94
|
-
}
|
|
87
|
+
"end": { "const": -1 }
|
|
95
88
|
},
|
|
96
|
-
"required": [
|
|
97
|
-
"end"
|
|
98
|
-
]
|
|
89
|
+
"required": ["end"]
|
|
99
90
|
}
|
|
100
91
|
]
|
|
101
92
|
},
|
|
102
93
|
"then": {
|
|
103
|
-
"not": {
|
|
104
|
-
"required": [
|
|
105
|
-
"endOffset"
|
|
106
|
-
]
|
|
107
|
-
}
|
|
94
|
+
"not": { "required": ["endOffset"] }
|
|
108
95
|
}
|
|
109
96
|
}
|
|
110
97
|
]
|
|
@@ -113,55 +100,87 @@
|
|
|
113
100
|
"$defs": {
|
|
114
101
|
"NestedTransform": {
|
|
115
102
|
"type": "object",
|
|
116
|
-
"description": "
|
|
103
|
+
"description": "A nested transform object that provides the string input for the substring operation. Its output string is the value the extraction is applied to. The 'name' field is optional in nested transforms per SailPoint docs.",
|
|
117
104
|
"additionalProperties": false,
|
|
118
105
|
"required": [
|
|
119
|
-
"type"
|
|
106
|
+
"type",
|
|
107
|
+
"attributes"
|
|
120
108
|
],
|
|
121
109
|
"properties": {
|
|
122
110
|
"id": {
|
|
123
|
-
"type": "string"
|
|
111
|
+
"type": "string",
|
|
112
|
+
"description": "Optional ID when referencing an existing saved transform."
|
|
124
113
|
},
|
|
125
114
|
"name": {
|
|
126
115
|
"type": "string",
|
|
127
|
-
"minLength": 1
|
|
116
|
+
"minLength": 1,
|
|
117
|
+
"description": "Optional display name for the nested transform."
|
|
128
118
|
},
|
|
129
119
|
"type": {
|
|
130
120
|
"type": "string",
|
|
131
|
-
"minLength": 1
|
|
121
|
+
"minLength": 1,
|
|
122
|
+
"description": "The operation type of the nested transform (e.g., 'accountAttribute', 'identityAttribute', 'trim')."
|
|
132
123
|
},
|
|
133
124
|
"requiresPeriodicRefresh": {
|
|
134
|
-
"type": "boolean"
|
|
125
|
+
"type": "boolean",
|
|
126
|
+
"description": "Whether this nested transform re-evaluates during nightly refresh."
|
|
135
127
|
},
|
|
136
128
|
"attributes": {
|
|
137
|
-
"type":
|
|
138
|
-
"object",
|
|
139
|
-
"null"
|
|
140
|
-
],
|
|
129
|
+
"type": "object",
|
|
141
130
|
"additionalProperties": true,
|
|
142
|
-
"description": "Operation-specific attributes for the nested transform
|
|
131
|
+
"description": "Operation-specific attributes for the nested transform."
|
|
143
132
|
}
|
|
144
133
|
}
|
|
145
134
|
}
|
|
146
135
|
},
|
|
147
136
|
"examples": [
|
|
148
137
|
{
|
|
138
|
+
"name": "Extract Characters 2 to 4 (abcdef → cd)",
|
|
149
139
|
"type": "substring",
|
|
150
|
-
"name": "Substring Transform",
|
|
151
140
|
"attributes": {
|
|
152
141
|
"begin": 2,
|
|
153
142
|
"end": 4
|
|
154
143
|
}
|
|
155
144
|
},
|
|
156
145
|
{
|
|
146
|
+
"name": "Extract from Start to Index 3",
|
|
147
|
+
"type": "substring",
|
|
148
|
+
"attributes": {
|
|
149
|
+
"begin": -1,
|
|
150
|
+
"end": 3
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
"name": "Extract from Index 2 to End of String",
|
|
155
|
+
"type": "substring",
|
|
156
|
+
"attributes": {
|
|
157
|
+
"begin": 2
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
"name": "Extract with Offsets (abcdef → cde)",
|
|
157
162
|
"type": "substring",
|
|
158
|
-
"name": "Substring Transform (Offsets)",
|
|
159
163
|
"attributes": {
|
|
160
164
|
"begin": 1,
|
|
161
|
-
"end": 3,
|
|
162
165
|
"beginOffset": 1,
|
|
166
|
+
"end": 3,
|
|
163
167
|
"endOffset": 2
|
|
164
168
|
}
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
"name": "Extract Domain from Account Attribute",
|
|
172
|
+
"type": "substring",
|
|
173
|
+
"attributes": {
|
|
174
|
+
"begin": 0,
|
|
175
|
+
"end": 3,
|
|
176
|
+
"input": {
|
|
177
|
+
"type": "accountAttribute",
|
|
178
|
+
"attributes": {
|
|
179
|
+
"sourceName": "HR Source",
|
|
180
|
+
"attributeName": "costCenter"
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
165
184
|
}
|
|
166
185
|
]
|
|
167
186
|
}
|
package/dist/transforms/lint.js
CHANGED
|
@@ -46,7 +46,7 @@ const ALLOWED_ATTRS = {
|
|
|
46
46
|
rule: "open", // rule-specific attributes vary
|
|
47
47
|
split: new Set(["delimiter", "index", "input"]),
|
|
48
48
|
static: "open", // value + any named VTL dynamic variable keys allowed per docs
|
|
49
|
-
substring: new Set(["begin", "end", "input"]),
|
|
49
|
+
substring: new Set(["begin", "beginOffset", "end", "endOffset", "input"]),
|
|
50
50
|
trim: new Set(["input"]),
|
|
51
51
|
upper: new Set(["input"]),
|
|
52
52
|
usernameGenerator: new Set(["patterns", "sourceCheck"]),
|
|
@@ -428,29 +428,52 @@ function lintFirstValid(attrs) {
|
|
|
428
428
|
return msgs;
|
|
429
429
|
}
|
|
430
430
|
// ---------------------------------------------------------------------------
|
|
431
|
-
// 8. replace —
|
|
431
|
+
// 8. replace — regex compile, all-instances info, backreference hint
|
|
432
|
+
// Docs: https://developer.sailpoint.com/docs/extensibility/transforms/operations/replace
|
|
432
433
|
// ---------------------------------------------------------------------------
|
|
433
434
|
function lintReplace(attrs) {
|
|
434
435
|
const msgs = [];
|
|
436
|
+
// 1. regex: must be a non-empty string and a valid compilable regex
|
|
435
437
|
if (attrs?.regex !== undefined) {
|
|
436
438
|
if (typeof attrs.regex !== "string") {
|
|
437
439
|
push(msgs, "error", "regex must be a string.", "attributes.regex");
|
|
438
440
|
}
|
|
441
|
+
else if (attrs.regex.trim() === "") {
|
|
442
|
+
push(msgs, "error", "regex must not be empty.", "attributes.regex");
|
|
443
|
+
}
|
|
439
444
|
else {
|
|
440
|
-
//
|
|
445
|
+
// Compile check — surface syntax errors before ISC does
|
|
441
446
|
try {
|
|
442
447
|
new RegExp(attrs.regex);
|
|
443
448
|
}
|
|
444
449
|
catch (e) {
|
|
445
|
-
push(msgs, "error", `regex '${attrs.regex}' is not a valid regular expression: ${e?.message ?? e}
|
|
450
|
+
push(msgs, "error", `regex '${attrs.regex}' is not a valid regular expression: ${e?.message ?? String(e)}. ` +
|
|
451
|
+
"Use bracket notation for literal special characters (e.g., '[.]' for a literal dot, '[-]' for a literal hyphen).", "attributes.regex");
|
|
446
452
|
}
|
|
453
|
+
// All-instances info — users often expect first-match-only behaviour
|
|
454
|
+
push(msgs, "info", "replace replaces ALL occurrences of the pattern in the input string, not just the first match. " +
|
|
455
|
+
"To target only a specific occurrence, use a more precise regex that anchors to the position you want.", "attributes.regex");
|
|
447
456
|
}
|
|
448
457
|
}
|
|
449
|
-
|
|
450
|
-
|
|
458
|
+
// 2. replacement: must be a string; empty string is valid and deletes all matches
|
|
459
|
+
if (attrs?.replacement !== undefined) {
|
|
460
|
+
if (typeof attrs.replacement !== "string") {
|
|
461
|
+
push(msgs, "error", "replacement must be a string. Use an empty string \"\" to delete all text matched by the regex.", "attributes.replacement");
|
|
462
|
+
}
|
|
463
|
+
else if (/\$\d+/.test(attrs.replacement)) {
|
|
464
|
+
// Backreference detected — confirm the regex has the matching capture group
|
|
465
|
+
push(msgs, "info", "replacement contains a backreference (e.g., '$1'). Ensure the regex contains a matching capture group (e.g., '(.+)'). " +
|
|
466
|
+
"'$0' refers to the entire match; '$1' refers to the first capture group, '$2' to the second, etc.", "attributes.replacement");
|
|
467
|
+
}
|
|
451
468
|
}
|
|
452
|
-
|
|
453
|
-
|
|
469
|
+
// 3. input: optional per docs (omit to use UI-configured source+attribute).
|
|
470
|
+
// Validate type only when present.
|
|
471
|
+
if (attrs?.input !== undefined) {
|
|
472
|
+
const inp = attrs.input;
|
|
473
|
+
if (!(typeof inp === "string" || (isPlainObject(inp) && typeof inp.type === "string"))) {
|
|
474
|
+
push(msgs, "warn", "input must be a nested transform object {type, attributes} providing the string to apply the regex to, or a static string. " +
|
|
475
|
+
"If omitted, the transform uses the source+attribute combination configured in the identity profile UI.", "attributes.input");
|
|
476
|
+
}
|
|
454
477
|
}
|
|
455
478
|
return msgs;
|
|
456
479
|
}
|
|
@@ -1083,15 +1106,92 @@ function lintPad(attrs) {
|
|
|
1083
1106
|
return msgs;
|
|
1084
1107
|
}
|
|
1085
1108
|
// ---------------------------------------------------------------------------
|
|
1086
|
-
// 23. substring — begin/end
|
|
1109
|
+
// 23. substring — begin/end indexing, offset cross-checks, begin>=end guard
|
|
1110
|
+
// Docs: https://developer.sailpoint.com/docs/extensibility/transforms/operations/substring
|
|
1087
1111
|
// ---------------------------------------------------------------------------
|
|
1088
1112
|
function lintSubstring(attrs) {
|
|
1089
1113
|
const msgs = [];
|
|
1090
|
-
|
|
1091
|
-
|
|
1114
|
+
const begin = attrs?.begin;
|
|
1115
|
+
const end = attrs?.end;
|
|
1116
|
+
const beginOffset = attrs?.beginOffset;
|
|
1117
|
+
const endOffset = attrs?.endOffset;
|
|
1118
|
+
// 1. begin: required integer; -1 is the "start from character 0" sentinel
|
|
1119
|
+
if (begin !== undefined) {
|
|
1120
|
+
if (!isNumberish(begin)) {
|
|
1121
|
+
push(msgs, "error", "begin must be an integer (zero-based start index). Use -1 to start from character 0.", "attributes.begin");
|
|
1122
|
+
}
|
|
1123
|
+
else {
|
|
1124
|
+
const beginNum = Number(begin);
|
|
1125
|
+
if (beginNum === -1) {
|
|
1126
|
+
push(msgs, "info", "begin: -1 starts the substring at character 0 (the very beginning of the string). " +
|
|
1127
|
+
"beginOffset is ignored when begin is -1.", "attributes.begin");
|
|
1128
|
+
}
|
|
1129
|
+
else if (beginNum < -1) {
|
|
1130
|
+
push(msgs, "error", `begin value ${beginNum} is invalid. Only -1 (start at char 0) or a non-negative zero-based index are allowed.`, "attributes.begin");
|
|
1131
|
+
}
|
|
1132
|
+
// beginOffset only applies when begin != -1
|
|
1133
|
+
if (beginOffset !== undefined) {
|
|
1134
|
+
if (!isNumberish(beginOffset)) {
|
|
1135
|
+
push(msgs, "error", "beginOffset must be an integer added to the begin index.", "attributes.beginOffset");
|
|
1136
|
+
}
|
|
1137
|
+
else if (beginNum === -1) {
|
|
1138
|
+
push(msgs, "warn", "beginOffset has no effect when begin is -1. beginOffset is only applied when begin is a non-negative index.", "attributes.beginOffset");
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1092
1142
|
}
|
|
1093
|
-
|
|
1094
|
-
|
|
1143
|
+
// 2. end: optional integer; -1 or omitted means "through end of string"
|
|
1144
|
+
if (end !== undefined) {
|
|
1145
|
+
if (!isNumberish(end)) {
|
|
1146
|
+
push(msgs, "error", "end must be an integer (zero-based exclusive end index). Use -1 or omit end to return characters through the end of the string.", "attributes.end");
|
|
1147
|
+
}
|
|
1148
|
+
else {
|
|
1149
|
+
const endNum = Number(end);
|
|
1150
|
+
if (endNum === -1) {
|
|
1151
|
+
push(msgs, "info", "end: -1 returns all characters from begin through the end of the string. endOffset is ignored when end is -1.", "attributes.end");
|
|
1152
|
+
}
|
|
1153
|
+
else if (endNum < -1) {
|
|
1154
|
+
push(msgs, "error", `end value ${endNum} is invalid. Only -1 (through end of string) or a non-negative zero-based index are allowed.`, "attributes.end");
|
|
1155
|
+
}
|
|
1156
|
+
// endOffset only applies when end is provided and end != -1
|
|
1157
|
+
if (endOffset !== undefined) {
|
|
1158
|
+
if (!isNumberish(endOffset)) {
|
|
1159
|
+
push(msgs, "error", "endOffset must be an integer added to the end index.", "attributes.endOffset");
|
|
1160
|
+
}
|
|
1161
|
+
else if (endNum === -1) {
|
|
1162
|
+
push(msgs, "warn", "endOffset has no effect when end is -1. endOffset is only applied when end is a non-negative index.", "attributes.endOffset");
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
// 3. endOffset without end is meaningless
|
|
1168
|
+
if (endOffset !== undefined && end === undefined) {
|
|
1169
|
+
push(msgs, "warn", "endOffset has no effect when end is not provided. endOffset is only applied when end is explicitly set to a non-negative index.", "attributes.endOffset");
|
|
1170
|
+
}
|
|
1171
|
+
// 4. Effective begin >= effective end guard
|
|
1172
|
+
if (begin !== undefined && end !== undefined &&
|
|
1173
|
+
isNumberish(begin) && isNumberish(end)) {
|
|
1174
|
+
const beginNum = Number(begin);
|
|
1175
|
+
const endNum = Number(end);
|
|
1176
|
+
if (beginNum !== -1 && endNum !== -1) {
|
|
1177
|
+
const effectiveBegin = beginNum + (isNumberish(beginOffset) ? Number(beginOffset) : 0);
|
|
1178
|
+
const effectiveEnd = endNum + (isNumberish(endOffset) ? Number(endOffset) : 0);
|
|
1179
|
+
if (effectiveBegin >= effectiveEnd) {
|
|
1180
|
+
push(msgs, "error", `Effective begin (${effectiveBegin}) is ≥ effective end (${effectiveEnd}). ` +
|
|
1181
|
+
"This produces an empty or error result. Ensure begin + beginOffset < end + endOffset.", "attributes");
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
// 5. "Last N chars" limitation note — docs explicitly call this out and recommend getEndOfString
|
|
1186
|
+
push(msgs, "info", "To extract the last N characters of a string, use the getEndOfString transform instead. " +
|
|
1187
|
+
"The substring transform does not provide an easy way to extract characters from the end of a string.", "attributes");
|
|
1188
|
+
// 6. input: validate type if provided (optional per docs)
|
|
1189
|
+
if (attrs?.input !== undefined) {
|
|
1190
|
+
const inp = attrs.input;
|
|
1191
|
+
if (!(typeof inp === "string" || (isPlainObject(inp) && typeof inp.type === "string"))) {
|
|
1192
|
+
push(msgs, "warn", "input must be a nested transform object {type, attributes} providing the string to extract from, or a static string. " +
|
|
1193
|
+
"If omitted, the transform uses the source+attribute combination configured in the identity profile UI.", "attributes.input");
|
|
1194
|
+
}
|
|
1095
1195
|
}
|
|
1096
1196
|
return msgs;
|
|
1097
1197
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "isc-transforms-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.19",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "MCP server for SailPoint Identity Security Cloud (ISC) Transform authoring — scaffold, strict lint, catalog, and safe upsert to live tenants.",
|
|
6
6
|
"author": {
|