isc-transforms-mcp 1.0.11 → 1.0.13
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.dateMath.schema.json",
|
|
4
4
|
"title": "SailPoint ISC Transform Schema - dateMath",
|
|
5
|
-
"description": "Strict schema
|
|
5
|
+
"description": "Strict schema for the SailPoint ISC Date Math transform. Adds, subtracts, and rounds components of a datetime using 'now' or an explicit ISO8601 UTC input. IMPORTANT CONSTRAINTS: (1) Input must be ISO8601 UTC format (yyyy-MM-dd'T'HH:mm:ss.SSSZ). (2) Output format is 'yyyy-MM-dd\\'T\\'HH:mm' — NOT full ISO8601; wrap with dateFormat outputFormat: ISO8601 when nesting in other transforms. (3) Week rounding (/w) is not supported. (4) If expression uses 'now' and an input attribute is also provided, 'now' takes precedence and input is ignored.",
|
|
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": "dateMath"
|
|
15
|
+
"const": "dateMath",
|
|
16
|
+
"description": "Transform operation type. Must be exactly 'dateMath'."
|
|
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. RECOMMENDED when expression uses 'now', so the computed date stays current. Default is false."
|
|
25
27
|
},
|
|
26
28
|
"attributes": {
|
|
27
29
|
"type": "object",
|
|
@@ -33,21 +35,17 @@
|
|
|
33
35
|
"expression": {
|
|
34
36
|
"type": "string",
|
|
35
37
|
"minLength": 1,
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
+
"pattern": "^(?!\\.*/w)(now[+\\-\\/0-9yMwdhms]*|[+\\-\\/][0-9yMwdhms+\\-\\/]*)$",
|
|
39
|
+
"description": "Date-math expression defining the operation. Valid time units: y(year) M(month) w(week) d(day) h(hour) m(minute) s(second). Keyword 'now' = current datetime (must be at start). Operators: +(add) -(subtract) /(round — must be last, NOT /w). No whitespace allowed. Examples: 'now' | 'now/h' | 'now-5d/d' | 'now+1w' | '+3M' | '+12h/s' | 'now+1y+1M+2d-4h+1m-3s/s'. NOTE: output is yyyy-MM-dd'T'HH:mm — wrap with dateFormat for ISO8601."
|
|
38
40
|
},
|
|
39
41
|
"roundUp": {
|
|
40
42
|
"type": "boolean",
|
|
41
43
|
"default": false,
|
|
42
|
-
"description": "
|
|
44
|
+
"description": "Controls rounding direction when the '/' operator is used. true = round up (truncate to unit then add one unit). false = round down (truncate only, default). Has NO effect if expression does not contain '/'."
|
|
43
45
|
},
|
|
44
46
|
"input": {
|
|
45
|
-
"description": "
|
|
46
|
-
"
|
|
47
|
-
{
|
|
48
|
-
"$ref": "#/$defs/NestedTransform"
|
|
49
|
-
}
|
|
50
|
-
]
|
|
47
|
+
"description": "Nested transform providing the input datetime. Output of the nested transform must be ISO8601 UTC (yyyy-MM-dd'T'HH:mm:ss.SSSZ). Use a dateFormat transform with outputFormat: 'ISO8601' if the source attribute is not already ISO8601. IGNORED if expression contains 'now'.",
|
|
48
|
+
"$ref": "#/$defs/NestedTransform"
|
|
51
49
|
}
|
|
52
50
|
}
|
|
53
51
|
}
|
|
@@ -55,7 +53,7 @@
|
|
|
55
53
|
"$defs": {
|
|
56
54
|
"NestedTransform": {
|
|
57
55
|
"type": "object",
|
|
58
|
-
"description": "
|
|
56
|
+
"description": "A nested transform object whose output is the input datetime for this dateMath transform. Must produce an ISO8601 UTC string. Commonly wraps an accountAttribute through a dateFormat transform. The 'name' field is optional in nested transforms per SailPoint docs.",
|
|
59
57
|
"additionalProperties": false,
|
|
60
58
|
"required": [
|
|
61
59
|
"type",
|
|
@@ -63,18 +61,22 @@
|
|
|
63
61
|
],
|
|
64
62
|
"properties": {
|
|
65
63
|
"id": {
|
|
66
|
-
"type": "string"
|
|
64
|
+
"type": "string",
|
|
65
|
+
"description": "Optional ID when referencing an existing saved transform."
|
|
67
66
|
},
|
|
68
67
|
"name": {
|
|
69
68
|
"type": "string",
|
|
70
|
-
"minLength": 1
|
|
69
|
+
"minLength": 1,
|
|
70
|
+
"description": "Optional display name for the nested transform."
|
|
71
71
|
},
|
|
72
72
|
"type": {
|
|
73
73
|
"type": "string",
|
|
74
|
-
"minLength": 1
|
|
74
|
+
"minLength": 1,
|
|
75
|
+
"description": "The operation type of the nested transform (e.g., 'dateFormat', 'accountAttribute')."
|
|
75
76
|
},
|
|
76
77
|
"requiresPeriodicRefresh": {
|
|
77
|
-
"type": "boolean"
|
|
78
|
+
"type": "boolean",
|
|
79
|
+
"description": "Whether this nested transform re-evaluates during nightly refresh."
|
|
78
80
|
},
|
|
79
81
|
"attributes": {
|
|
80
82
|
"type": "object",
|
|
@@ -86,16 +88,33 @@
|
|
|
86
88
|
},
|
|
87
89
|
"examples": [
|
|
88
90
|
{
|
|
91
|
+
"name": "Five Days Ago Rounded to Day",
|
|
89
92
|
"type": "dateMath",
|
|
90
|
-
"
|
|
93
|
+
"requiresPeriodicRefresh": true,
|
|
91
94
|
"attributes": {
|
|
92
95
|
"expression": "now-5d/d",
|
|
93
96
|
"roundUp": false
|
|
94
97
|
}
|
|
95
98
|
},
|
|
96
99
|
{
|
|
100
|
+
"name": "Current Time Rounded to Hour",
|
|
101
|
+
"type": "dateMath",
|
|
102
|
+
"requiresPeriodicRefresh": true,
|
|
103
|
+
"attributes": {
|
|
104
|
+
"expression": "now/h"
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"name": "One Week From Now",
|
|
109
|
+
"type": "dateMath",
|
|
110
|
+
"requiresPeriodicRefresh": true,
|
|
111
|
+
"attributes": {
|
|
112
|
+
"expression": "now+1w"
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"name": "Add 12 Hours to Source Date Rounded to Second",
|
|
97
117
|
"type": "dateMath",
|
|
98
|
-
"name": "Date Math Transform (Explicit Input)",
|
|
99
118
|
"attributes": {
|
|
100
119
|
"expression": "+12h/s",
|
|
101
120
|
"roundUp": true,
|
|
@@ -114,6 +133,14 @@
|
|
|
114
133
|
}
|
|
115
134
|
}
|
|
116
135
|
}
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
"name": "Complex: Add 1 Year 1 Month 2 Days Minus 4 Hours Rounded to Second",
|
|
139
|
+
"type": "dateMath",
|
|
140
|
+
"requiresPeriodicRefresh": true,
|
|
141
|
+
"attributes": {
|
|
142
|
+
"expression": "now+1y+1M+2d-4h+1m-3s/s"
|
|
143
|
+
}
|
|
117
144
|
}
|
|
118
145
|
]
|
|
119
146
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "sailpoint.isc.transforms.firstValid.schema.json",
|
|
4
4
|
"title": "SailPoint ISC Transform Schema - firstValid",
|
|
5
|
-
"description": "Strict schema
|
|
5
|
+
"description": "Strict schema for the SailPoint ISC First Valid transform. Evaluates an ordered array of values or nested transforms and returns the first entry that produces a non-null result. Entries are evaluated left-to-right; the highest-priority source should be listed first with a static string fallback last. ignoreErrors controls whether evaluation errors (e.g., NPE on missing manager) are skipped or thrown.",
|
|
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": "firstValid"
|
|
15
|
+
"const": "firstValid",
|
|
16
|
+
"description": "Transform operation type. Must be exactly 'firstValid'."
|
|
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",
|
|
@@ -33,29 +35,12 @@
|
|
|
33
35
|
"values": {
|
|
34
36
|
"type": "array",
|
|
35
37
|
"minItems": 1,
|
|
36
|
-
"description": "Ordered list of values
|
|
38
|
+
"description": "Ordered list of values to evaluate. The first entry that produces a non-null result is returned. Entries are evaluated left-to-right — place highest-priority sources first and include a static string as the last entry to guarantee a fallback. Each entry must be a static string or a nested transform object.",
|
|
37
39
|
"items": {
|
|
38
|
-
"
|
|
40
|
+
"anyOf": [
|
|
39
41
|
{
|
|
40
|
-
"type": "string"
|
|
41
|
-
|
|
42
|
-
{
|
|
43
|
-
"type": "number"
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
"type": "integer"
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
"type": "boolean"
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
"type": "array"
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
"type": "object"
|
|
56
|
-
},
|
|
57
|
-
{
|
|
58
|
-
"type": "null"
|
|
42
|
+
"type": "string",
|
|
43
|
+
"description": "Static string value. Use as a fallback (e.g., 'none', 'N/A') or a fixed value option."
|
|
59
44
|
},
|
|
60
45
|
{
|
|
61
46
|
"$ref": "#/$defs/NestedTransform"
|
|
@@ -66,7 +51,7 @@
|
|
|
66
51
|
"ignoreErrors": {
|
|
67
52
|
"type": "boolean",
|
|
68
53
|
"default": false,
|
|
69
|
-
"description": "
|
|
54
|
+
"description": "Controls error handling during evaluation. false (default) = throw on errors such as null pointer exceptions (e.g., accessing a manager attribute for a user with no manager). true = catch errors silently and move to the next entry in values. Set to true when any entry may throw on some identities."
|
|
70
55
|
}
|
|
71
56
|
}
|
|
72
57
|
}
|
|
@@ -74,7 +59,7 @@
|
|
|
74
59
|
"$defs": {
|
|
75
60
|
"NestedTransform": {
|
|
76
61
|
"type": "object",
|
|
77
|
-
"description": "
|
|
62
|
+
"description": "A nested transform object used as an entry in the values array. Must have a 'type' field. Common types: accountAttribute, identityAttribute, static, getReferenceIdentityAttribute. The 'name' field is optional in nested transforms per SailPoint docs.",
|
|
78
63
|
"additionalProperties": false,
|
|
79
64
|
"required": [
|
|
80
65
|
"type",
|
|
@@ -82,18 +67,22 @@
|
|
|
82
67
|
],
|
|
83
68
|
"properties": {
|
|
84
69
|
"id": {
|
|
85
|
-
"type": "string"
|
|
70
|
+
"type": "string",
|
|
71
|
+
"description": "Optional ID when referencing an existing saved transform."
|
|
86
72
|
},
|
|
87
73
|
"name": {
|
|
88
74
|
"type": "string",
|
|
89
|
-
"minLength": 1
|
|
75
|
+
"minLength": 1,
|
|
76
|
+
"description": "Optional display name for the nested transform."
|
|
90
77
|
},
|
|
91
78
|
"type": {
|
|
92
79
|
"type": "string",
|
|
93
|
-
"minLength": 1
|
|
80
|
+
"minLength": 1,
|
|
81
|
+
"description": "The operation type of the nested transform (e.g., 'accountAttribute', 'identityAttribute', 'static', 'getReferenceIdentityAttribute')."
|
|
94
82
|
},
|
|
95
83
|
"requiresPeriodicRefresh": {
|
|
96
|
-
"type": "boolean"
|
|
84
|
+
"type": "boolean",
|
|
85
|
+
"description": "Whether this nested transform re-evaluates during nightly refresh."
|
|
97
86
|
},
|
|
98
87
|
"attributes": {
|
|
99
88
|
"type": "object",
|
|
@@ -105,8 +94,8 @@
|
|
|
105
94
|
},
|
|
106
95
|
"examples": [
|
|
107
96
|
{
|
|
97
|
+
"name": "Preferred Name with Fallback",
|
|
108
98
|
"type": "firstValid",
|
|
109
|
-
"name": "First Valid Transform",
|
|
110
99
|
"attributes": {
|
|
111
100
|
"values": [
|
|
112
101
|
{
|
|
@@ -120,10 +109,54 @@
|
|
|
120
109
|
"attributes": {
|
|
121
110
|
"name": "givenName"
|
|
122
111
|
}
|
|
123
|
-
}
|
|
112
|
+
},
|
|
113
|
+
"Unknown"
|
|
124
114
|
],
|
|
125
115
|
"ignoreErrors": false
|
|
126
116
|
}
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"name": "Work Email Fallback to Personal Email",
|
|
120
|
+
"type": "firstValid",
|
|
121
|
+
"attributes": {
|
|
122
|
+
"values": [
|
|
123
|
+
{
|
|
124
|
+
"type": "accountAttribute",
|
|
125
|
+
"attributes": {
|
|
126
|
+
"sourceName": "Active Directory",
|
|
127
|
+
"attributeName": "mail"
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"type": "accountAttribute",
|
|
132
|
+
"attributes": {
|
|
133
|
+
"sourceName": "HR Source",
|
|
134
|
+
"attributeName": "personal_email"
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
"no-email@example.com"
|
|
138
|
+
],
|
|
139
|
+
"ignoreErrors": true
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"name": "Manager Email with NPE Protection",
|
|
144
|
+
"type": "firstValid",
|
|
145
|
+
"attributes": {
|
|
146
|
+
"values": [
|
|
147
|
+
{
|
|
148
|
+
"type": "getReferenceIdentityAttribute",
|
|
149
|
+
"attributes": {
|
|
150
|
+
"name": "Cloud Services Deployment Utility",
|
|
151
|
+
"operation": "getReferenceIdentityAttribute",
|
|
152
|
+
"uid": "manager",
|
|
153
|
+
"attributeName": "email"
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
"no-manager@example.com"
|
|
157
|
+
],
|
|
158
|
+
"ignoreErrors": true
|
|
159
|
+
}
|
|
127
160
|
}
|
|
128
161
|
]
|
|
129
162
|
}
|
package/dist/transforms/lint.js
CHANGED
|
@@ -26,7 +26,7 @@ const ALLOWED_ATTRS = {
|
|
|
26
26
|
decomposeDiacriticalMarks: new Set(["input"]),
|
|
27
27
|
displayName: new Set(["input"]),
|
|
28
28
|
e164phone: new Set(["input", "defaultRegion"]),
|
|
29
|
-
firstValid: new Set(["values", "
|
|
29
|
+
firstValid: new Set(["values", "ignoreErrors"]),
|
|
30
30
|
identityAttribute: new Set(["name", "input"]),
|
|
31
31
|
indexOf: new Set(["substring", "input"]),
|
|
32
32
|
iso3166: new Set(["format", "input"]),
|
|
@@ -349,7 +349,86 @@ function lintConditional(attrs) {
|
|
|
349
349
|
return msgs;
|
|
350
350
|
}
|
|
351
351
|
// ---------------------------------------------------------------------------
|
|
352
|
-
// 7.
|
|
352
|
+
// 7. firstValid — values array + ignoreErrors
|
|
353
|
+
// ---------------------------------------------------------------------------
|
|
354
|
+
function lintFirstValid(attrs) {
|
|
355
|
+
const msgs = [];
|
|
356
|
+
const values = attrs?.values;
|
|
357
|
+
// --- 1. values: required non-empty array (schema enforces, lint gives richer message) ---
|
|
358
|
+
if (values === undefined || values === null) {
|
|
359
|
+
push(msgs, "error", "values is required for firstValid. Provide an ordered array of strings or nested transforms — first non-null result is returned.", "attributes.values");
|
|
360
|
+
return msgs;
|
|
361
|
+
}
|
|
362
|
+
if (!Array.isArray(values)) {
|
|
363
|
+
push(msgs, "error", "values must be an array of strings and/or nested transform objects.", "attributes.values");
|
|
364
|
+
return msgs;
|
|
365
|
+
}
|
|
366
|
+
if (values.length === 0) {
|
|
367
|
+
push(msgs, "error", "values array must not be empty. Provide at least one string or nested transform.", "attributes.values");
|
|
368
|
+
return msgs;
|
|
369
|
+
}
|
|
370
|
+
// --- 2. Warn if only one value — firstValid is pointless with a single entry ---
|
|
371
|
+
if (values.length === 1) {
|
|
372
|
+
push(msgs, "warn", "values array has only one entry. firstValid is designed to fall back across multiple options — " +
|
|
373
|
+
"consider adding additional fallback values or using a simpler transform.", "attributes.values");
|
|
374
|
+
}
|
|
375
|
+
// --- 3. Validate each item: must be string or nested transform object ---
|
|
376
|
+
values.forEach((item, idx) => {
|
|
377
|
+
if (item === null || item === undefined) {
|
|
378
|
+
push(msgs, "warn", `values[${idx}] is null/undefined — this entry will always be skipped. Remove it or replace with a static string fallback.`, `attributes.values[${idx}]`);
|
|
379
|
+
}
|
|
380
|
+
else if (typeof item === "string") {
|
|
381
|
+
// strings are valid — no error
|
|
382
|
+
}
|
|
383
|
+
else if (isPlainObject(item)) {
|
|
384
|
+
if (typeof item.type !== "string" || item.type.trim() === "") {
|
|
385
|
+
push(msgs, "error", `values[${idx}] is an object but is missing a 'type' field — it does not look like a valid nested transform. ` +
|
|
386
|
+
"Add a 'type' (e.g., 'accountAttribute', 'identityAttribute', 'static').", `attributes.values[${idx}]`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
push(msgs, "error", `values[${idx}] must be a string or a nested transform object {type, attributes}. ` +
|
|
391
|
+
`Got: ${typeof item}.`, `attributes.values[${idx}]`);
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
// --- 4. Recommend a string fallback as the last entry ---
|
|
395
|
+
const lastItem = values[values.length - 1];
|
|
396
|
+
if (values.length > 1 && typeof lastItem !== "string") {
|
|
397
|
+
push(msgs, "info", "Consider making the last entry in values a static string fallback (e.g., 'none', 'N/A') " +
|
|
398
|
+
"to guarantee a non-null result when all other values are unavailable.", "attributes.values");
|
|
399
|
+
}
|
|
400
|
+
// --- 5. ignoreErrors: boolean check + semantics info ---
|
|
401
|
+
if (attrs?.ignoreErrors !== undefined) {
|
|
402
|
+
if (typeof attrs.ignoreErrors !== "boolean") {
|
|
403
|
+
push(msgs, "error", "ignoreErrors must be a boolean. " +
|
|
404
|
+
"true = skip values that throw errors (e.g., NPE on missing manager) and evaluate next entry. " +
|
|
405
|
+
"false = throw on errors (default).", "attributes.ignoreErrors");
|
|
406
|
+
}
|
|
407
|
+
else if (attrs.ignoreErrors === false) {
|
|
408
|
+
// Explicit false — check if any nested transforms reference identity attributes that could NPE
|
|
409
|
+
const hasReferenceTransforms = values.some((v) => isPlainObject(v) &&
|
|
410
|
+
["identityAttribute", "accountAttribute", "getReferenceIdentityAttribute"].includes(v.type));
|
|
411
|
+
if (hasReferenceTransforms) {
|
|
412
|
+
push(msgs, "info", "ignoreErrors is false (default). If any entry references an attribute that doesn't exist on some identities " +
|
|
413
|
+
"(e.g., a manager attribute for users without managers), a null pointer exception will stop evaluation. " +
|
|
414
|
+
"Set ignoreErrors: true to safely skip failing entries.", "attributes.ignoreErrors");
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
// ignoreErrors not set — same NPE risk hint if reference transforms present
|
|
420
|
+
const hasReferenceTransforms = values.some((v) => isPlainObject(v) &&
|
|
421
|
+
["identityAttribute", "accountAttribute", "getReferenceIdentityAttribute"].includes(v.type));
|
|
422
|
+
if (hasReferenceTransforms) {
|
|
423
|
+
push(msgs, "info", "ignoreErrors defaults to false. If any entry may throw an error on some identities " +
|
|
424
|
+
"(e.g., accessing a manager attribute for users without managers), set ignoreErrors: true " +
|
|
425
|
+
"to skip failing entries instead of halting evaluation.", "attributes.ignoreErrors");
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return msgs;
|
|
429
|
+
}
|
|
430
|
+
// ---------------------------------------------------------------------------
|
|
431
|
+
// 8. replace — validate regex compiles
|
|
353
432
|
// ---------------------------------------------------------------------------
|
|
354
433
|
function lintReplace(attrs) {
|
|
355
434
|
const msgs = [];
|
|
@@ -399,27 +478,34 @@ function lintReplaceAll(attrs) {
|
|
|
399
478
|
function lintDateMath(attrs) {
|
|
400
479
|
const msgs = [];
|
|
401
480
|
const expr = attrs?.expression;
|
|
481
|
+
let startsWithNow = false;
|
|
482
|
+
let sawRound = false;
|
|
402
483
|
if (expr !== undefined) {
|
|
403
484
|
if (typeof expr !== "string" || expr.trim().length === 0) {
|
|
404
485
|
push(msgs, "error", "expression must be a non-empty string.", "attributes.expression");
|
|
405
486
|
}
|
|
406
487
|
else {
|
|
407
488
|
const s = expr.trim();
|
|
489
|
+
// --- No whitespace allowed ---
|
|
408
490
|
if (/\s/.test(s)) {
|
|
409
491
|
push(msgs, "error", "expression must not contain whitespace.", "attributes.expression");
|
|
410
492
|
}
|
|
493
|
+
// --- Valid character set: digits, y M w d h m s, n o w (for 'now'), +, -, / ---
|
|
411
494
|
if (!/^[0-9yMwdhmsnow+\-/]+$/.test(s)) {
|
|
412
|
-
push(msgs, "error", "expression contains invalid characters.
|
|
495
|
+
push(msgs, "error", "expression contains invalid characters. " +
|
|
496
|
+
"Allowed units: y(year) M(month) w(week) d(day) h(hour) m(minute) s(second), keyword 'now', operators: + - /.", "attributes.expression");
|
|
413
497
|
}
|
|
414
498
|
let i = 0;
|
|
415
499
|
const n = s.length;
|
|
416
|
-
|
|
500
|
+
startsWithNow = s.startsWith("now");
|
|
417
501
|
if (startsWithNow)
|
|
418
502
|
i += 3;
|
|
503
|
+
// 'now' must only appear at the start
|
|
419
504
|
if (!startsWithNow && s.includes("now")) {
|
|
420
|
-
push(msgs, "error", "'now' keyword must appear only at the start of the expression.", "attributes.expression");
|
|
505
|
+
push(msgs, "error", "'now' keyword must appear only at the start of the expression (e.g., 'now-5d/d', 'now+1w').", "attributes.expression");
|
|
421
506
|
}
|
|
422
|
-
let sawOp = false
|
|
507
|
+
let sawOp = false;
|
|
508
|
+
let roundUnit = null;
|
|
423
509
|
const readInt = () => {
|
|
424
510
|
const start = i;
|
|
425
511
|
while (i < n && /[0-9]/.test(s[i]))
|
|
@@ -438,19 +524,21 @@ function lintDateMath(attrs) {
|
|
|
438
524
|
const readSignedTerm = (op) => {
|
|
439
525
|
const num = readInt();
|
|
440
526
|
if (!num) {
|
|
441
|
-
push(msgs, "error", `Missing integer after '${op}' in expression.`, "attributes.expression");
|
|
527
|
+
push(msgs, "error", `Missing integer after '${op}' in expression. Example: '${op}3d'.`, "attributes.expression");
|
|
442
528
|
return;
|
|
443
529
|
}
|
|
444
|
-
if (Number(num) === 0)
|
|
445
|
-
push(msgs, "warn", `Term '${op}${num}' is a no-op
|
|
530
|
+
if (Number(num) === 0) {
|
|
531
|
+
push(msgs, "warn", `Term '${op}${num}' adds/subtracts zero — this is a no-op.`, "attributes.expression");
|
|
532
|
+
}
|
|
446
533
|
const unit = readUnit();
|
|
447
|
-
if (!unit)
|
|
448
|
-
push(msgs, "error", `Missing unit after '${op}${num}'. Allowed: y M w d h m s.`, "attributes.expression");
|
|
534
|
+
if (!unit) {
|
|
535
|
+
push(msgs, "error", `Missing time unit after '${op}${num}'. Allowed units: y(year) M(month) w(week) d(day) h(hour) m(minute) s(second).`, "attributes.expression");
|
|
536
|
+
}
|
|
449
537
|
};
|
|
450
538
|
const readRound = () => {
|
|
451
539
|
const unit = readUnit();
|
|
452
540
|
if (!unit) {
|
|
453
|
-
push(msgs, "error", "Rounding '/' must be followed by a unit
|
|
541
|
+
push(msgs, "error", "Rounding operator '/' must be followed by a time unit. Allowed: y M d h m s (NOT w — week rounding is unsupported).", "attributes.expression");
|
|
454
542
|
return;
|
|
455
543
|
}
|
|
456
544
|
sawRound = true;
|
|
@@ -460,14 +548,14 @@ function lintDateMath(attrs) {
|
|
|
460
548
|
i++;
|
|
461
549
|
readRound();
|
|
462
550
|
if (i < n)
|
|
463
|
-
push(msgs, "error", "Rounding must be the last
|
|
551
|
+
push(msgs, "error", "Rounding '/' must be the last segment of the expression (e.g., 'now-5d/d').", "attributes.expression");
|
|
464
552
|
}
|
|
465
553
|
else {
|
|
466
554
|
while (i < n) {
|
|
467
555
|
const ch = s[i];
|
|
468
556
|
if (ch === "+" || ch === "-") {
|
|
469
557
|
if (sawRound) {
|
|
470
|
-
push(msgs, "error", "Add/subtract terms cannot appear after rounding ('/').", "attributes.expression");
|
|
558
|
+
push(msgs, "error", "Add/subtract terms cannot appear after the rounding operator ('/').", "attributes.expression");
|
|
471
559
|
break;
|
|
472
560
|
}
|
|
473
561
|
sawOp = true;
|
|
@@ -477,43 +565,75 @@ function lintDateMath(attrs) {
|
|
|
477
565
|
}
|
|
478
566
|
if (ch === "/") {
|
|
479
567
|
if (sawRound) {
|
|
480
|
-
push(msgs, "error", "Only one rounding operator '/' is
|
|
568
|
+
push(msgs, "error", "Only one rounding operator '/' is allowed per expression.", "attributes.expression");
|
|
481
569
|
break;
|
|
482
570
|
}
|
|
483
571
|
i++;
|
|
484
572
|
readRound();
|
|
485
573
|
if (i < n)
|
|
486
|
-
push(msgs, "error", "Rounding must be the last
|
|
574
|
+
push(msgs, "error", "Rounding '/' must be the last segment of the expression.", "attributes.expression");
|
|
487
575
|
break;
|
|
488
576
|
}
|
|
489
|
-
push(msgs, "error", `Unexpected token '${ch}'
|
|
577
|
+
push(msgs, "error", `Unexpected token '${ch}' in expression '${s}'. ` +
|
|
578
|
+
"Valid forms: 'now', 'now-5d/d', 'now+1y+1M', '+3M', '+12h/s'.", "attributes.expression");
|
|
490
579
|
break;
|
|
491
580
|
}
|
|
492
581
|
}
|
|
582
|
+
// Expression must start with 'now', +, -, or /
|
|
493
583
|
if (!startsWithNow && s !== "" && s[0] !== "+" && s[0] !== "-" && s[0] !== "/") {
|
|
494
|
-
push(msgs, "error",
|
|
584
|
+
push(msgs, "error", `Expression must start with 'now', '+', '-', or '/'. Got: '${s}'. ` +
|
|
585
|
+
"Examples: 'now-5d/d', '+3M', '-1y/d'.", "attributes.expression");
|
|
495
586
|
}
|
|
496
|
-
// Week rounding
|
|
587
|
+
// Week rounding is explicitly unsupported per docs
|
|
497
588
|
if (roundUnit === "w") {
|
|
498
|
-
push(msgs, "error", "Rounding with 'w' (week) is not supported
|
|
589
|
+
push(msgs, "error", "Rounding with 'w' (week) is not supported by SailPoint dateMath and will produce an error at runtime. " +
|
|
590
|
+
"Use a different unit for rounding (y, M, d, h, m, s).", "attributes.expression");
|
|
499
591
|
}
|
|
592
|
+
// Expression without 'now' and no ops is invalid
|
|
500
593
|
if (!startsWithNow && !sawOp && !sawRound) {
|
|
501
|
-
push(msgs, "error", "Expression must contain 'now', at least one +/- term, or a rounding segment."
|
|
594
|
+
push(msgs, "error", "Expression must contain 'now', at least one +/- term, or a rounding segment. " +
|
|
595
|
+
"Examples: 'now', 'now-5d/d', '+3M/h'.", "attributes.expression");
|
|
502
596
|
}
|
|
597
|
+
// Expression without 'now' requires an input date
|
|
503
598
|
if (!startsWithNow && attrs?.input === undefined) {
|
|
504
|
-
push(msgs, "error", "dateMath expression without 'now' requires an input date
|
|
599
|
+
push(msgs, "error", "dateMath expression without 'now' requires an explicit input date via attributes.input (nested transform). " +
|
|
600
|
+
"The input must produce an ISO8601 UTC datetime. Use a dateFormat transform with outputFormat: 'ISO8601' if needed.", "attributes.input");
|
|
601
|
+
}
|
|
602
|
+
// Output format info — dateMath output is yyyy-MM-dd'T'HH:mm, not full ISO8601
|
|
603
|
+
push(msgs, "info", "dateMath output format is 'yyyy-MM-dd\\'T\\'HH:mm' — this is NOT full ISO8601. " +
|
|
604
|
+
"If this transform feeds into another transform that expects ISO8601 (e.g., dateCompare), " +
|
|
605
|
+
"wrap it with a dateFormat transform using outputFormat: 'ISO8601'.", "attributes.expression");
|
|
606
|
+
// Recommend requiresPeriodicRefresh when 'now' is used
|
|
607
|
+
if (startsWithNow) {
|
|
608
|
+
push(msgs, "info", "Expression uses 'now'. Set requiresPeriodicRefresh: true at the transform root level " +
|
|
609
|
+
"so the date re-evaluates during nightly identity refresh — otherwise results may become stale.", "attributes.expression");
|
|
505
610
|
}
|
|
506
611
|
}
|
|
507
612
|
}
|
|
613
|
+
// --- roundUp: boolean check ---
|
|
508
614
|
if (attrs?.roundUp !== undefined && typeof attrs.roundUp !== "boolean") {
|
|
509
|
-
push(msgs, "error", "roundUp must be a boolean.", "attributes.roundUp");
|
|
615
|
+
push(msgs, "error", "roundUp must be a boolean. true = round up (truncate + add one unit); false = truncate only (default).", "attributes.roundUp");
|
|
616
|
+
}
|
|
617
|
+
// --- roundUp without rounding operator has no effect ---
|
|
618
|
+
if (attrs?.roundUp === true &&
|
|
619
|
+
typeof expr === "string" &&
|
|
620
|
+
!expr.includes("/")) {
|
|
621
|
+
push(msgs, "warn", "roundUp is true but expression contains no rounding operator '/'. " +
|
|
622
|
+
"roundUp has no effect without '/'. Add a rounding segment (e.g., '/d') or remove roundUp.", "attributes.roundUp");
|
|
510
623
|
}
|
|
624
|
+
// --- input: must be a nested transform object ---
|
|
511
625
|
if (attrs?.input !== undefined) {
|
|
512
626
|
const inp = attrs.input;
|
|
513
627
|
if (!(inp && typeof inp === "object" && typeof inp.type === "string")) {
|
|
514
|
-
push(msgs, "warn", "input
|
|
628
|
+
push(msgs, "warn", "input must be a nested transform object {type, attributes} that produces an ISO8601 UTC datetime. " +
|
|
629
|
+
"Use a dateFormat transform with outputFormat: 'ISO8601' if the source attribute is not already ISO8601.", "attributes.input");
|
|
515
630
|
}
|
|
516
631
|
}
|
|
632
|
+
// --- 'now' + input conflict warning ---
|
|
633
|
+
if (startsWithNow && attrs?.input !== undefined) {
|
|
634
|
+
push(msgs, "warn", "Expression uses 'now' and an input attribute is also provided. " +
|
|
635
|
+
"Per SailPoint docs, when 'now' is in the expression the transform ignores the input attribute entirely.", "attributes.input");
|
|
636
|
+
}
|
|
517
637
|
return msgs;
|
|
518
638
|
}
|
|
519
639
|
// ---------------------------------------------------------------------------
|
|
@@ -997,6 +1117,8 @@ export function lintTransform(input) {
|
|
|
997
1117
|
messages.push(...lintAccountAttribute(attrs));
|
|
998
1118
|
if (requestedType === "conditional")
|
|
999
1119
|
messages.push(...lintConditional(attrs));
|
|
1120
|
+
if (requestedType === "firstValid")
|
|
1121
|
+
messages.push(...lintFirstValid(attrs));
|
|
1000
1122
|
if (requestedType === "replace")
|
|
1001
1123
|
messages.push(...lintReplace(attrs));
|
|
1002
1124
|
if (requestedType === "replaceAll")
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "isc-transforms-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.13",
|
|
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": {
|