isc-transforms-mcp 1.0.20 → 1.0.22
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.concat.schema.json",
|
|
4
4
|
"title": "SailPoint ISC Transform Schema - concat",
|
|
5
|
-
"description": "Strict schema
|
|
5
|
+
"description": "Strict schema for the SailPoint ISC concat (Concatenation) transform. Joins an ordered array of values (static strings or nested transform outputs) into a single combined string. Key behaviors: (1) Items are joined in order with NO automatic separator — spaces, hyphens, or other delimiters must be added as explicit string entries in the array. (2) Each array entry can be a static string literal or a nested transform object whose output string is used. (3) A single-entry values array is valid JSON but semantically pointless — use the nested transform directly in that case.",
|
|
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": "concat"
|
|
15
|
+
"const": "concat",
|
|
16
|
+
"description": "Transform operation type. Must be exactly 'concat'."
|
|
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,15 +31,17 @@
|
|
|
29
31
|
"required": [
|
|
30
32
|
"values"
|
|
31
33
|
],
|
|
34
|
+
"description": "Required container for concat operation attributes. The only valid attribute is 'values'.",
|
|
32
35
|
"properties": {
|
|
33
36
|
"values": {
|
|
34
37
|
"type": "array",
|
|
35
|
-
"minItems":
|
|
36
|
-
"description": "
|
|
38
|
+
"minItems": 1,
|
|
39
|
+
"description": "Required. Ordered array of items to join into a single output string. Items are concatenated in sequence with no automatic separator. Rules: (1) Each entry must be a static string literal or a nested transform object {type, attributes} whose output is used. (2) To include spaces, hyphens, or any other separator, add them as explicit string entries between the value entries. (3) A single-entry array is valid but pointless — the nested transform can be used directly. Examples: [firstName, ' ', lastName] → 'Jane Doe'; [jobTitle, ' - ', jobCode] → 'Engineer - ENG001'.",
|
|
37
40
|
"items": {
|
|
38
|
-
"
|
|
41
|
+
"anyOf": [
|
|
39
42
|
{
|
|
40
|
-
"type": "string"
|
|
43
|
+
"type": "string",
|
|
44
|
+
"description": "Static string literal included as-is in the output (e.g., ' ', ' - ', ' (Contractor)')."
|
|
41
45
|
},
|
|
42
46
|
{
|
|
43
47
|
"$ref": "#/$defs/NestedTransform"
|
|
@@ -51,7 +55,7 @@
|
|
|
51
55
|
"$defs": {
|
|
52
56
|
"NestedTransform": {
|
|
53
57
|
"type": "object",
|
|
54
|
-
"description": "
|
|
58
|
+
"description": "A nested transform object used as one element in the values array. Its output string is inserted at the corresponding position in the concatenated result. The 'name' field is optional in nested transforms per SailPoint docs.",
|
|
55
59
|
"additionalProperties": false,
|
|
56
60
|
"required": [
|
|
57
61
|
"type",
|
|
@@ -59,18 +63,22 @@
|
|
|
59
63
|
],
|
|
60
64
|
"properties": {
|
|
61
65
|
"id": {
|
|
62
|
-
"type": "string"
|
|
66
|
+
"type": "string",
|
|
67
|
+
"description": "Optional ID when referencing an existing saved transform."
|
|
63
68
|
},
|
|
64
69
|
"name": {
|
|
65
70
|
"type": "string",
|
|
66
|
-
"minLength": 1
|
|
71
|
+
"minLength": 1,
|
|
72
|
+
"description": "Optional display name for the nested transform."
|
|
67
73
|
},
|
|
68
74
|
"type": {
|
|
69
75
|
"type": "string",
|
|
70
|
-
"minLength": 1
|
|
76
|
+
"minLength": 1,
|
|
77
|
+
"description": "The operation type of the nested transform (e.g., 'accountAttribute', 'identityAttribute', 'static', 'lower', 'trim')."
|
|
71
78
|
},
|
|
72
79
|
"requiresPeriodicRefresh": {
|
|
73
|
-
"type": "boolean"
|
|
80
|
+
"type": "boolean",
|
|
81
|
+
"description": "Whether this nested transform re-evaluates during nightly refresh."
|
|
74
82
|
},
|
|
75
83
|
"attributes": {
|
|
76
84
|
"type": "object",
|
|
@@ -82,8 +90,54 @@
|
|
|
82
90
|
},
|
|
83
91
|
"examples": [
|
|
84
92
|
{
|
|
93
|
+
"name": "Full Name from HR Source",
|
|
94
|
+
"type": "concat",
|
|
95
|
+
"attributes": {
|
|
96
|
+
"values": [
|
|
97
|
+
{
|
|
98
|
+
"type": "accountAttribute",
|
|
99
|
+
"attributes": {
|
|
100
|
+
"sourceName": "HR Source",
|
|
101
|
+
"attributeName": "FirstName"
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
" ",
|
|
105
|
+
{
|
|
106
|
+
"type": "accountAttribute",
|
|
107
|
+
"attributes": {
|
|
108
|
+
"sourceName": "HR Source",
|
|
109
|
+
"attributeName": "LastName"
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
]
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"name": "Job Title with Code",
|
|
117
|
+
"type": "concat",
|
|
118
|
+
"attributes": {
|
|
119
|
+
"values": [
|
|
120
|
+
{
|
|
121
|
+
"type": "accountAttribute",
|
|
122
|
+
"attributes": {
|
|
123
|
+
"sourceName": "HR Source",
|
|
124
|
+
"attributeName": "JobTitle"
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
" - ",
|
|
128
|
+
{
|
|
129
|
+
"type": "accountAttribute",
|
|
130
|
+
"attributes": {
|
|
131
|
+
"sourceName": "HR Source",
|
|
132
|
+
"attributeName": "JobCode"
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
]
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
"name": "Contractor Display Name",
|
|
85
140
|
"type": "concat",
|
|
86
|
-
"name": "Test Concat Transform",
|
|
87
141
|
"attributes": {
|
|
88
142
|
"values": [
|
|
89
143
|
{
|
|
@@ -104,6 +158,38 @@
|
|
|
104
158
|
" (Contractor)"
|
|
105
159
|
]
|
|
106
160
|
}
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
"name": "Lowercase Email from Identity Attributes",
|
|
164
|
+
"type": "concat",
|
|
165
|
+
"attributes": {
|
|
166
|
+
"values": [
|
|
167
|
+
{
|
|
168
|
+
"type": "lower",
|
|
169
|
+
"attributes": {
|
|
170
|
+
"input": {
|
|
171
|
+
"type": "identityAttribute",
|
|
172
|
+
"attributes": {
|
|
173
|
+
"name": "firstname"
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
".",
|
|
179
|
+
{
|
|
180
|
+
"type": "lower",
|
|
181
|
+
"attributes": {
|
|
182
|
+
"input": {
|
|
183
|
+
"type": "identityAttribute",
|
|
184
|
+
"attributes": {
|
|
185
|
+
"name": "lastname"
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
"@example.com"
|
|
191
|
+
]
|
|
192
|
+
}
|
|
107
193
|
}
|
|
108
194
|
]
|
|
109
195
|
}
|
package/dist/transforms/lint.js
CHANGED
|
@@ -18,7 +18,7 @@ const ALLOWED_ATTRS = {
|
|
|
18
18
|
]),
|
|
19
19
|
base64Decode: new Set(["input"]),
|
|
20
20
|
base64Encode: new Set(["input"]),
|
|
21
|
-
concat: new Set(["values"
|
|
21
|
+
concat: new Set(["values"]),
|
|
22
22
|
conditional: "open", // dynamic variable keys allowed per docs
|
|
23
23
|
dateCompare: new Set(["firstDate", "secondDate", "operator", "positiveCondition", "negativeCondition"]),
|
|
24
24
|
dateFormat: new Set(["input", "inputFormat", "outputFormat"]),
|
|
@@ -129,7 +129,8 @@ function checkRequired(spec, attrs) {
|
|
|
129
129
|
continue;
|
|
130
130
|
}
|
|
131
131
|
const val = attrs?.[req];
|
|
132
|
-
|
|
132
|
+
// Empty string "" is intentional for fields like negativeCondition, positiveCondition, static.value
|
|
133
|
+
if (val === undefined || val === null) {
|
|
133
134
|
push(msgs, "error", `Missing required attribute: ${req}.`, `attributes.${req}`);
|
|
134
135
|
}
|
|
135
136
|
}
|
|
@@ -276,25 +277,30 @@ function lintConditional(attrs) {
|
|
|
276
277
|
return msgs;
|
|
277
278
|
}
|
|
278
279
|
// --- 2. Forbidden operators (using any of these throws IllegalArgumentException at runtime) ---
|
|
280
|
+
// If a forbidden operator is found, that is the root cause. Skip rules 3 and 4 to avoid
|
|
281
|
+
// duplicate errors (they would fire as consequences of the forbidden operator, not separate issues).
|
|
279
282
|
const forbidden = /(!=|==|>=|<=|>|<|\bne\b|\bgt\b|\blt\b|\bge\b|\ble\b)/i;
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
+
const forbiddenOpFound = forbidden.test(expr);
|
|
284
|
+
if (forbiddenOpFound) {
|
|
285
|
+
push(msgs, "error", `Unsupported operator in expression: '${expr}'. Only 'eq' comparator is supported ` +
|
|
286
|
+
"(e.g., '$var eq value'). Using !=, ==, >, <, ne, gt, lt, ge, le throws IllegalArgumentException at runtime.", "attributes.expression");
|
|
283
287
|
}
|
|
284
|
-
// --- 3. Must contain exactly one 'eq' ---
|
|
288
|
+
// --- 3. Must contain exactly one 'eq' (skip if forbidden operator already diagnosed) ---
|
|
285
289
|
const eqMatches = expr.match(/\beq\b/gi) ?? [];
|
|
286
|
-
if (
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
"
|
|
290
|
+
if (!forbiddenOpFound) {
|
|
291
|
+
if (eqMatches.length === 0) {
|
|
292
|
+
push(msgs, "error", `Conditional expression must use the 'eq' comparator: '<ValueA> eq <ValueB>'. Got: '${expr}'.`, "attributes.expression");
|
|
293
|
+
}
|
|
294
|
+
else if (eqMatches.length > 1) {
|
|
295
|
+
push(msgs, "error", `Expression must contain exactly one 'eq'. Found ${eqMatches.length} occurrences in: '${expr}'. ` +
|
|
296
|
+
"Nest multiple conditions using separate conditional transforms if needed.", "attributes.expression");
|
|
297
|
+
}
|
|
292
298
|
}
|
|
293
|
-
// --- 4. Both sides of 'eq' must be non-empty ---
|
|
299
|
+
// --- 4. Both sides of 'eq' must be non-empty (skip if forbidden operator already diagnosed) ---
|
|
294
300
|
const parts = expr.split(/\beq\b/i);
|
|
295
301
|
const valueA = parts[0]?.trim() ?? "";
|
|
296
302
|
const valueB = parts[1]?.trim() ?? "";
|
|
297
|
-
if (parts.length !== 2 || valueA.length === 0 || valueB.length === 0) {
|
|
303
|
+
if (!forbiddenOpFound && (parts.length !== 2 || valueA.length === 0 || valueB.length === 0)) {
|
|
298
304
|
push(msgs, "error", `Expression must follow '<ValueA> eq <ValueB>' with non-empty values on both sides. Got: '${expr}'.`, "attributes.expression");
|
|
299
305
|
}
|
|
300
306
|
// --- 5. Case-sensitivity info for literal operands ---
|
|
@@ -839,6 +845,13 @@ function lintDateFormat(attrs) {
|
|
|
839
845
|
"(expected tokens like y, M, d, H, h, m, s, S, Z). " +
|
|
840
846
|
`Valid named formats: ${Array.from(NAMED_FORMATS).join(", ")}.`, `attributes.${field}`);
|
|
841
847
|
}
|
|
848
|
+
// Warn on uppercase Y (week-year) — a very common Java SimpleDateFormat mistake.
|
|
849
|
+
// YYYY = ISO 8601 week-based year (e.g. 2023-12-30 could become 2024); yyyy = calendar year.
|
|
850
|
+
if (/Y/.test(t) && !/^[A-Z][A-Z0-9_]+$/.test(t)) {
|
|
851
|
+
push(msgs, "warn", `${field} '${t}' contains uppercase 'Y' which is the ISO 8601 week-year in Java SimpleDateFormat, NOT the calendar year. ` +
|
|
852
|
+
"This is a common mistake: 'YYYY' for a December date can silently produce the following year's number. " +
|
|
853
|
+
"Use lowercase 'yyyy' for the calendar year (e.g., 'dd-MM-yyyy' instead of 'dd-MM-YYYY').", `attributes.${field}`);
|
|
854
|
+
}
|
|
842
855
|
};
|
|
843
856
|
checkFmt("inputFormat");
|
|
844
857
|
checkFmt("outputFormat");
|
|
@@ -1313,9 +1326,20 @@ function lintStatic(attrs) {
|
|
|
1313
1326
|
while ((m = refPattern.exec(value)) !== null) {
|
|
1314
1327
|
vtlRefs.add(m[1]);
|
|
1315
1328
|
}
|
|
1329
|
+
// 3a. Detect VTL-local variables assigned via #set($var = ...) — these are internal
|
|
1330
|
+
// VTL temporaries, NOT dynamic attributes that need to be defined in the attributes object.
|
|
1331
|
+
const vtlLocalVars = new Set();
|
|
1332
|
+
const setPattern = /#set\(\s*\$([A-Za-z_][A-Za-z0-9_]*)\s*[=\s]/g;
|
|
1333
|
+
let sm;
|
|
1334
|
+
while ((sm = setPattern.exec(value)) !== null) {
|
|
1335
|
+
vtlLocalVars.add(sm[1]);
|
|
1336
|
+
}
|
|
1316
1337
|
if (vtlRefs.size > 0) {
|
|
1317
|
-
// 4. Warn for each VTL reference that has no matching dynamic variable in attributes
|
|
1338
|
+
// 4. Warn for each VTL reference that has no matching dynamic variable in attributes.
|
|
1339
|
+
// Skip VTL-local variables (those assigned with #set inside the template itself).
|
|
1318
1340
|
for (const varName of vtlRefs) {
|
|
1341
|
+
if (vtlLocalVars.has(varName))
|
|
1342
|
+
continue; // #set local — not an attribute
|
|
1319
1343
|
if (!Object.prototype.hasOwnProperty.call(attrs, varName)) {
|
|
1320
1344
|
push(msgs, "warn", `VTL references $${varName} in value but no dynamic variable '${varName}' is defined in attributes. ` +
|
|
1321
1345
|
`Add a '${varName}' key to attributes with a static string or nested transform that supplies the value.`, `attributes.${varName}`);
|
|
@@ -1412,6 +1436,63 @@ function lintRfc5646(attrs) {
|
|
|
1412
1436
|
return msgs;
|
|
1413
1437
|
}
|
|
1414
1438
|
// ---------------------------------------------------------------------------
|
|
1439
|
+
// lintConcat — concat (Concatenation)
|
|
1440
|
+
// Docs: https://developer.sailpoint.com/docs/extensibility/transforms/operations/concatenation
|
|
1441
|
+
// Joins an ordered array of strings / nested-transform outputs into one string.
|
|
1442
|
+
// No automatic separators — spaces, hyphens, etc. must be explicit array entries.
|
|
1443
|
+
// ---------------------------------------------------------------------------
|
|
1444
|
+
function lintConcat(attrs) {
|
|
1445
|
+
const msgs = [];
|
|
1446
|
+
const values = attrs?.values;
|
|
1447
|
+
// --- 1. values is the only attribute and is required ---
|
|
1448
|
+
if (values === undefined || values === null) {
|
|
1449
|
+
push(msgs, "error", "values is required for concat. Provide an ordered array of strings and/or nested transform objects " +
|
|
1450
|
+
"whose outputs will be joined into a single string.", "attributes.values");
|
|
1451
|
+
return msgs;
|
|
1452
|
+
}
|
|
1453
|
+
if (!Array.isArray(values)) {
|
|
1454
|
+
push(msgs, "error", "values must be an array of strings and/or nested transform objects.", "attributes.values");
|
|
1455
|
+
return msgs;
|
|
1456
|
+
}
|
|
1457
|
+
if (values.length === 0) {
|
|
1458
|
+
push(msgs, "error", "values array must not be empty. Provide at least one string or nested transform.", "attributes.values");
|
|
1459
|
+
return msgs;
|
|
1460
|
+
}
|
|
1461
|
+
// --- 2. Warn if only a single entry — concat is pointless with one value ---
|
|
1462
|
+
if (values.length === 1) {
|
|
1463
|
+
push(msgs, "warn", "values array has only one entry. concat is designed to join multiple values — " +
|
|
1464
|
+
"consider using the nested transform directly instead of wrapping it in a concat.", "attributes.values");
|
|
1465
|
+
}
|
|
1466
|
+
// --- 3. Validate each item: must be string or a nested transform object ---
|
|
1467
|
+
values.forEach((item, idx) => {
|
|
1468
|
+
if (item === null || item === undefined) {
|
|
1469
|
+
push(msgs, "warn", `values[${idx}] is null/undefined — this will produce the string "null"/"undefined" in the output. ` +
|
|
1470
|
+
"Remove it or replace with a static string or a nested transform.", `attributes.values[${idx}]`);
|
|
1471
|
+
}
|
|
1472
|
+
else if (typeof item === "string") {
|
|
1473
|
+
// Valid — static strings (including spaces, separators, literals) are expected
|
|
1474
|
+
}
|
|
1475
|
+
else if (isPlainObject(item)) {
|
|
1476
|
+
if (typeof item.type !== "string" || item.type.trim() === "") {
|
|
1477
|
+
push(msgs, "error", `values[${idx}] is an object but is missing a 'type' field — it does not look like a valid nested transform. ` +
|
|
1478
|
+
"Add a 'type' (e.g., 'accountAttribute', 'identityAttribute', 'static').", `attributes.values[${idx}]`);
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
else {
|
|
1482
|
+
push(msgs, "error", `values[${idx}] must be a string or a nested transform object {type, attributes}. ` +
|
|
1483
|
+
`Got: ${typeof item}.`, `attributes.values[${idx}]`);
|
|
1484
|
+
}
|
|
1485
|
+
});
|
|
1486
|
+
// --- 4. Separator hint: inform if no explicit spacing string found between transform objects ---
|
|
1487
|
+
const allTransforms = values.every((v) => isPlainObject(v));
|
|
1488
|
+
if (allTransforms && values.length > 1) {
|
|
1489
|
+
push(msgs, "info", "concat does not insert any separator between values automatically. " +
|
|
1490
|
+
"If the output needs spaces, hyphens, or other delimiters, add them as explicit string entries " +
|
|
1491
|
+
"in the values array (e.g., [firstName, \" \", lastName]).", "attributes.values");
|
|
1492
|
+
}
|
|
1493
|
+
return msgs;
|
|
1494
|
+
}
|
|
1495
|
+
// ---------------------------------------------------------------------------
|
|
1415
1496
|
// Main lintTransform export
|
|
1416
1497
|
// ---------------------------------------------------------------------------
|
|
1417
1498
|
export function lintTransform(input) {
|
|
@@ -1502,6 +1583,8 @@ export function lintTransform(input) {
|
|
|
1502
1583
|
messages.push(...lintRandom(requestedType, attrs));
|
|
1503
1584
|
if (requestedType === "rfc5646")
|
|
1504
1585
|
messages.push(...lintRfc5646(attrs));
|
|
1586
|
+
if (requestedType === "concat")
|
|
1587
|
+
messages.push(...lintConcat(attrs));
|
|
1505
1588
|
// --- Recursive nested transform lint ---
|
|
1506
1589
|
// Recursively lint every nested transform found inside attributes.
|
|
1507
1590
|
// We start from normalized.attributes (not the root) to avoid double-linting root.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "isc-transforms-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.22",
|
|
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": {
|