isc-transforms-mcp 1.0.0
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.
- package/JSONS/authoritative-operation-catalog.json +280 -0
- package/JSONS/sailpoint.isc.transforms.accountAttribute.schema.json +164 -0
- package/JSONS/sailpoint.isc.transforms.base64Decode.schema.json +37 -0
- package/JSONS/sailpoint.isc.transforms.base64Encode.schema.json +32 -0
- package/JSONS/sailpoint.isc.transforms.concat.schema.json +109 -0
- package/JSONS/sailpoint.isc.transforms.conditional.schema.json +161 -0
- package/JSONS/sailpoint.isc.transforms.dateCompare.schema.json +159 -0
- package/JSONS/sailpoint.isc.transforms.dateFormat.schema.json +101 -0
- package/JSONS/sailpoint.isc.transforms.dateMath.schema.json +119 -0
- package/JSONS/sailpoint.isc.transforms.decomposeDiacriticalMarks.schema.json +92 -0
- package/JSONS/sailpoint.isc.transforms.displayName.schema.json +42 -0
- package/JSONS/sailpoint.isc.transforms.e164phone.schema.json +107 -0
- package/JSONS/sailpoint.isc.transforms.firstValid.schema.json +129 -0
- package/JSONS/sailpoint.isc.transforms.generateRandomString.schema.json +94 -0
- package/JSONS/sailpoint.isc.transforms.getEndOfString.schema.json +118 -0
- package/JSONS/sailpoint.isc.transforms.getReferenceIdentityAttribute.schema.json +79 -0
- package/JSONS/sailpoint.isc.transforms.identityAttribute.schema.json +104 -0
- package/JSONS/sailpoint.isc.transforms.index.schema.json +48 -0
- package/JSONS/sailpoint.isc.transforms.indexOf.schema.json +90 -0
- package/JSONS/sailpoint.isc.transforms.iso3166.schema.json +103 -0
- package/JSONS/sailpoint.isc.transforms.join.schema.json +113 -0
- package/JSONS/sailpoint.isc.transforms.lastIndexOf.schema.json +90 -0
- package/JSONS/sailpoint.isc.transforms.leftPad.schema.json +96 -0
- package/JSONS/sailpoint.isc.transforms.lookup.schema.json +100 -0
- package/JSONS/sailpoint.isc.transforms.lower.schema.json +80 -0
- package/JSONS/sailpoint.isc.transforms.normalizeNames.schema.json +79 -0
- package/JSONS/sailpoint.isc.transforms.randomAlphaNumeric.schema.json +53 -0
- package/JSONS/sailpoint.isc.transforms.randomNumeric.schema.json +53 -0
- package/JSONS/sailpoint.isc.transforms.reference.schema.json +90 -0
- package/JSONS/sailpoint.isc.transforms.replace.schema.json +96 -0
- package/JSONS/sailpoint.isc.transforms.replaceAll.schema.json +96 -0
- package/JSONS/sailpoint.isc.transforms.rfc5646.schema.json +79 -0
- package/JSONS/sailpoint.isc.transforms.rightPad.schema.json +96 -0
- package/JSONS/sailpoint.isc.transforms.rule.schema.json +106 -0
- package/JSONS/sailpoint.isc.transforms.split.schema.json +103 -0
- package/JSONS/sailpoint.isc.transforms.static.schema.json +131 -0
- package/JSONS/sailpoint.isc.transforms.substring.schema.json +167 -0
- package/JSONS/sailpoint.isc.transforms.trim.schema.json +93 -0
- package/JSONS/sailpoint.isc.transforms.upper.schema.json +80 -0
- package/JSONS/sailpoint.isc.transforms.usernameGenerator.schema.json +106 -0
- package/JSONS/sailpoint.isc.transforms.uuid.schema.json +32 -0
- package/LICENSE +21 -0
- package/README.md +221 -0
- package/bin/isc-transforms-mcp.mjs +3 -0
- package/dist/allowlist.js +37 -0
- package/dist/config.js +67 -0
- package/dist/http/errors.js +19 -0
- package/dist/http/iscAuth.js +45 -0
- package/dist/http/iscClient.js +73 -0
- package/dist/index.js +613 -0
- package/dist/logger.js +9 -0
- package/dist/redact.js +28 -0
- package/dist/transforms/catalog.js +566 -0
- package/dist/transforms/explain.js +266 -0
- package/dist/transforms/generate.js +551 -0
- package/dist/transforms/index.js +9 -0
- package/dist/transforms/lint.js +839 -0
- package/dist/transforms/normalize.js +96 -0
- package/dist/transforms/patterns.js +295 -0
- package/dist/transforms/testcases.js +350 -0
- package/dist/transforms/validate.js +250 -0
- package/dist/util/diff.js +23 -0
- package/package.json +76 -0
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
// src/transforms/generate.ts
|
|
2
|
+
// Requirement-to-Transform JSON generator.
|
|
3
|
+
//
|
|
4
|
+
// Pipeline:
|
|
5
|
+
// 1. Parse requirement for entities (source names, attribute names, date formats, literals, etc.)
|
|
6
|
+
// 2. Score each operation type against requirement keywords
|
|
7
|
+
// 3. Select best operation, build JSON from catalog scaffold + extracted params
|
|
8
|
+
// 4. Return transform JSON + confidence + alternatives + placeholders
|
|
9
|
+
//
|
|
10
|
+
// This module is fully offline — no tenant access required.
|
|
11
|
+
import { TRANSFORM_CATALOG } from "./catalog.js";
|
|
12
|
+
function parseRequirement(req) {
|
|
13
|
+
const lower = req.toLowerCase();
|
|
14
|
+
// Extract quoted strings
|
|
15
|
+
const quotedStrings = [];
|
|
16
|
+
for (const m of req.matchAll(/["']([^"']+)["']/g))
|
|
17
|
+
quotedStrings.push(m[1]);
|
|
18
|
+
// Date format patterns — Java-style tokens
|
|
19
|
+
const datePatterns = (req.match(/\b(yyyy|YYYY|MM|dd|HH|hh|mm|ss|SSS|Z|'T'|ISO8601|EPOCH_TIME_JAVA|EPOCH_TIME_WIN32|LDAP_GENERALIZED_TIME)\b/g) ?? []).map(String);
|
|
20
|
+
// Number literals
|
|
21
|
+
const numberLiterals = (req.match(/\b\d+\b/g) ?? []).map(Number).filter((n) => n >= 0 && n <= 1000);
|
|
22
|
+
// Capitalised words (2+ chars) that aren't at the start of sentence → likely source/attr names
|
|
23
|
+
const properNouns = (req.match(/\b[A-Z][a-zA-Z]{2,}\b/g) ?? [])
|
|
24
|
+
.filter((w) => !["SailPoint", "ISC", "MCP", "JSON", "The", "This", "For", "From", "When", "If", "Or", "And", "Get", "Set", "Use"].includes(w));
|
|
25
|
+
const potentialSources = properNouns.filter((w) => /source|system|app|directory|ldap|active|hr|workday|sap|salesforce|ad|azure|okta/i.test(req) &&
|
|
26
|
+
!["True", "False"].includes(w));
|
|
27
|
+
const potentialAttributes = properNouns.filter((w) => /attribute|field|column|property/i.test(req) || w.match(/^(email|phone|name|dept|department|title|manager|company|country|locale|username|login|displayname|givenname|surname|sn|lastname|firstname|middlename|employeeid|costcenter|division|location|city|state|zip|postalcode|birthdate|hiredate|terminationdate)$/i));
|
|
28
|
+
return {
|
|
29
|
+
raw: req,
|
|
30
|
+
lower,
|
|
31
|
+
quotedStrings,
|
|
32
|
+
potentialSources,
|
|
33
|
+
potentialAttributes,
|
|
34
|
+
datePatterns,
|
|
35
|
+
numberLiterals,
|
|
36
|
+
hasNullHandling: /null|empty|missing|blank|undefined|not set|no value/i.test(req),
|
|
37
|
+
hasFallback: /fallback|default|if null|first valid|first non.?null|otherwise/i.test(req),
|
|
38
|
+
hasUniqueness: /unique|uniqueness|duplicate|conflict|counter/i.test(req),
|
|
39
|
+
hasNesting: /nested|combine|multiple|chain|composed|build from/i.test(req),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const OPERATION_SCORES = {
|
|
43
|
+
static: [
|
|
44
|
+
{ keywords: ["static", "constant", "fixed", "hardcoded", "literal", "always"], weight: 10 },
|
|
45
|
+
{ keywords: ["value", "string"], phraseBoost: ["static value", "constant value", "fixed value"], weight: 5 },
|
|
46
|
+
],
|
|
47
|
+
concat: [
|
|
48
|
+
{ keywords: ["concat", "concatenate", "join", "combine", "merge", "append", "build", "construct"], weight: 10 },
|
|
49
|
+
{ phraseBoost: ["first name", "last name", "full name", "display name"], keywords: [], weight: 6 },
|
|
50
|
+
{ keywords: ["dot", "dash", "hyphen", "space", "separator", "between"], weight: 4 },
|
|
51
|
+
],
|
|
52
|
+
conditional: [
|
|
53
|
+
{ keywords: ["if", "else", "when", "conditional", "condition", "depends", "based on", "evaluate"], weight: 10 },
|
|
54
|
+
{ keywords: ["true", "false", "yes", "no", "return"], weight: 3 },
|
|
55
|
+
{ phraseBoost: ["if equals", "when equals", "based on value", "if department", "if status"], keywords: [], weight: 7 },
|
|
56
|
+
],
|
|
57
|
+
firstValid: [
|
|
58
|
+
{ keywords: ["fallback", "first valid", "first non-null", "first non null", "coalesce"], weight: 12 },
|
|
59
|
+
{ keywords: ["null", "empty", "missing", "not set"], phraseBoost: ["if null", "if empty", "fallback to"], weight: 8 },
|
|
60
|
+
{ keywords: ["preferred", "alternative", "backup"], weight: 5 },
|
|
61
|
+
],
|
|
62
|
+
dateFormat: [
|
|
63
|
+
{ keywords: ["date format", "format date", "convert date", "reformat", "date conversion"], weight: 12 },
|
|
64
|
+
{ keywords: ["epoch", "iso8601", "iso 8601", "timestamp", "dateformat", "inputformat", "outputformat"], weight: 10 },
|
|
65
|
+
{ keywords: ["yyyy", "mm/dd", "dd/mm", "date pattern", "java date"], weight: 8 },
|
|
66
|
+
{ keywords: ["date", "time", "format"], weight: 5 },
|
|
67
|
+
],
|
|
68
|
+
dateMath: [
|
|
69
|
+
{ keywords: ["add days", "subtract days", "date math", "date arithmetic", "days from", "months from", "plus days", "minus days"], weight: 12 },
|
|
70
|
+
{ keywords: ["add", "subtract", "plus", "minus", "future", "past", "offset", "round"], phraseBoost: ["add to date", "subtract from date"], weight: 5 },
|
|
71
|
+
{ keywords: ["expiry", "expiration", "90 days", "30 days", "1 year"], weight: 8 },
|
|
72
|
+
],
|
|
73
|
+
dateCompare: [
|
|
74
|
+
{ keywords: ["compare date", "date compare", "before", "after", "earlier", "later"], weight: 12 },
|
|
75
|
+
{ keywords: ["gt", "lt", "gte", "lte", "greater than", "less than"], phraseBoost: ["date before", "date after"], weight: 8 },
|
|
76
|
+
{ keywords: ["expired", "active", "inactive", "lifecycle"], weight: 5 },
|
|
77
|
+
],
|
|
78
|
+
usernameGenerator: [
|
|
79
|
+
{ keywords: ["username", "login", "account name", "user id", "userid", "unique username", "generate username"], weight: 15 },
|
|
80
|
+
{ keywords: ["unique", "uniquecounter", "collision", "duplicate check"], phraseBoost: ["unique username", "no duplicates"], weight: 10 },
|
|
81
|
+
{ keywords: ["pattern", "template", "${", "fn", "ln"], weight: 6 },
|
|
82
|
+
],
|
|
83
|
+
accountAttribute: [
|
|
84
|
+
{ keywords: ["account attribute", "source attribute", "from source", "from active directory", "from hr", "from workday"], weight: 12 },
|
|
85
|
+
{ keywords: ["source", "application", "account", "sourcename", "attributename"], weight: 6 },
|
|
86
|
+
],
|
|
87
|
+
identityAttribute: [
|
|
88
|
+
{ keywords: ["identity attribute", "identity field", "profile attribute"], weight: 12 },
|
|
89
|
+
{ keywords: ["identity", "givenname", "sn", "email", "manager"], weight: 4 },
|
|
90
|
+
],
|
|
91
|
+
lookup: [
|
|
92
|
+
{ keywords: ["lookup", "map", "mapping", "table", "dictionary", "translate", "convert code", "replace code"], weight: 12 },
|
|
93
|
+
{ keywords: ["key", "value", "pair", "department code", "country code", "abbreviation"], weight: 5 },
|
|
94
|
+
],
|
|
95
|
+
replace: [
|
|
96
|
+
{ keywords: ["replace", "regex", "regular expression", "remove characters", "strip", "substitute"], weight: 10 },
|
|
97
|
+
{ keywords: ["pattern", "match", "find and replace"], weight: 5 },
|
|
98
|
+
],
|
|
99
|
+
replaceAll: [
|
|
100
|
+
{ keywords: ["replace all", "remove spaces", "remove dashes", "clean string", "sanitize", "normalize string"], weight: 10 },
|
|
101
|
+
{ phraseBoost: ["replace multiple", "clean up"], keywords: [], weight: 5 },
|
|
102
|
+
],
|
|
103
|
+
split: [
|
|
104
|
+
{ keywords: ["split", "extract part", "delimiter", "tokenize", "divide"], weight: 10 },
|
|
105
|
+
{ keywords: ["comma separated", "pipe separated", "first part", "second part", "index"], weight: 6 },
|
|
106
|
+
],
|
|
107
|
+
substring: [
|
|
108
|
+
{ keywords: ["substring", "extract characters", "first n characters", "chars", "slice", "truncate"], weight: 10 },
|
|
109
|
+
{ keywords: ["begin", "end", "start", "position", "character"], weight: 5 },
|
|
110
|
+
],
|
|
111
|
+
lower: [
|
|
112
|
+
{ keywords: ["lowercase", "lower case", "lower"], weight: 12 },
|
|
113
|
+
{ phraseBoost: ["all lowercase", "convert to lowercase"], keywords: [], weight: 8 },
|
|
114
|
+
],
|
|
115
|
+
upper: [
|
|
116
|
+
{ keywords: ["uppercase", "upper case", "upper", "caps"], weight: 12 },
|
|
117
|
+
{ phraseBoost: ["all uppercase", "all caps", "convert to uppercase"], keywords: [], weight: 8 },
|
|
118
|
+
],
|
|
119
|
+
trim: [
|
|
120
|
+
{ keywords: ["trim", "strip whitespace", "remove spaces", "leading spaces", "trailing spaces"], weight: 12 },
|
|
121
|
+
],
|
|
122
|
+
normalizeNames: [
|
|
123
|
+
{ keywords: ["normalize name", "name normalizer", "capitalize name", "format name", "normalize capitalization"], weight: 12 },
|
|
124
|
+
{ keywords: ["mcintosh", "von", "de la", "roman numeral", "mc mac"], weight: 8 },
|
|
125
|
+
],
|
|
126
|
+
e164phone: [
|
|
127
|
+
{ keywords: ["e.164", "e164", "phone number", "international phone", "phone format", "standardize phone"], weight: 12 },
|
|
128
|
+
{ keywords: ["phone", "mobile", "telephone", "region code", "country code"], weight: 5 },
|
|
129
|
+
],
|
|
130
|
+
iso3166: [
|
|
131
|
+
{ keywords: ["iso3166", "iso 3166", "country code", "alpha2", "alpha3", "numeric country"], weight: 12 },
|
|
132
|
+
{ keywords: ["country", "nation", "iso"], weight: 4 },
|
|
133
|
+
],
|
|
134
|
+
rfc5646: [
|
|
135
|
+
{ keywords: ["rfc5646", "rfc 5646", "locale", "language code", "bcp47", "language tag"], weight: 12 },
|
|
136
|
+
{ keywords: ["locale", "language", "region"], weight: 3 },
|
|
137
|
+
],
|
|
138
|
+
leftPad: [
|
|
139
|
+
{ keywords: ["left pad", "zero pad", "pad left", "leading zeros", "zero fill"], weight: 12 },
|
|
140
|
+
{ keywords: ["pad", "fill", "length", "zeros"], weight: 5 },
|
|
141
|
+
],
|
|
142
|
+
rightPad: [
|
|
143
|
+
{ keywords: ["right pad", "pad right", "trailing"], weight: 12 },
|
|
144
|
+
{ keywords: ["pad", "fill", "length"], weight: 5 },
|
|
145
|
+
],
|
|
146
|
+
indexOf: [
|
|
147
|
+
{ keywords: ["index of", "find position", "position of", "find char", "find string"], weight: 10 },
|
|
148
|
+
{ keywords: ["position", "offset", "location"], weight: 4 },
|
|
149
|
+
],
|
|
150
|
+
lastIndexOf: [
|
|
151
|
+
{ keywords: ["last index", "last occurrence", "last position", "last slash", "last dot"], weight: 10 },
|
|
152
|
+
],
|
|
153
|
+
join: [
|
|
154
|
+
{ keywords: ["join array", "array to string", "list to string", "join with"], weight: 10 },
|
|
155
|
+
{ keywords: ["separator", "values array", "list"], weight: 4 },
|
|
156
|
+
],
|
|
157
|
+
decomposeDiacriticalMarks: [
|
|
158
|
+
{ keywords: ["diacritical", "accent", "diacritic", "decompose", "unicode normalize", "ascii", "strip accent"], weight: 12 },
|
|
159
|
+
{ keywords: ["special characters", "unicode", "é", "ü", "ñ"], weight: 6 },
|
|
160
|
+
],
|
|
161
|
+
displayName: [
|
|
162
|
+
{ keywords: ["display name", "displayname", "preferred name", "full display"], weight: 12 },
|
|
163
|
+
],
|
|
164
|
+
base64Encode: [
|
|
165
|
+
{ keywords: ["base64 encode", "encode base64", "base64"], weight: 12 },
|
|
166
|
+
],
|
|
167
|
+
base64Decode: [
|
|
168
|
+
{ keywords: ["base64 decode", "decode base64", "base64 encoded"], weight: 12 },
|
|
169
|
+
],
|
|
170
|
+
uuid: [
|
|
171
|
+
{ keywords: ["uuid", "guid", "unique id", "random uuid", "generate uuid"], weight: 15 },
|
|
172
|
+
],
|
|
173
|
+
randomAlphaNumeric: [
|
|
174
|
+
{ keywords: ["random alphanumeric", "random string", "random password", "alphanumeric"], weight: 12 },
|
|
175
|
+
{ keywords: ["random", "generate", "string"], weight: 3 },
|
|
176
|
+
],
|
|
177
|
+
randomNumeric: [
|
|
178
|
+
{ keywords: ["random numeric", "random number", "random digits", "numeric random"], weight: 12 },
|
|
179
|
+
],
|
|
180
|
+
generateRandomString: [
|
|
181
|
+
{ keywords: ["random string", "generate random", "random password", "secure random"], phraseBoost: ["generate random string"], weight: 14 },
|
|
182
|
+
{ keywords: ["numbers", "special chars", "include numbers"], weight: 6 },
|
|
183
|
+
],
|
|
184
|
+
getEndOfString: [
|
|
185
|
+
{ keywords: ["end of string", "last n characters", "last chars", "last digits", "trailing chars"], weight: 12 },
|
|
186
|
+
{ keywords: ["numchars", "last", "end"], weight: 4 },
|
|
187
|
+
],
|
|
188
|
+
getReferenceIdentityAttribute: [
|
|
189
|
+
{ keywords: ["reference identity", "manager attribute", "get reference", "manager email", "manager name"], weight: 12 },
|
|
190
|
+
{ keywords: ["manager", "reference", "uid", "linked identity"], weight: 5 },
|
|
191
|
+
],
|
|
192
|
+
reference: [
|
|
193
|
+
{ keywords: ["reference transform", "reuse transform", "call transform", "existing transform"], weight: 12 },
|
|
194
|
+
],
|
|
195
|
+
};
|
|
196
|
+
function scoreOperations(parsed) {
|
|
197
|
+
const scores = {};
|
|
198
|
+
for (const [opType, rules] of Object.entries(OPERATION_SCORES)) {
|
|
199
|
+
let total = 0;
|
|
200
|
+
for (const rule of rules) {
|
|
201
|
+
for (const kw of rule.keywords) {
|
|
202
|
+
if (parsed.lower.includes(kw.toLowerCase()))
|
|
203
|
+
total += rule.weight;
|
|
204
|
+
}
|
|
205
|
+
for (const phrase of rule.phraseBoost ?? []) {
|
|
206
|
+
if (parsed.lower.includes(phrase.toLowerCase()))
|
|
207
|
+
total += rule.weight * 0.8;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
// Boosts from parsed metadata
|
|
211
|
+
if ((opType === "firstValid" || opType === "concat") && parsed.hasFallback)
|
|
212
|
+
total += 5;
|
|
213
|
+
if (opType === "usernameGenerator" && parsed.hasUniqueness)
|
|
214
|
+
total += 10;
|
|
215
|
+
if (opType === "dateFormat" && parsed.datePatterns.length > 0)
|
|
216
|
+
total += 8;
|
|
217
|
+
if ((opType === "substring" || opType === "leftPad" || opType === "rightPad") && parsed.numberLiterals.length > 0)
|
|
218
|
+
total += 4;
|
|
219
|
+
if (opType === "concat" && parsed.hasNesting)
|
|
220
|
+
total += 4;
|
|
221
|
+
if (opType === "firstValid" && parsed.hasNullHandling)
|
|
222
|
+
total += 6;
|
|
223
|
+
if (total > 0)
|
|
224
|
+
scores[opType] = total;
|
|
225
|
+
}
|
|
226
|
+
return Object.entries(scores)
|
|
227
|
+
.sort((a, b) => b[1] - a[1])
|
|
228
|
+
.map(([type, score]) => ({ type, score }));
|
|
229
|
+
}
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
// JSON builder — fills in scaffold with extracted params
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
function buildTransformJson(opType, parsed, transformName) {
|
|
234
|
+
const spec = TRANSFORM_CATALOG[opType];
|
|
235
|
+
const placeholders = [];
|
|
236
|
+
let transform;
|
|
237
|
+
if (!spec) {
|
|
238
|
+
transform = { type: opType, name: transformName, attributes: {} };
|
|
239
|
+
placeholders.push("<attributes>");
|
|
240
|
+
return { transform, placeholders };
|
|
241
|
+
}
|
|
242
|
+
// Start from the scaffold
|
|
243
|
+
transform = spec.scaffold(transformName);
|
|
244
|
+
// Apply extracted parameters to common operations
|
|
245
|
+
switch (opType) {
|
|
246
|
+
case "static": {
|
|
247
|
+
const val = parsed.quotedStrings[0] ?? "<STATIC_VALUE>";
|
|
248
|
+
if (!parsed.quotedStrings[0])
|
|
249
|
+
placeholders.push("<STATIC_VALUE>");
|
|
250
|
+
transform.attributes.value = val;
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
case "concat": {
|
|
254
|
+
// Build values from attribute names or use defaults
|
|
255
|
+
if (parsed.potentialAttributes.length >= 2) {
|
|
256
|
+
transform.attributes.values = parsed.potentialAttributes.slice(0, 3).map((a) => ({
|
|
257
|
+
type: "identityAttribute",
|
|
258
|
+
attributes: { name: a },
|
|
259
|
+
}));
|
|
260
|
+
}
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
case "accountAttribute": {
|
|
264
|
+
if (parsed.potentialSources[0]) {
|
|
265
|
+
transform.attributes.sourceName = parsed.potentialSources[0];
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
placeholders.push("<sourceName>");
|
|
269
|
+
}
|
|
270
|
+
if (parsed.potentialAttributes[0]) {
|
|
271
|
+
transform.attributes.attributeName = parsed.potentialAttributes[0];
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
placeholders.push("<attributeName>");
|
|
275
|
+
}
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
case "identityAttribute": {
|
|
279
|
+
if (parsed.potentialAttributes[0]) {
|
|
280
|
+
transform.attributes.name = parsed.potentialAttributes[0];
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
placeholders.push("<attributeName>");
|
|
284
|
+
}
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
case "dateFormat": {
|
|
288
|
+
if (parsed.datePatterns.length >= 2) {
|
|
289
|
+
transform.attributes.inputFormat = parsed.datePatterns[0];
|
|
290
|
+
transform.attributes.outputFormat = parsed.datePatterns[1];
|
|
291
|
+
}
|
|
292
|
+
else if (parsed.datePatterns.length === 1) {
|
|
293
|
+
transform.attributes.outputFormat = parsed.datePatterns[0];
|
|
294
|
+
placeholders.push("<inputFormat>");
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
placeholders.push("<inputFormat>", "<outputFormat>");
|
|
298
|
+
}
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
case "dateMath": {
|
|
302
|
+
// Look for patterns like "+90d", "now+1y"
|
|
303
|
+
const mathMatch = parsed.raw.match(/\b(now)?([+-]\d+[yMwdhmS])\b/);
|
|
304
|
+
if (mathMatch) {
|
|
305
|
+
transform.attributes.expression = (mathMatch[1] ?? "now") + mathMatch[2];
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
placeholders.push("<expression> (e.g. 'now+90d' or 'now-1y/M')");
|
|
309
|
+
}
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
case "conditional": {
|
|
313
|
+
if (parsed.potentialAttributes[0]) {
|
|
314
|
+
transform.attributes[parsed.potentialAttributes[0].toLowerCase()] = {
|
|
315
|
+
type: "accountAttribute",
|
|
316
|
+
attributes: { sourceName: parsed.potentialSources[0] ?? "<SOURCE_NAME>", attributeName: parsed.potentialAttributes[0] },
|
|
317
|
+
};
|
|
318
|
+
if (!parsed.potentialSources[0])
|
|
319
|
+
placeholders.push("<sourceName>");
|
|
320
|
+
transform.attributes.expression = `$${parsed.potentialAttributes[0].toLowerCase()} eq <VALUE>`;
|
|
321
|
+
placeholders.push("<VALUE> in expression");
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
placeholders.push("<variable>", "<expression>", "<positiveCondition>", "<negativeCondition>");
|
|
325
|
+
}
|
|
326
|
+
if (parsed.quotedStrings.length >= 2) {
|
|
327
|
+
transform.attributes.positiveCondition = parsed.quotedStrings[0];
|
|
328
|
+
transform.attributes.negativeCondition = parsed.quotedStrings[1];
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
placeholders.push("<positiveCondition>", "<negativeCondition>");
|
|
332
|
+
}
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
case "lookup": {
|
|
336
|
+
if (parsed.quotedStrings.length >= 2) {
|
|
337
|
+
const table = {};
|
|
338
|
+
for (let i = 0; i + 1 < parsed.quotedStrings.length; i += 2) {
|
|
339
|
+
table[parsed.quotedStrings[i]] = parsed.quotedStrings[i + 1];
|
|
340
|
+
}
|
|
341
|
+
table["default"] = "Unknown";
|
|
342
|
+
transform.attributes.table = table;
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
placeholders.push("<table> key-value pairs");
|
|
346
|
+
}
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
case "replace": {
|
|
350
|
+
if (parsed.quotedStrings[0]) {
|
|
351
|
+
transform.attributes.regex = parsed.quotedStrings[0];
|
|
352
|
+
transform.attributes.replacement = parsed.quotedStrings[1] ?? "";
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
placeholders.push("<regex>", "<replacement>");
|
|
356
|
+
}
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
case "replaceAll": {
|
|
360
|
+
if (parsed.quotedStrings.length >= 2) {
|
|
361
|
+
const table = {};
|
|
362
|
+
for (let i = 0; i + 1 < parsed.quotedStrings.length; i += 2) {
|
|
363
|
+
table[parsed.quotedStrings[i]] = parsed.quotedStrings[i + 1];
|
|
364
|
+
}
|
|
365
|
+
transform.attributes.table = table;
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
placeholders.push("<table> of characters-to-replace");
|
|
369
|
+
}
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
case "split": {
|
|
373
|
+
if (parsed.quotedStrings[0]) {
|
|
374
|
+
transform.attributes.delimiter = parsed.quotedStrings[0];
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
placeholders.push("<delimiter>");
|
|
378
|
+
}
|
|
379
|
+
transform.attributes.index = parsed.numberLiterals[0] ?? 0;
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
case "substring": {
|
|
383
|
+
transform.attributes.begin = parsed.numberLiterals[0] ?? 0;
|
|
384
|
+
if (parsed.numberLiterals[1] !== undefined) {
|
|
385
|
+
transform.attributes.end = parsed.numberLiterals[1];
|
|
386
|
+
}
|
|
387
|
+
break;
|
|
388
|
+
}
|
|
389
|
+
case "leftPad":
|
|
390
|
+
case "rightPad": {
|
|
391
|
+
if (parsed.numberLiterals[0])
|
|
392
|
+
transform.attributes.length = String(parsed.numberLiterals[0]);
|
|
393
|
+
else
|
|
394
|
+
placeholders.push("<length>");
|
|
395
|
+
if (parsed.quotedStrings[0])
|
|
396
|
+
transform.attributes.padding = parsed.quotedStrings[0];
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
case "usernameGenerator": {
|
|
400
|
+
// Build patterns from attribute names if available
|
|
401
|
+
const attrs = parsed.potentialAttributes;
|
|
402
|
+
if (attrs.length >= 2) {
|
|
403
|
+
const fn = attrs[0].toLowerCase().slice(0, 2); // e.g. "gi" from "givenName"
|
|
404
|
+
const ln = attrs[1].toLowerCase().slice(0, 2); // e.g. "su" from "surname"
|
|
405
|
+
transform.attributes = {
|
|
406
|
+
patterns: [
|
|
407
|
+
`\${${fn}}.\${${ln}}`,
|
|
408
|
+
`\${${fn}}.\${${ln}}\${uniqueCounter}`,
|
|
409
|
+
],
|
|
410
|
+
[fn]: { type: "identityAttribute", attributes: { name: attrs[0] } },
|
|
411
|
+
[ln]: { type: "identityAttribute", attributes: { name: attrs[1] } },
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
placeholders.push("<patterns array>", "<dynamic variable definitions>");
|
|
416
|
+
}
|
|
417
|
+
break;
|
|
418
|
+
}
|
|
419
|
+
case "e164phone": {
|
|
420
|
+
const regionMatch = parsed.raw.match(/\b([A-Z]{2})\b/);
|
|
421
|
+
if (regionMatch)
|
|
422
|
+
transform.attributes.defaultRegion = regionMatch[1];
|
|
423
|
+
else
|
|
424
|
+
placeholders.push("<defaultRegion> (ISO 3166-1 alpha-2 e.g. 'US')");
|
|
425
|
+
break;
|
|
426
|
+
}
|
|
427
|
+
case "iso3166": {
|
|
428
|
+
const fmt = parsed.lower.includes("alpha3") ? "alpha3" :
|
|
429
|
+
parsed.lower.includes("numeric") ? "numeric" : "alpha2";
|
|
430
|
+
transform.attributes.format = fmt;
|
|
431
|
+
break;
|
|
432
|
+
}
|
|
433
|
+
case "generateRandomString": {
|
|
434
|
+
const len = parsed.numberLiterals.find((n) => n >= 4 && n <= 450) ?? 16;
|
|
435
|
+
transform.attributes.length = String(len);
|
|
436
|
+
transform.attributes.includeNumbers = parsed.lower.includes("number") ? "true" : "false";
|
|
437
|
+
transform.attributes.includeSpecialChars = parsed.lower.includes("special") ? "true" : "false";
|
|
438
|
+
break;
|
|
439
|
+
}
|
|
440
|
+
case "getEndOfString": {
|
|
441
|
+
const n = parsed.numberLiterals.find((v) => v >= 1 && v <= 100) ?? 4;
|
|
442
|
+
transform.attributes.numChars = String(n);
|
|
443
|
+
break;
|
|
444
|
+
}
|
|
445
|
+
case "getReferenceIdentityAttribute": {
|
|
446
|
+
const isManager = parsed.lower.includes("manager");
|
|
447
|
+
transform.attributes.uid = isManager ? "manager" : "<IDENTITY_UID>";
|
|
448
|
+
if (!isManager)
|
|
449
|
+
placeholders.push("<uid> — identity user name or 'manager'");
|
|
450
|
+
if (parsed.potentialAttributes[0]) {
|
|
451
|
+
transform.attributes.attributeName = parsed.potentialAttributes[0];
|
|
452
|
+
}
|
|
453
|
+
else {
|
|
454
|
+
transform.attributes.attributeName = "<ATTRIBUTE_NAME>";
|
|
455
|
+
placeholders.push("<attributeName>");
|
|
456
|
+
}
|
|
457
|
+
break;
|
|
458
|
+
}
|
|
459
|
+
case "firstValid": {
|
|
460
|
+
if (parsed.potentialAttributes.length > 0) {
|
|
461
|
+
const vals = parsed.potentialAttributes.map((a) => ({
|
|
462
|
+
type: "identityAttribute",
|
|
463
|
+
attributes: { name: a },
|
|
464
|
+
}));
|
|
465
|
+
// Add a static fallback if there are quoted strings
|
|
466
|
+
if (parsed.quotedStrings[0]) {
|
|
467
|
+
vals.push({ type: "static", attributes: { value: parsed.quotedStrings[0] } });
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
vals.push({ type: "static", attributes: { value: "UNKNOWN" } });
|
|
471
|
+
placeholders.push("<fallback value> in last values entry");
|
|
472
|
+
}
|
|
473
|
+
transform.attributes.values = vals;
|
|
474
|
+
}
|
|
475
|
+
else {
|
|
476
|
+
placeholders.push("<values array with ordered fallbacks>");
|
|
477
|
+
}
|
|
478
|
+
break;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return { transform, placeholders };
|
|
482
|
+
}
|
|
483
|
+
// ---------------------------------------------------------------------------
|
|
484
|
+
// Main export
|
|
485
|
+
// ---------------------------------------------------------------------------
|
|
486
|
+
const DOCS_BASE = "https://developer.sailpoint.com/docs/extensibility/transforms/operations";
|
|
487
|
+
const TYPE_TO_DOC_SLUG = {
|
|
488
|
+
accountAttribute: "account-attribute", base64Decode: "base64-decode",
|
|
489
|
+
base64Encode: "base64-encode", concat: "concatenation", conditional: "conditional",
|
|
490
|
+
dateCompare: "date-compare", dateFormat: "date-format", dateMath: "date-math",
|
|
491
|
+
decomposeDiacriticalMarks: "decompose-diacritical-marks", displayName: "display-name",
|
|
492
|
+
e164phone: "e-164-phone", firstValid: "first-valid",
|
|
493
|
+
generateRandomString: "generate-random-string", getEndOfString: "get-end-of-string",
|
|
494
|
+
getReferenceIdentityAttribute: "get-reference-identity-attribute",
|
|
495
|
+
identityAttribute: "identity-attribute", indexOf: "index-of", iso3166: "iso3166",
|
|
496
|
+
join: "join", lastIndexOf: "last-index-of", leftPad: "left-pad", lookup: "lookup",
|
|
497
|
+
lower: "lower", normalizeNames: "name-normalizer", randomAlphaNumeric: "random-alphanumeric",
|
|
498
|
+
randomNumeric: "random-numeric", reference: "reference", replace: "replace",
|
|
499
|
+
replaceAll: "replace-all", rfc5646: "rfc5646", rightPad: "right-pad",
|
|
500
|
+
rule: "rule", split: "split", static: "static", substring: "substring",
|
|
501
|
+
trim: "trim", upper: "upper", usernameGenerator: "username-generator", uuid: "uuid-generator",
|
|
502
|
+
};
|
|
503
|
+
export function generateTransform(requirement, transformName) {
|
|
504
|
+
if (!requirement || typeof requirement !== "string" || requirement.trim().length === 0) {
|
|
505
|
+
throw new Error("requirement must be a non-empty string describing what the transform should do.");
|
|
506
|
+
}
|
|
507
|
+
const parsed = parseRequirement(requirement.trim());
|
|
508
|
+
const ranked = scoreOperations(parsed);
|
|
509
|
+
const warnings = [];
|
|
510
|
+
if (ranked.length === 0) {
|
|
511
|
+
warnings.push("No strong keyword match found. Defaulting to 'static' transform as a starting point.");
|
|
512
|
+
ranked.push({ type: "static", score: 1 });
|
|
513
|
+
}
|
|
514
|
+
const best = ranked[0];
|
|
515
|
+
const topScore = best.score;
|
|
516
|
+
const alternatives = ranked.slice(1, 4).map((r) => r.type);
|
|
517
|
+
const confidence = topScore >= 10 ? "high" : topScore >= 5 ? "medium" : "low";
|
|
518
|
+
if (confidence === "low") {
|
|
519
|
+
warnings.push(`Low confidence (score=${topScore}). Consider using isc.transforms.catalog to browse all operations and pick the right one manually.`);
|
|
520
|
+
}
|
|
521
|
+
const name = transformName ?? deriveTransformName(requirement, best.type);
|
|
522
|
+
const { transform, placeholders } = buildTransformJson(best.type, parsed, name);
|
|
523
|
+
const spec = TRANSFORM_CATALOG[best.type];
|
|
524
|
+
const docSlug = TYPE_TO_DOC_SLUG[best.type];
|
|
525
|
+
const doc_url = docSlug ? `${DOCS_BASE}/${docSlug}/` : `${DOCS_BASE}/`;
|
|
526
|
+
let explanation = `Selected operation: '${best.type}'`;
|
|
527
|
+
if (spec?.title)
|
|
528
|
+
explanation = `Selected '${spec.title}' (${best.type}) based on the requirement.`;
|
|
529
|
+
if (placeholders.length) {
|
|
530
|
+
explanation += ` Replace placeholder(s) before deploying: ${placeholders.join(", ")}.`;
|
|
531
|
+
}
|
|
532
|
+
if (alternatives.length) {
|
|
533
|
+
explanation += ` Alternatives to consider: ${alternatives.join(", ")}.`;
|
|
534
|
+
}
|
|
535
|
+
return {
|
|
536
|
+
transform,
|
|
537
|
+
operation_type: best.type,
|
|
538
|
+
confidence,
|
|
539
|
+
alternative_operations: alternatives,
|
|
540
|
+
doc_url,
|
|
541
|
+
explanation,
|
|
542
|
+
warnings,
|
|
543
|
+
placeholders,
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
function deriveTransformName(req, type) {
|
|
547
|
+
// Build a reasonable transform name from the requirement
|
|
548
|
+
const words = req.replace(/[^a-zA-Z0-9 ]/g, " ").split(/\s+/).filter(Boolean);
|
|
549
|
+
const capped = words.slice(0, 5).map((w) => w[0].toUpperCase() + w.slice(1)).join(" ");
|
|
550
|
+
return capped || `${type} Transform`;
|
|
551
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// src/transforms/index.ts
|
|
2
|
+
export * from "./catalog.js";
|
|
3
|
+
export * from "./normalize.js";
|
|
4
|
+
export * from "./lint.js";
|
|
5
|
+
export * from "./validate.js";
|
|
6
|
+
export * from "./generate.js";
|
|
7
|
+
export * from "./patterns.js";
|
|
8
|
+
export * from "./testcases.js";
|
|
9
|
+
export * from "./explain.js";
|