isc-transforms-mcp 1.0.9 → 1.0.10
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.dateCompare.schema.json",
|
|
4
4
|
"title": "SailPoint ISC Transform Schema - dateCompare",
|
|
5
|
-
"description": "Strict schema
|
|
5
|
+
"description": "Strict schema for the SailPoint ISC Date Compare transform. Compares firstDate against secondDate using the specified operator and returns positiveCondition (true) or negativeCondition (false). Both date operands must be ISO8601 datetime strings, the keyword 'now', or nested transforms whose output is ISO8601.",
|
|
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": "dateCompare"
|
|
15
|
+
"const": "dateCompare",
|
|
16
|
+
"description": "Transform operation type. Must be exactly 'dateCompare'."
|
|
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 either date operand uses 'now', so time-based comparisons stay current. Default is false."
|
|
25
27
|
},
|
|
26
28
|
"attributes": {
|
|
27
29
|
"type": "object",
|
|
@@ -35,47 +37,42 @@
|
|
|
35
37
|
],
|
|
36
38
|
"properties": {
|
|
37
39
|
"firstDate": {
|
|
38
|
-
"$ref": "#/$defs/DateOperand"
|
|
40
|
+
"$ref": "#/$defs/DateOperand",
|
|
41
|
+
"description": "Left-hand side of the comparison. Must be an ISO8601 datetime string (with time and timezone), the keyword 'now', or a nested transform whose output is ISO8601."
|
|
39
42
|
},
|
|
40
43
|
"secondDate": {
|
|
41
|
-
"$ref": "#/$defs/DateOperand"
|
|
44
|
+
"$ref": "#/$defs/DateOperand",
|
|
45
|
+
"description": "Right-hand side of the comparison. Must be an ISO8601 datetime string (with time and timezone), the keyword 'now', or a nested transform whose output is ISO8601."
|
|
42
46
|
},
|
|
43
47
|
"operator": {
|
|
44
48
|
"type": "string",
|
|
45
|
-
"description": "Comparison operator
|
|
46
|
-
"enum": [
|
|
47
|
-
"LT",
|
|
48
|
-
"LTE",
|
|
49
|
-
"GT",
|
|
50
|
-
"GTE",
|
|
51
|
-
"lt",
|
|
52
|
-
"lte",
|
|
53
|
-
"gt",
|
|
54
|
-
"gte"
|
|
55
|
-
]
|
|
49
|
+
"description": "Comparison operator (SailPoint docs specify uppercase). LT = firstDate < secondDate, LTE = firstDate ≤ secondDate, GT = firstDate > secondDate, GTE = firstDate ≥ secondDate. Case-insensitive at runtime but uppercase is recommended.",
|
|
50
|
+
"enum": ["LT", "LTE", "GT", "GTE", "lt", "lte", "gt", "gte"]
|
|
56
51
|
},
|
|
57
52
|
"positiveCondition": {
|
|
58
53
|
"type": "string",
|
|
59
|
-
"description": "
|
|
54
|
+
"description": "String value returned when the comparison evaluates to true. Can be any string (e.g., 'active', 'true', 'legacy'). Cannot be null."
|
|
60
55
|
},
|
|
61
56
|
"negativeCondition": {
|
|
62
57
|
"type": "string",
|
|
63
|
-
"description": "
|
|
58
|
+
"description": "String value returned when the comparison evaluates to false. Can be any string (e.g., 'inactive', 'false', 'regular'). Cannot be null."
|
|
64
59
|
}
|
|
65
60
|
}
|
|
66
61
|
}
|
|
67
62
|
},
|
|
68
63
|
"$defs": {
|
|
69
64
|
"DateOperand": {
|
|
70
|
-
"description": "A date operand:
|
|
65
|
+
"description": "A date operand: the keyword 'now' (evaluated at runtime), a full ISO8601 datetime string (must include time and timezone), or a nested transform object whose output is an ISO8601 datetime string.",
|
|
71
66
|
"oneOf": [
|
|
72
67
|
{
|
|
73
68
|
"type": "string",
|
|
74
|
-
"const": "now"
|
|
69
|
+
"const": "now",
|
|
70
|
+
"description": "The special keyword 'now', evaluated to the current datetime at transform execution. Must be lowercase."
|
|
75
71
|
},
|
|
76
72
|
{
|
|
77
73
|
"type": "string",
|
|
78
|
-
"
|
|
74
|
+
"pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}(:\\d{2}(\\.\\d{1,9})?)?(Z|[+-]\\d{2}:\\d{2})$",
|
|
75
|
+
"description": "ISO8601 datetime string with time and timezone. Examples: '2025-01-15T00:00:00Z', '1995-12-31T00:00:00-05:00'."
|
|
79
76
|
},
|
|
80
77
|
{
|
|
81
78
|
"$ref": "#/$defs/NestedTransform"
|
|
@@ -84,7 +81,7 @@
|
|
|
84
81
|
},
|
|
85
82
|
"NestedTransform": {
|
|
86
83
|
"type": "object",
|
|
87
|
-
"description": "
|
|
84
|
+
"description": "A nested transform object that produces a date value. The output must be an ISO8601 datetime string. Use a dateFormat transform with outputFormat: 'ISO8601' if the source attribute is not already in ISO8601 format. The 'name' field is optional in nested transforms per SailPoint docs.",
|
|
88
85
|
"additionalProperties": false,
|
|
89
86
|
"required": [
|
|
90
87
|
"type",
|
|
@@ -92,18 +89,22 @@
|
|
|
92
89
|
],
|
|
93
90
|
"properties": {
|
|
94
91
|
"id": {
|
|
95
|
-
"type": "string"
|
|
92
|
+
"type": "string",
|
|
93
|
+
"description": "Optional ID when referencing an existing saved transform."
|
|
96
94
|
},
|
|
97
95
|
"name": {
|
|
98
96
|
"type": "string",
|
|
99
|
-
"minLength": 1
|
|
97
|
+
"minLength": 1,
|
|
98
|
+
"description": "Optional display name for the nested transform."
|
|
100
99
|
},
|
|
101
100
|
"type": {
|
|
102
101
|
"type": "string",
|
|
103
|
-
"minLength": 1
|
|
102
|
+
"minLength": 1,
|
|
103
|
+
"description": "The operation type of the nested transform (e.g., 'accountAttribute', 'dateFormat', 'dateMath')."
|
|
104
104
|
},
|
|
105
105
|
"requiresPeriodicRefresh": {
|
|
106
|
-
"type": "boolean"
|
|
106
|
+
"type": "boolean",
|
|
107
|
+
"description": "Whether this nested transform re-evaluates during nightly refresh."
|
|
107
108
|
},
|
|
108
109
|
"attributes": {
|
|
109
110
|
"type": "object",
|
|
@@ -115,8 +116,9 @@
|
|
|
115
116
|
},
|
|
116
117
|
"examples": [
|
|
117
118
|
{
|
|
119
|
+
"name": "Is Termination Date in the Future",
|
|
118
120
|
"type": "dateCompare",
|
|
119
|
-
"
|
|
121
|
+
"requiresPeriodicRefresh": true,
|
|
120
122
|
"attributes": {
|
|
121
123
|
"firstDate": {
|
|
122
124
|
"type": "accountAttribute",
|
|
@@ -126,14 +128,14 @@
|
|
|
126
128
|
}
|
|
127
129
|
},
|
|
128
130
|
"secondDate": "now",
|
|
129
|
-
"operator": "
|
|
131
|
+
"operator": "GT",
|
|
130
132
|
"positiveCondition": "active",
|
|
131
133
|
"negativeCondition": "terminated"
|
|
132
134
|
}
|
|
133
135
|
},
|
|
134
136
|
{
|
|
137
|
+
"name": "Legacy vs Regular Employee Cutover",
|
|
135
138
|
"type": "dateCompare",
|
|
136
|
-
"name": "Date Compare Transform (Legacy Cutover)",
|
|
137
139
|
"attributes": {
|
|
138
140
|
"firstDate": {
|
|
139
141
|
"type": "accountAttribute",
|
|
@@ -150,10 +152,28 @@
|
|
|
150
152
|
"outputFormat": "ISO8601"
|
|
151
153
|
}
|
|
152
154
|
},
|
|
153
|
-
"operator": "
|
|
155
|
+
"operator": "LTE",
|
|
154
156
|
"positiveCondition": "legacy",
|
|
155
157
|
"negativeCondition": "regular"
|
|
156
158
|
}
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
"name": "Hire Date Before Cutover Check",
|
|
162
|
+
"type": "dateCompare",
|
|
163
|
+
"requiresPeriodicRefresh": false,
|
|
164
|
+
"attributes": {
|
|
165
|
+
"firstDate": "2020-01-01T00:00:00Z",
|
|
166
|
+
"secondDate": {
|
|
167
|
+
"type": "accountAttribute",
|
|
168
|
+
"attributes": {
|
|
169
|
+
"applicationName": "corp-hr-system",
|
|
170
|
+
"attributeName": "HIREDATE"
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
"operator": "LT",
|
|
174
|
+
"positiveCondition": "pre-2020",
|
|
175
|
+
"negativeCondition": "post-2020"
|
|
176
|
+
}
|
|
157
177
|
}
|
|
158
178
|
]
|
|
159
179
|
}
|
package/dist/transforms/lint.js
CHANGED
|
@@ -20,7 +20,7 @@ const ALLOWED_ATTRS = {
|
|
|
20
20
|
base64Encode: new Set(["input"]),
|
|
21
21
|
concat: new Set(["values", "input"]),
|
|
22
22
|
conditional: "open", // dynamic variable keys allowed per docs
|
|
23
|
-
dateCompare: new Set(["firstDate", "secondDate", "operator", "positiveCondition", "negativeCondition"
|
|
23
|
+
dateCompare: new Set(["firstDate", "secondDate", "operator", "positiveCondition", "negativeCondition"]),
|
|
24
24
|
dateFormat: new Set(["input", "inputFormat", "outputFormat"]),
|
|
25
25
|
dateMath: new Set(["expression", "input", "roundUp"]),
|
|
26
26
|
decomposeDiacriticalMarks: new Set(["input"]),
|
|
@@ -523,6 +523,7 @@ function lintDateCompare(attrs) {
|
|
|
523
523
|
const msgs = [];
|
|
524
524
|
const ISO8601_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d{1,9})?)?(Z|[+-]\d{2}:\d{2})$/;
|
|
525
525
|
const isNestedTransform = (v) => !!(v && typeof v === "object" && typeof v.type === "string");
|
|
526
|
+
let usesNow = false;
|
|
526
527
|
const checkDateOperand = (field) => {
|
|
527
528
|
const v = attrs?.[field];
|
|
528
529
|
if (v === undefined || v === null) {
|
|
@@ -531,41 +532,83 @@ function lintDateCompare(attrs) {
|
|
|
531
532
|
}
|
|
532
533
|
if (typeof v === "string") {
|
|
533
534
|
const t = v.trim();
|
|
534
|
-
|
|
535
|
+
// "now" keyword — SailPoint docs show lowercase; warn if casing differs
|
|
536
|
+
if (t.toLowerCase() === "now") {
|
|
537
|
+
usesNow = true;
|
|
538
|
+
if (t !== "now") {
|
|
539
|
+
push(msgs, "warn", `${field} value '${t}' should be the lowercase keyword 'now'. ISC evaluates it case-sensitively.`, `attributes.${field}`);
|
|
540
|
+
}
|
|
535
541
|
return;
|
|
542
|
+
}
|
|
543
|
+
// Must be a full ISO8601 datetime with time and timezone
|
|
536
544
|
if (!ISO8601_RE.test(t)) {
|
|
537
|
-
push(msgs, "error", `${field}
|
|
545
|
+
push(msgs, "error", `${field} string '${t}' is not valid. Must be an ISO8601 datetime with time and timezone ` +
|
|
546
|
+
"(e.g., '2025-01-15T00:00:00Z' or '2025-01-15T00:00:00+05:30'), the keyword 'now', or a nested transform object.", `attributes.${field}`);
|
|
538
547
|
}
|
|
539
548
|
return;
|
|
540
549
|
}
|
|
541
|
-
if (isNestedTransform(v))
|
|
550
|
+
if (isNestedTransform(v)) {
|
|
551
|
+
// Nested transforms must ultimately output an ISO8601 string for the comparison to work
|
|
552
|
+
push(msgs, "info", `${field} uses a nested '${v.type}' transform. Ensure its output is an ISO8601 datetime string. ` +
|
|
553
|
+
"If the source attribute is not ISO8601, wrap it with a dateFormat transform using outputFormat: 'ISO8601'.", `attributes.${field}`);
|
|
542
554
|
return;
|
|
543
|
-
|
|
555
|
+
}
|
|
556
|
+
push(msgs, "error", `${field} must be an ISO8601 datetime string, the keyword 'now', or a nested transform object with a 'type' field.`, `attributes.${field}`);
|
|
544
557
|
};
|
|
545
558
|
checkDateOperand("firstDate");
|
|
546
559
|
checkDateOperand("secondDate");
|
|
560
|
+
// --- operator: required, LT / LTE / GT / GTE ---
|
|
561
|
+
const VALID_OPS = new Set(["LT", "LTE", "GT", "GTE"]);
|
|
562
|
+
const OP_SEMANTICS = {
|
|
563
|
+
LT: "firstDate < secondDate",
|
|
564
|
+
LTE: "firstDate ≤ secondDate",
|
|
565
|
+
GT: "firstDate > secondDate",
|
|
566
|
+
GTE: "firstDate ≥ secondDate",
|
|
567
|
+
};
|
|
547
568
|
const op = attrs?.operator;
|
|
548
569
|
if (!op || String(op).trim() === "") {
|
|
549
|
-
push(msgs, "error", "operator is required.", "attributes.operator");
|
|
570
|
+
push(msgs, "error", "operator is required. Must be one of: LT (less than), LTE (less than or equal), GT (greater than), GTE (greater than or equal).", "attributes.operator");
|
|
550
571
|
}
|
|
551
572
|
else {
|
|
552
|
-
const
|
|
553
|
-
if (!
|
|
554
|
-
push(msgs, "error",
|
|
573
|
+
const opUpper = String(op).trim().toUpperCase();
|
|
574
|
+
if (!VALID_OPS.has(opUpper)) {
|
|
575
|
+
push(msgs, "error", `operator '${op}' is not valid. Allowed values: LT, LTE, GT, GTE (case-insensitive). ` +
|
|
576
|
+
"LT = firstDate < secondDate, LTE = ≤, GT = >, GTE = ≥.", "attributes.operator");
|
|
577
|
+
}
|
|
578
|
+
else {
|
|
579
|
+
if (op !== opUpper) {
|
|
580
|
+
push(msgs, "warn", `operator '${op}' is accepted but SailPoint docs specify uppercase. Use '${opUpper}' for consistency.`, "attributes.operator");
|
|
581
|
+
}
|
|
582
|
+
push(msgs, "info", `operator '${opUpper}': ${OP_SEMANTICS[opUpper]}. ` +
|
|
583
|
+
"Returns positiveCondition when true, negativeCondition when false.", "attributes.operator");
|
|
555
584
|
}
|
|
556
585
|
}
|
|
586
|
+
// --- positiveCondition: required string ---
|
|
557
587
|
if (attrs?.positiveCondition === undefined || attrs.positiveCondition === null) {
|
|
558
|
-
push(msgs, "error", "positiveCondition is required
|
|
588
|
+
push(msgs, "error", "positiveCondition is required — the string value returned when the date comparison evaluates to true.", "attributes.positiveCondition");
|
|
559
589
|
}
|
|
560
590
|
else if (typeof attrs.positiveCondition !== "string") {
|
|
561
591
|
push(msgs, "error", "positiveCondition must be a string.", "attributes.positiveCondition");
|
|
562
592
|
}
|
|
593
|
+
// --- negativeCondition: required string ---
|
|
563
594
|
if (attrs?.negativeCondition === undefined || attrs.negativeCondition === null) {
|
|
564
|
-
push(msgs, "error", "negativeCondition is required
|
|
595
|
+
push(msgs, "error", "negativeCondition is required — the string value returned when the date comparison evaluates to false.", "attributes.negativeCondition");
|
|
565
596
|
}
|
|
566
597
|
else if (typeof attrs.negativeCondition !== "string") {
|
|
567
598
|
push(msgs, "error", "negativeCondition must be a string.", "attributes.negativeCondition");
|
|
568
599
|
}
|
|
600
|
+
// --- Warn if positiveCondition === negativeCondition (comparison has no effect) ---
|
|
601
|
+
if (typeof attrs?.positiveCondition === "string" &&
|
|
602
|
+
typeof attrs?.negativeCondition === "string" &&
|
|
603
|
+
attrs.positiveCondition === attrs.negativeCondition) {
|
|
604
|
+
push(msgs, "warn", `positiveCondition and negativeCondition are both '${attrs.positiveCondition}'. ` +
|
|
605
|
+
"The comparison result has no effect since both branches return the same value.", "attributes");
|
|
606
|
+
}
|
|
607
|
+
// --- Recommend requiresPeriodicRefresh when 'now' is used ---
|
|
608
|
+
if (usesNow) {
|
|
609
|
+
push(msgs, "info", "One or both date operands use 'now'. Set requiresPeriodicRefresh: true at the transform root level " +
|
|
610
|
+
"so the comparison re-evaluates during nightly identity refresh — otherwise results may become stale for active identities.", "attributes");
|
|
611
|
+
}
|
|
569
612
|
return msgs;
|
|
570
613
|
}
|
|
571
614
|
// ---------------------------------------------------------------------------
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "isc-transforms-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10",
|
|
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": {
|