isc-transforms-mcp 1.0.10 → 1.0.12
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.dateFormat.schema.json",
|
|
4
4
|
"title": "SailPoint ISC Transform Schema - dateFormat",
|
|
5
|
-
"description": "Strict schema
|
|
5
|
+
"description": "Strict schema for the SailPoint ISC Date Format transform. Converts a datetime string from one format to another using Java SimpleDateFormat patterns or 5 named formats: ISO8601, LDAP, PEOPLE_SOFT, EPOCH_TIME_JAVA, EPOCH_TIME_WIN32. IMPORTANT: The 'now' keyword is NOT supported as an input value for this transform.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"additionalProperties": false,
|
|
8
8
|
"required": [
|
|
@@ -11,55 +11,61 @@
|
|
|
11
11
|
],
|
|
12
12
|
"properties": {
|
|
13
13
|
"type": {
|
|
14
|
-
"const": "dateFormat"
|
|
14
|
+
"const": "dateFormat",
|
|
15
|
+
"description": "Transform operation type. Must be exactly 'dateFormat'."
|
|
15
16
|
},
|
|
16
17
|
"name": {
|
|
17
18
|
"type": "string",
|
|
18
|
-
"minLength": 1
|
|
19
|
+
"minLength": 1,
|
|
20
|
+
"description": "Display name for this transform, shown in UI dropdowns and identity profile mappings."
|
|
19
21
|
},
|
|
20
22
|
"requiresPeriodicRefresh": {
|
|
21
23
|
"type": "boolean",
|
|
22
24
|
"default": false,
|
|
23
|
-
"description": "
|
|
25
|
+
"description": "If true, re-evaluates this transform during the nightly identity refresh cycle. Default is false."
|
|
24
26
|
},
|
|
25
27
|
"attributes": {
|
|
26
28
|
"type": "object",
|
|
27
|
-
"description": "Date format configuration
|
|
29
|
+
"description": "Date format configuration. All attributes are optional — inputFormat and outputFormat both default to ISO8601 if omitted.",
|
|
28
30
|
"additionalProperties": false,
|
|
29
31
|
"properties": {
|
|
30
32
|
"inputFormat": {
|
|
31
33
|
"default": "ISO8601",
|
|
32
|
-
"description": "Format of the incoming date.
|
|
34
|
+
"description": "Format of the incoming date string. Default is ISO8601. Accepts one of the 5 named formats OR any Java SimpleDateFormat pattern. Named formats: ISO8601 (yyyy-MM-dd'T'HH:mm:ss.SSSZ), LDAP (yyyyMMddHHmmss.Z), PEOPLE_SOFT (MM/dd/yyyy), EPOCH_TIME_JAVA (milliseconds since Jan 1 1970), EPOCH_TIME_WIN32 (100-nanosecond intervals since Jan 1 1601).",
|
|
33
35
|
"anyOf": [
|
|
34
36
|
{
|
|
35
37
|
"enum": ["ISO8601", "LDAP", "PEOPLE_SOFT", "EPOCH_TIME_JAVA", "EPOCH_TIME_WIN32"],
|
|
36
|
-
"description": "Built-in named format."
|
|
38
|
+
"description": "Built-in named format. ISO8601=yyyy-MM-dd'T'HH:mm:ss.SSSZ, LDAP=yyyyMMddHHmmss.Z, PEOPLE_SOFT=MM/dd/yyyy, EPOCH_TIME_JAVA=ms since 1970, EPOCH_TIME_WIN32=100ns intervals since 1601."
|
|
37
39
|
},
|
|
38
40
|
{
|
|
39
41
|
"type": "string",
|
|
40
42
|
"not": { "pattern": "^[A-Z][A-Z0-9_]+$" },
|
|
41
|
-
"description": "Java SimpleDateFormat pattern (e.g. dd-MM-yyyy, yyyy-MM-dd'T'HH:mm:ssZ)."
|
|
43
|
+
"description": "Java SimpleDateFormat pattern (e.g. 'M/d/yyyy', 'dd-MM-yyyy', \"yyyy-MM-dd'T'HH:mm:ssZ\"). Must contain at least one date token."
|
|
42
44
|
}
|
|
43
45
|
]
|
|
44
46
|
},
|
|
45
47
|
"outputFormat": {
|
|
46
48
|
"default": "ISO8601",
|
|
47
|
-
"description": "Desired output format.
|
|
49
|
+
"description": "Desired output format. Default is ISO8601. Accepts one of the 5 named formats OR any Java SimpleDateFormat pattern. Named formats: ISO8601 (yyyy-MM-dd'T'HH:mm:ss.SSSZ), LDAP (yyyyMMddHHmmss.Z), PEOPLE_SOFT (MM/dd/yyyy), EPOCH_TIME_JAVA (milliseconds since Jan 1 1970), EPOCH_TIME_WIN32 (100-nanosecond intervals since Jan 1 1601).",
|
|
48
50
|
"anyOf": [
|
|
49
51
|
{
|
|
50
52
|
"enum": ["ISO8601", "LDAP", "PEOPLE_SOFT", "EPOCH_TIME_JAVA", "EPOCH_TIME_WIN32"],
|
|
51
|
-
"description": "Built-in named format."
|
|
53
|
+
"description": "Built-in named format. ISO8601=yyyy-MM-dd'T'HH:mm:ss.SSSZ, LDAP=yyyyMMddHHmmss.Z, PEOPLE_SOFT=MM/dd/yyyy, EPOCH_TIME_JAVA=ms since 1970, EPOCH_TIME_WIN32=100ns intervals since 1601."
|
|
52
54
|
},
|
|
53
55
|
{
|
|
54
56
|
"type": "string",
|
|
55
57
|
"not": { "pattern": "^[A-Z][A-Z0-9_]+$" },
|
|
56
|
-
"description": "Java SimpleDateFormat pattern (e.g.
|
|
58
|
+
"description": "Java SimpleDateFormat pattern (e.g. 'yyyy-MM-dd', \"yyyy-MM-dd'T'HH:mm:ssZ\"). Must contain at least one date token."
|
|
57
59
|
}
|
|
58
60
|
]
|
|
59
61
|
},
|
|
60
62
|
"input": {
|
|
61
|
-
"description": "Explicitly defines the input data passed into the transform.
|
|
62
|
-
"
|
|
63
|
+
"description": "Explicitly defines the input data passed into the transform. Can be a static date string matching the inputFormat pattern, or a nested transform object whose output is a date string. NOTE: The 'now' keyword is NOT supported by dateFormat — use dateMath instead for current-time operations.",
|
|
64
|
+
"anyOf": [
|
|
65
|
+
{
|
|
66
|
+
"type": "string",
|
|
67
|
+
"description": "Static date string that matches the inputFormat pattern (e.g., '4/1/1975' for inputFormat 'M/d/yyyy', or '144642632190' for EPOCH_TIME_JAVA). The 'now' keyword is NOT supported."
|
|
68
|
+
},
|
|
63
69
|
{
|
|
64
70
|
"$ref": "#/$defs/NestedTransform"
|
|
65
71
|
}
|
|
@@ -71,7 +77,7 @@
|
|
|
71
77
|
"$defs": {
|
|
72
78
|
"NestedTransform": {
|
|
73
79
|
"type": "object",
|
|
74
|
-
"description": "
|
|
80
|
+
"description": "A nested transform object whose output is used as the input date string for this dateFormat transform. Common use: accountAttribute to pull a date from a source, or dateMath to compute a relative date. The 'name' field is optional in nested transforms per SailPoint docs.",
|
|
75
81
|
"additionalProperties": false,
|
|
76
82
|
"required": [
|
|
77
83
|
"type",
|
|
@@ -79,18 +85,22 @@
|
|
|
79
85
|
],
|
|
80
86
|
"properties": {
|
|
81
87
|
"id": {
|
|
82
|
-
"type": "string"
|
|
88
|
+
"type": "string",
|
|
89
|
+
"description": "Optional ID when referencing an existing saved transform."
|
|
83
90
|
},
|
|
84
91
|
"name": {
|
|
85
92
|
"type": "string",
|
|
86
|
-
"minLength": 1
|
|
93
|
+
"minLength": 1,
|
|
94
|
+
"description": "Optional display name for the nested transform."
|
|
87
95
|
},
|
|
88
96
|
"type": {
|
|
89
97
|
"type": "string",
|
|
90
|
-
"minLength": 1
|
|
98
|
+
"minLength": 1,
|
|
99
|
+
"description": "The operation type of the nested transform (e.g., 'accountAttribute', 'dateMath', 'static')."
|
|
91
100
|
},
|
|
92
101
|
"requiresPeriodicRefresh": {
|
|
93
|
-
"type": "boolean"
|
|
102
|
+
"type": "boolean",
|
|
103
|
+
"description": "Whether this nested transform re-evaluates during nightly refresh."
|
|
94
104
|
},
|
|
95
105
|
"attributes": {
|
|
96
106
|
"type": "object",
|
|
@@ -102,20 +112,52 @@
|
|
|
102
112
|
},
|
|
103
113
|
"examples": [
|
|
104
114
|
{
|
|
115
|
+
"name": "Java Epoch to ISO8601",
|
|
105
116
|
"type": "dateFormat",
|
|
106
|
-
"name": "Date Format Transform",
|
|
107
117
|
"attributes": {
|
|
108
118
|
"inputFormat": "EPOCH_TIME_JAVA",
|
|
109
119
|
"outputFormat": "ISO8601"
|
|
110
120
|
}
|
|
111
121
|
},
|
|
112
122
|
{
|
|
123
|
+
"name": "US Date to Database Format",
|
|
113
124
|
"type": "dateFormat",
|
|
114
|
-
"name": "Date Format Transform (US date to DB)",
|
|
115
125
|
"attributes": {
|
|
116
126
|
"inputFormat": "M/d/yyyy",
|
|
117
127
|
"outputFormat": "yyyy-MM-dd"
|
|
118
128
|
}
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"name": "Static Date String Conversion",
|
|
132
|
+
"type": "dateFormat",
|
|
133
|
+
"attributes": {
|
|
134
|
+
"input": "12/31/1995",
|
|
135
|
+
"inputFormat": "M/d/yyyy",
|
|
136
|
+
"outputFormat": "ISO8601"
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
"name": "Source Hire Date to ISO8601",
|
|
141
|
+
"type": "dateFormat",
|
|
142
|
+
"attributes": {
|
|
143
|
+
"input": {
|
|
144
|
+
"type": "accountAttribute",
|
|
145
|
+
"attributes": {
|
|
146
|
+
"sourceName": "HR Source",
|
|
147
|
+
"attributeName": "hire_date"
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
"inputFormat": "MM/dd/yyyy",
|
|
151
|
+
"outputFormat": "ISO8601"
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
"name": "PeopleSoft Date to LDAP Format",
|
|
156
|
+
"type": "dateFormat",
|
|
157
|
+
"attributes": {
|
|
158
|
+
"inputFormat": "PEOPLE_SOFT",
|
|
159
|
+
"outputFormat": "LDAP"
|
|
160
|
+
}
|
|
119
161
|
}
|
|
120
162
|
]
|
|
121
163
|
}
|
|
@@ -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
|
}
|
package/dist/transforms/lint.js
CHANGED
|
@@ -399,27 +399,34 @@ function lintReplaceAll(attrs) {
|
|
|
399
399
|
function lintDateMath(attrs) {
|
|
400
400
|
const msgs = [];
|
|
401
401
|
const expr = attrs?.expression;
|
|
402
|
+
let startsWithNow = false;
|
|
403
|
+
let sawRound = false;
|
|
402
404
|
if (expr !== undefined) {
|
|
403
405
|
if (typeof expr !== "string" || expr.trim().length === 0) {
|
|
404
406
|
push(msgs, "error", "expression must be a non-empty string.", "attributes.expression");
|
|
405
407
|
}
|
|
406
408
|
else {
|
|
407
409
|
const s = expr.trim();
|
|
410
|
+
// --- No whitespace allowed ---
|
|
408
411
|
if (/\s/.test(s)) {
|
|
409
412
|
push(msgs, "error", "expression must not contain whitespace.", "attributes.expression");
|
|
410
413
|
}
|
|
414
|
+
// --- Valid character set: digits, y M w d h m s, n o w (for 'now'), +, -, / ---
|
|
411
415
|
if (!/^[0-9yMwdhmsnow+\-/]+$/.test(s)) {
|
|
412
|
-
push(msgs, "error", "expression contains invalid characters.
|
|
416
|
+
push(msgs, "error", "expression contains invalid characters. " +
|
|
417
|
+
"Allowed units: y(year) M(month) w(week) d(day) h(hour) m(minute) s(second), keyword 'now', operators: + - /.", "attributes.expression");
|
|
413
418
|
}
|
|
414
419
|
let i = 0;
|
|
415
420
|
const n = s.length;
|
|
416
|
-
|
|
421
|
+
startsWithNow = s.startsWith("now");
|
|
417
422
|
if (startsWithNow)
|
|
418
423
|
i += 3;
|
|
424
|
+
// 'now' must only appear at the start
|
|
419
425
|
if (!startsWithNow && s.includes("now")) {
|
|
420
|
-
push(msgs, "error", "'now' keyword must appear only at the start of the expression.", "attributes.expression");
|
|
426
|
+
push(msgs, "error", "'now' keyword must appear only at the start of the expression (e.g., 'now-5d/d', 'now+1w').", "attributes.expression");
|
|
421
427
|
}
|
|
422
|
-
let sawOp = false
|
|
428
|
+
let sawOp = false;
|
|
429
|
+
let roundUnit = null;
|
|
423
430
|
const readInt = () => {
|
|
424
431
|
const start = i;
|
|
425
432
|
while (i < n && /[0-9]/.test(s[i]))
|
|
@@ -438,19 +445,21 @@ function lintDateMath(attrs) {
|
|
|
438
445
|
const readSignedTerm = (op) => {
|
|
439
446
|
const num = readInt();
|
|
440
447
|
if (!num) {
|
|
441
|
-
push(msgs, "error", `Missing integer after '${op}' in expression.`, "attributes.expression");
|
|
448
|
+
push(msgs, "error", `Missing integer after '${op}' in expression. Example: '${op}3d'.`, "attributes.expression");
|
|
442
449
|
return;
|
|
443
450
|
}
|
|
444
|
-
if (Number(num) === 0)
|
|
445
|
-
push(msgs, "warn", `Term '${op}${num}' is a no-op
|
|
451
|
+
if (Number(num) === 0) {
|
|
452
|
+
push(msgs, "warn", `Term '${op}${num}' adds/subtracts zero — this is a no-op.`, "attributes.expression");
|
|
453
|
+
}
|
|
446
454
|
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");
|
|
455
|
+
if (!unit) {
|
|
456
|
+
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");
|
|
457
|
+
}
|
|
449
458
|
};
|
|
450
459
|
const readRound = () => {
|
|
451
460
|
const unit = readUnit();
|
|
452
461
|
if (!unit) {
|
|
453
|
-
push(msgs, "error", "Rounding '/' must be followed by a unit
|
|
462
|
+
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
463
|
return;
|
|
455
464
|
}
|
|
456
465
|
sawRound = true;
|
|
@@ -460,14 +469,14 @@ function lintDateMath(attrs) {
|
|
|
460
469
|
i++;
|
|
461
470
|
readRound();
|
|
462
471
|
if (i < n)
|
|
463
|
-
push(msgs, "error", "Rounding must be the last
|
|
472
|
+
push(msgs, "error", "Rounding '/' must be the last segment of the expression (e.g., 'now-5d/d').", "attributes.expression");
|
|
464
473
|
}
|
|
465
474
|
else {
|
|
466
475
|
while (i < n) {
|
|
467
476
|
const ch = s[i];
|
|
468
477
|
if (ch === "+" || ch === "-") {
|
|
469
478
|
if (sawRound) {
|
|
470
|
-
push(msgs, "error", "Add/subtract terms cannot appear after rounding ('/').", "attributes.expression");
|
|
479
|
+
push(msgs, "error", "Add/subtract terms cannot appear after the rounding operator ('/').", "attributes.expression");
|
|
471
480
|
break;
|
|
472
481
|
}
|
|
473
482
|
sawOp = true;
|
|
@@ -477,43 +486,75 @@ function lintDateMath(attrs) {
|
|
|
477
486
|
}
|
|
478
487
|
if (ch === "/") {
|
|
479
488
|
if (sawRound) {
|
|
480
|
-
push(msgs, "error", "Only one rounding operator '/' is
|
|
489
|
+
push(msgs, "error", "Only one rounding operator '/' is allowed per expression.", "attributes.expression");
|
|
481
490
|
break;
|
|
482
491
|
}
|
|
483
492
|
i++;
|
|
484
493
|
readRound();
|
|
485
494
|
if (i < n)
|
|
486
|
-
push(msgs, "error", "Rounding must be the last
|
|
495
|
+
push(msgs, "error", "Rounding '/' must be the last segment of the expression.", "attributes.expression");
|
|
487
496
|
break;
|
|
488
497
|
}
|
|
489
|
-
push(msgs, "error", `Unexpected token '${ch}'
|
|
498
|
+
push(msgs, "error", `Unexpected token '${ch}' in expression '${s}'. ` +
|
|
499
|
+
"Valid forms: 'now', 'now-5d/d', 'now+1y+1M', '+3M', '+12h/s'.", "attributes.expression");
|
|
490
500
|
break;
|
|
491
501
|
}
|
|
492
502
|
}
|
|
503
|
+
// Expression must start with 'now', +, -, or /
|
|
493
504
|
if (!startsWithNow && s !== "" && s[0] !== "+" && s[0] !== "-" && s[0] !== "/") {
|
|
494
|
-
push(msgs, "error",
|
|
505
|
+
push(msgs, "error", `Expression must start with 'now', '+', '-', or '/'. Got: '${s}'. ` +
|
|
506
|
+
"Examples: 'now-5d/d', '+3M', '-1y/d'.", "attributes.expression");
|
|
495
507
|
}
|
|
496
|
-
// Week rounding
|
|
508
|
+
// Week rounding is explicitly unsupported per docs
|
|
497
509
|
if (roundUnit === "w") {
|
|
498
|
-
push(msgs, "error", "Rounding with 'w' (week) is not supported
|
|
510
|
+
push(msgs, "error", "Rounding with 'w' (week) is not supported by SailPoint dateMath and will produce an error at runtime. " +
|
|
511
|
+
"Use a different unit for rounding (y, M, d, h, m, s).", "attributes.expression");
|
|
499
512
|
}
|
|
513
|
+
// Expression without 'now' and no ops is invalid
|
|
500
514
|
if (!startsWithNow && !sawOp && !sawRound) {
|
|
501
|
-
push(msgs, "error", "Expression must contain 'now', at least one +/- term, or a rounding segment."
|
|
515
|
+
push(msgs, "error", "Expression must contain 'now', at least one +/- term, or a rounding segment. " +
|
|
516
|
+
"Examples: 'now', 'now-5d/d', '+3M/h'.", "attributes.expression");
|
|
502
517
|
}
|
|
518
|
+
// Expression without 'now' requires an input date
|
|
503
519
|
if (!startsWithNow && attrs?.input === undefined) {
|
|
504
|
-
push(msgs, "error", "dateMath expression without 'now' requires an input date
|
|
520
|
+
push(msgs, "error", "dateMath expression without 'now' requires an explicit input date via attributes.input (nested transform). " +
|
|
521
|
+
"The input must produce an ISO8601 UTC datetime. Use a dateFormat transform with outputFormat: 'ISO8601' if needed.", "attributes.input");
|
|
522
|
+
}
|
|
523
|
+
// Output format info — dateMath output is yyyy-MM-dd'T'HH:mm, not full ISO8601
|
|
524
|
+
push(msgs, "info", "dateMath output format is 'yyyy-MM-dd\\'T\\'HH:mm' — this is NOT full ISO8601. " +
|
|
525
|
+
"If this transform feeds into another transform that expects ISO8601 (e.g., dateCompare), " +
|
|
526
|
+
"wrap it with a dateFormat transform using outputFormat: 'ISO8601'.", "attributes.expression");
|
|
527
|
+
// Recommend requiresPeriodicRefresh when 'now' is used
|
|
528
|
+
if (startsWithNow) {
|
|
529
|
+
push(msgs, "info", "Expression uses 'now'. Set requiresPeriodicRefresh: true at the transform root level " +
|
|
530
|
+
"so the date re-evaluates during nightly identity refresh — otherwise results may become stale.", "attributes.expression");
|
|
505
531
|
}
|
|
506
532
|
}
|
|
507
533
|
}
|
|
534
|
+
// --- roundUp: boolean check ---
|
|
508
535
|
if (attrs?.roundUp !== undefined && typeof attrs.roundUp !== "boolean") {
|
|
509
|
-
push(msgs, "error", "roundUp must be a boolean.", "attributes.roundUp");
|
|
536
|
+
push(msgs, "error", "roundUp must be a boolean. true = round up (truncate + add one unit); false = truncate only (default).", "attributes.roundUp");
|
|
510
537
|
}
|
|
538
|
+
// --- roundUp without rounding operator has no effect ---
|
|
539
|
+
if (attrs?.roundUp === true &&
|
|
540
|
+
typeof expr === "string" &&
|
|
541
|
+
!expr.includes("/")) {
|
|
542
|
+
push(msgs, "warn", "roundUp is true but expression contains no rounding operator '/'. " +
|
|
543
|
+
"roundUp has no effect without '/'. Add a rounding segment (e.g., '/d') or remove roundUp.", "attributes.roundUp");
|
|
544
|
+
}
|
|
545
|
+
// --- input: must be a nested transform object ---
|
|
511
546
|
if (attrs?.input !== undefined) {
|
|
512
547
|
const inp = attrs.input;
|
|
513
548
|
if (!(inp && typeof inp === "object" && typeof inp.type === "string")) {
|
|
514
|
-
push(msgs, "warn", "input
|
|
549
|
+
push(msgs, "warn", "input must be a nested transform object {type, attributes} that produces an ISO8601 UTC datetime. " +
|
|
550
|
+
"Use a dateFormat transform with outputFormat: 'ISO8601' if the source attribute is not already ISO8601.", "attributes.input");
|
|
515
551
|
}
|
|
516
552
|
}
|
|
553
|
+
// --- 'now' + input conflict warning ---
|
|
554
|
+
if (startsWithNow && attrs?.input !== undefined) {
|
|
555
|
+
push(msgs, "warn", "Expression uses 'now' and an input attribute is also provided. " +
|
|
556
|
+
"Per SailPoint docs, when 'now' is in the expression the transform ignores the input attribute entirely.", "attributes.input");
|
|
557
|
+
}
|
|
517
558
|
return msgs;
|
|
518
559
|
}
|
|
519
560
|
// ---------------------------------------------------------------------------
|
|
@@ -618,7 +659,17 @@ function lintDateFormat(attrs) {
|
|
|
618
659
|
const msgs = [];
|
|
619
660
|
// Exact set of named formats per SailPoint docs — no variants accepted.
|
|
620
661
|
const NAMED_FORMATS = new Set(["ISO8601", "LDAP", "PEOPLE_SOFT", "EPOCH_TIME_JAVA", "EPOCH_TIME_WIN32"]);
|
|
621
|
-
//
|
|
662
|
+
// Human-readable descriptions for each named format (shown as info when used correctly)
|
|
663
|
+
const NAMED_FORMAT_DESCRIPTIONS = {
|
|
664
|
+
ISO8601: "yyyy-MM-dd'T'HH:mm:ss.SSSZ — ISO 8601 standard datetime",
|
|
665
|
+
LDAP: "yyyyMMddHHmmss.Z — LDAP directory format",
|
|
666
|
+
PEOPLE_SOFT: "MM/dd/yyyy — PeopleSoft system format",
|
|
667
|
+
EPOCH_TIME_JAVA: "milliseconds since Jan 1, 1970 (Java epoch) — input must be a numeric string",
|
|
668
|
+
EPOCH_TIME_WIN32: "100-nanosecond intervals since Jan 1, 1601 (Windows/Win32 epoch) — input must be a numeric string",
|
|
669
|
+
};
|
|
670
|
+
// ALL_CAPS_WITH_UNDERSCORES values are clearly intended as named constants, not patterns.
|
|
671
|
+
// Do NOT fall through to isLikelyPattern — e.g. EPOCH_TIME_JAVA_IN_MILLIS contains
|
|
672
|
+
// 'H' and 'M' and would falsely pass the pattern check.
|
|
622
673
|
const looksLikeNamedConstant = (s) => /^[A-Z][A-Z0-9_]+$/.test(s);
|
|
623
674
|
const isLikelyPattern = (s) => /[yMdHhmsSZ]/.test(s);
|
|
624
675
|
const checkFmt = (field) => {
|
|
@@ -626,35 +677,59 @@ function lintDateFormat(attrs) {
|
|
|
626
677
|
if (raw === undefined)
|
|
627
678
|
return;
|
|
628
679
|
if (typeof raw !== "string") {
|
|
629
|
-
push(msgs, "error", `${field} must be a string
|
|
680
|
+
push(msgs, "error", `${field} must be a string — either a named format (${Array.from(NAMED_FORMATS).join(", ")}) or a Java SimpleDateFormat pattern.`, `attributes.${field}`);
|
|
630
681
|
return;
|
|
631
682
|
}
|
|
632
683
|
const t = raw.trim();
|
|
633
|
-
// If the value looks like a named constant (ALL_CAPS_UNDERSCORES), validate strictly.
|
|
634
|
-
// Do NOT fall through to isLikelyPattern — e.g. EPOCH_TIME_JAVA_IN_MILLIS contains
|
|
635
|
-
// 'H' and 'M' and would falsely pass the pattern check.
|
|
636
684
|
if (looksLikeNamedConstant(t)) {
|
|
637
685
|
if (!NAMED_FORMATS.has(t)) {
|
|
638
686
|
push(msgs, "error", `'${t}' is not a valid named format for ${field}. ` +
|
|
639
687
|
`Allowed named formats: ${Array.from(NAMED_FORMATS).join(", ")}. ` +
|
|
640
|
-
`Alternatively, use a Java SimpleDateFormat pattern (e.g. dd-MM-yyyy, yyyy-MM-dd'T'HH:mm:ssZ).`, `attributes.${field}`);
|
|
688
|
+
`Alternatively, use a Java SimpleDateFormat pattern (e.g. 'dd-MM-yyyy', 'yyyy-MM-dd\\'T\\'HH:mm:ssZ').`, `attributes.${field}`);
|
|
689
|
+
}
|
|
690
|
+
else {
|
|
691
|
+
push(msgs, "info", `${field} '${t}' → ${NAMED_FORMAT_DESCRIPTIONS[t]}.`, `attributes.${field}`);
|
|
641
692
|
}
|
|
642
693
|
return;
|
|
643
694
|
}
|
|
644
|
-
// Value looks like a date pattern — check it has at least one date token.
|
|
695
|
+
// Value looks like a date pattern — check it has at least one recognisable date token.
|
|
645
696
|
if (!isLikelyPattern(t)) {
|
|
646
|
-
push(msgs, "warn", `${field} '${t}' doesn't match a known named format and doesn't look like a
|
|
697
|
+
push(msgs, "warn", `${field} '${t}' doesn't match a known named format and doesn't look like a Java SimpleDateFormat pattern ` +
|
|
698
|
+
"(expected tokens like y, M, d, H, h, m, s, S, Z). " +
|
|
699
|
+
`Valid named formats: ${Array.from(NAMED_FORMATS).join(", ")}.`, `attributes.${field}`);
|
|
647
700
|
}
|
|
648
701
|
};
|
|
649
702
|
checkFmt("inputFormat");
|
|
650
703
|
checkFmt("outputFormat");
|
|
704
|
+
// --- EPOCH format input reminders ---
|
|
705
|
+
const inFmt = attrs?.inputFormat;
|
|
706
|
+
if (typeof inFmt === "string" && (inFmt === "EPOCH_TIME_JAVA" || inFmt === "EPOCH_TIME_WIN32")) {
|
|
707
|
+
push(msgs, "info", `inputFormat '${inFmt}' expects a numeric string as input — ` +
|
|
708
|
+
(inFmt === "EPOCH_TIME_JAVA"
|
|
709
|
+
? "milliseconds since Jan 1, 1970 (e.g., '1609459200000')."
|
|
710
|
+
: "100-nanosecond intervals since Jan 1, 1601 (Windows FILETIME)."), "attributes.inputFormat");
|
|
711
|
+
}
|
|
712
|
+
// --- input: validate type, reject 'now', guide on valid forms ---
|
|
651
713
|
if (attrs?.input !== undefined) {
|
|
652
714
|
const inp = attrs.input;
|
|
653
|
-
if (typeof inp === "
|
|
654
|
-
|
|
715
|
+
if (typeof inp === "string") {
|
|
716
|
+
// 'now' is explicitly NOT supported by dateFormat per official docs
|
|
717
|
+
if (inp.trim().toLowerCase() === "now") {
|
|
718
|
+
push(msgs, "error", "dateFormat does not support 'now' as an input value (official docs limitation). " +
|
|
719
|
+
"To derive a date from the current time, use a dateMath transform producing an ISO8601 string " +
|
|
720
|
+
"and reference it as a nested transform in the input field.", "attributes.input");
|
|
721
|
+
}
|
|
722
|
+
// A static date string is valid — no further warning needed
|
|
655
723
|
}
|
|
656
|
-
else if (
|
|
657
|
-
|
|
724
|
+
else if (isPlainObject(inp)) {
|
|
725
|
+
if (typeof inp.type !== "string") {
|
|
726
|
+
push(msgs, "warn", "input is an object but is missing a 'type' field — it does not look like a valid nested transform. " +
|
|
727
|
+
"Add a 'type' (e.g., 'accountAttribute', 'dateMath') to make it a proper nested transform.", "attributes.input");
|
|
728
|
+
}
|
|
729
|
+
// Otherwise it's a well-formed nested transform — no warning
|
|
730
|
+
}
|
|
731
|
+
else {
|
|
732
|
+
push(msgs, "warn", "input should be a static date string matching inputFormat, or a nested transform object {type, attributes}.", "attributes.input");
|
|
658
733
|
}
|
|
659
734
|
}
|
|
660
735
|
return msgs;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "isc-transforms-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.12",
|
|
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": {
|