@aigne/afs-mapping 1.11.0-beta.6
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/LICENSE.md +26 -0
- package/README.md +286 -0
- package/dist/index.d.mts +3186 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +828 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +57 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,828 @@
|
|
|
1
|
+
import { readFile, stat } from "node:fs/promises";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { parse } from "yaml";
|
|
4
|
+
import { fromError } from "zod-validation-error";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
|
|
7
|
+
//#region src/binder.ts
|
|
8
|
+
/**
|
|
9
|
+
* Expression binder for evaluating binding expressions
|
|
10
|
+
*/
|
|
11
|
+
var ExpressionBinder = class {
|
|
12
|
+
/**
|
|
13
|
+
* Parse a binding expression string
|
|
14
|
+
*/
|
|
15
|
+
parseExpression(expr) {
|
|
16
|
+
const trimmed = expr.trim();
|
|
17
|
+
const parts = this.splitByPipe(trimmed);
|
|
18
|
+
const sourcePart = parts[0]?.trim() ?? "";
|
|
19
|
+
const pipes = parts.slice(1).map((p) => this.parsePipe(p.trim()));
|
|
20
|
+
if (sourcePart.startsWith("$")) return {
|
|
21
|
+
source: "jsonpath",
|
|
22
|
+
key: sourcePart,
|
|
23
|
+
pipes
|
|
24
|
+
};
|
|
25
|
+
const dotIndex = sourcePart.indexOf(".");
|
|
26
|
+
if (dotIndex === -1) return {
|
|
27
|
+
source: "literal",
|
|
28
|
+
key: sourcePart,
|
|
29
|
+
pipes
|
|
30
|
+
};
|
|
31
|
+
const source = sourcePart.slice(0, dotIndex);
|
|
32
|
+
const key = sourcePart.slice(dotIndex + 1);
|
|
33
|
+
if (source !== "path" && source !== "query" && source !== "input") return {
|
|
34
|
+
source: "literal",
|
|
35
|
+
key: sourcePart,
|
|
36
|
+
pipes
|
|
37
|
+
};
|
|
38
|
+
return {
|
|
39
|
+
source,
|
|
40
|
+
key,
|
|
41
|
+
pipes
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Split expression by pipe operator, respecting parentheses
|
|
46
|
+
*/
|
|
47
|
+
splitByPipe(expr) {
|
|
48
|
+
const parts = [];
|
|
49
|
+
let current = "";
|
|
50
|
+
let parenDepth = 0;
|
|
51
|
+
for (let i = 0; i < expr.length; i++) {
|
|
52
|
+
const char = expr[i];
|
|
53
|
+
if (char === "(") {
|
|
54
|
+
parenDepth++;
|
|
55
|
+
current += char;
|
|
56
|
+
} else if (char === ")") {
|
|
57
|
+
parenDepth--;
|
|
58
|
+
current += char;
|
|
59
|
+
} else if (char === "|" && parenDepth === 0) {
|
|
60
|
+
parts.push(current);
|
|
61
|
+
current = "";
|
|
62
|
+
} else current += char;
|
|
63
|
+
}
|
|
64
|
+
if (current) parts.push(current);
|
|
65
|
+
return parts;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Parse a pipe operator string like "default('open')" or "int"
|
|
69
|
+
*/
|
|
70
|
+
parsePipe(pipeStr) {
|
|
71
|
+
const parenIndex = pipeStr.indexOf("(");
|
|
72
|
+
if (parenIndex === -1) return {
|
|
73
|
+
name: pipeStr,
|
|
74
|
+
args: []
|
|
75
|
+
};
|
|
76
|
+
const name = pipeStr.slice(0, parenIndex);
|
|
77
|
+
const argsStr = pipeStr.slice(parenIndex + 1, -1);
|
|
78
|
+
if (!argsStr) return {
|
|
79
|
+
name,
|
|
80
|
+
args: []
|
|
81
|
+
};
|
|
82
|
+
return {
|
|
83
|
+
name,
|
|
84
|
+
args: this.parseArgs(argsStr)
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Parse comma-separated arguments, handling quoted strings and numbers
|
|
89
|
+
*/
|
|
90
|
+
parseArgs(argsStr) {
|
|
91
|
+
const args = [];
|
|
92
|
+
let current = "";
|
|
93
|
+
let inString = false;
|
|
94
|
+
let stringChar = "";
|
|
95
|
+
for (let i = 0; i < argsStr.length; i++) {
|
|
96
|
+
const char = argsStr[i];
|
|
97
|
+
if (!inString && (char === "\"" || char === "'")) {
|
|
98
|
+
inString = true;
|
|
99
|
+
stringChar = char;
|
|
100
|
+
} else if (inString && char === stringChar) {
|
|
101
|
+
inString = false;
|
|
102
|
+
args.push(current);
|
|
103
|
+
current = "";
|
|
104
|
+
if (argsStr[i + 1] === ",") i++;
|
|
105
|
+
} else if (!inString && char === ",") {
|
|
106
|
+
if (current.trim()) args.push(this.parseValue(current.trim()));
|
|
107
|
+
current = "";
|
|
108
|
+
} else current += char;
|
|
109
|
+
}
|
|
110
|
+
if (current.trim() && !inString) args.push(this.parseValue(current.trim()));
|
|
111
|
+
return args;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Parse a value (number or string)
|
|
115
|
+
*/
|
|
116
|
+
parseValue(value) {
|
|
117
|
+
const num = Number(value);
|
|
118
|
+
if (!Number.isNaN(num)) return num;
|
|
119
|
+
return value;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Evaluate a binding expression against a context
|
|
123
|
+
*/
|
|
124
|
+
evaluate(expr, context) {
|
|
125
|
+
const parsed = this.parseExpression(expr);
|
|
126
|
+
let value;
|
|
127
|
+
switch (parsed.source) {
|
|
128
|
+
case "path":
|
|
129
|
+
value = this.getNestedValue(context.path, parsed.key);
|
|
130
|
+
break;
|
|
131
|
+
case "query":
|
|
132
|
+
value = this.getNestedValue(context.query, parsed.key);
|
|
133
|
+
break;
|
|
134
|
+
case "input":
|
|
135
|
+
value = this.getNestedValue(context.input, parsed.key);
|
|
136
|
+
break;
|
|
137
|
+
case "jsonpath":
|
|
138
|
+
value = this.evaluateJsonPath(parsed.key, context.data);
|
|
139
|
+
break;
|
|
140
|
+
case "literal":
|
|
141
|
+
value = parsed.key;
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
for (const pipe of parsed.pipes) value = this.applyPipe(value, pipe);
|
|
145
|
+
return value;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Get a nested value from an object using dot notation
|
|
149
|
+
*/
|
|
150
|
+
getNestedValue(obj, key) {
|
|
151
|
+
const parts = key.split(".");
|
|
152
|
+
let current = obj;
|
|
153
|
+
for (const part of parts) {
|
|
154
|
+
if (current === null || current === void 0) return;
|
|
155
|
+
if (typeof current !== "object") return;
|
|
156
|
+
current = current[part];
|
|
157
|
+
}
|
|
158
|
+
return current;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Evaluate a simple JSONPath expression
|
|
162
|
+
* Note: This is a simplified implementation supporting basic paths
|
|
163
|
+
*/
|
|
164
|
+
evaluateJsonPath(jsonPath, data) {
|
|
165
|
+
if (!jsonPath.startsWith("$")) throw new Error(`Invalid JSONPath: ${jsonPath}`);
|
|
166
|
+
const path = jsonPath.slice(1);
|
|
167
|
+
if (!path || path === ".") return data;
|
|
168
|
+
const parts = this.parseJsonPathParts(path);
|
|
169
|
+
let current = data;
|
|
170
|
+
for (const part of parts) {
|
|
171
|
+
if (current === null || current === void 0) return;
|
|
172
|
+
if (part === "[*]") continue;
|
|
173
|
+
if (typeof current !== "object") return;
|
|
174
|
+
current = current[part];
|
|
175
|
+
}
|
|
176
|
+
return current;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Parse JSONPath parts like ".user.login" or "[0].name"
|
|
180
|
+
*/
|
|
181
|
+
parseJsonPathParts(path) {
|
|
182
|
+
const parts = [];
|
|
183
|
+
let current = "";
|
|
184
|
+
for (let i = 0; i < path.length; i++) {
|
|
185
|
+
const char = path[i];
|
|
186
|
+
if (char === ".") {
|
|
187
|
+
if (current) parts.push(current);
|
|
188
|
+
current = "";
|
|
189
|
+
} else if (char === "[") {
|
|
190
|
+
if (current) parts.push(current);
|
|
191
|
+
current = "";
|
|
192
|
+
const endBracket = path.indexOf("]", i);
|
|
193
|
+
if (endBracket === -1) throw new Error(`Unmatched bracket in JSONPath: ${path}`);
|
|
194
|
+
const bracketContent = path.slice(i, endBracket + 1);
|
|
195
|
+
parts.push(bracketContent);
|
|
196
|
+
i = endBracket;
|
|
197
|
+
} else current += char;
|
|
198
|
+
}
|
|
199
|
+
if (current) parts.push(current);
|
|
200
|
+
return parts;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Apply a pipe operator to a value
|
|
204
|
+
*/
|
|
205
|
+
applyPipe(value, pipe) {
|
|
206
|
+
switch (pipe.name) {
|
|
207
|
+
case "default": return value === void 0 || value === null ? pipe.args[0] : value;
|
|
208
|
+
case "int":
|
|
209
|
+
if (value === void 0 || value === null) return void 0;
|
|
210
|
+
return parseInt(String(value), 10);
|
|
211
|
+
case "string":
|
|
212
|
+
if (value === void 0 || value === null) return void 0;
|
|
213
|
+
return String(value);
|
|
214
|
+
case "bool": return Boolean(value);
|
|
215
|
+
case "json": return JSON.stringify(value);
|
|
216
|
+
default: return value;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Bind multiple expressions and return result object
|
|
221
|
+
*/
|
|
222
|
+
bindAll(bindings, context) {
|
|
223
|
+
const result = {};
|
|
224
|
+
for (const [key, expr] of Object.entries(bindings)) result[key] = this.evaluate(expr, context);
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
//#endregion
|
|
230
|
+
//#region src/schema.ts
|
|
231
|
+
/**
|
|
232
|
+
* Mapping DSL Schema Definitions
|
|
233
|
+
*
|
|
234
|
+
* Zod schemas for validating mapping configuration files.
|
|
235
|
+
* The DSL allows declarative mapping of AFS paths to external API calls.
|
|
236
|
+
*/
|
|
237
|
+
/**
|
|
238
|
+
* Transform entry mapping - how to convert API response fields to AFS entry fields
|
|
239
|
+
*/
|
|
240
|
+
const transformEntrySchema = z.object({
|
|
241
|
+
id: z.string().optional(),
|
|
242
|
+
path: z.string().optional(),
|
|
243
|
+
summary: z.string().optional(),
|
|
244
|
+
description: z.string().optional(),
|
|
245
|
+
content: z.string().optional(),
|
|
246
|
+
metadata: z.record(z.any()).optional()
|
|
247
|
+
}).passthrough();
|
|
248
|
+
/**
|
|
249
|
+
* Transform configuration - how to convert API response to AFS entries
|
|
250
|
+
*/
|
|
251
|
+
const transformSchema = z.object({
|
|
252
|
+
items: z.string().optional(),
|
|
253
|
+
entry: transformEntrySchema.optional()
|
|
254
|
+
});
|
|
255
|
+
/**
|
|
256
|
+
* Operation schema - a single API operation (list, read, write, etc.)
|
|
257
|
+
*/
|
|
258
|
+
const operationSchema = z.object({
|
|
259
|
+
type: z.enum([
|
|
260
|
+
"http",
|
|
261
|
+
"graphql",
|
|
262
|
+
"mcp-tool"
|
|
263
|
+
]).optional(),
|
|
264
|
+
method: z.enum([
|
|
265
|
+
"GET",
|
|
266
|
+
"POST",
|
|
267
|
+
"PUT",
|
|
268
|
+
"PATCH",
|
|
269
|
+
"DELETE"
|
|
270
|
+
]).optional(),
|
|
271
|
+
path: z.string().optional(),
|
|
272
|
+
params: z.record(z.string()).optional(),
|
|
273
|
+
body: z.record(z.string()).optional(),
|
|
274
|
+
headers: z.record(z.string()).optional(),
|
|
275
|
+
query: z.string().optional(),
|
|
276
|
+
variables: z.record(z.string()).optional(),
|
|
277
|
+
tool: z.string().optional(),
|
|
278
|
+
transform: transformSchema.optional()
|
|
279
|
+
});
|
|
280
|
+
/**
|
|
281
|
+
* Route definition - operations for a specific path pattern
|
|
282
|
+
*/
|
|
283
|
+
const routeDefinitionSchema = z.object({
|
|
284
|
+
list: operationSchema.optional(),
|
|
285
|
+
read: operationSchema.optional(),
|
|
286
|
+
write: operationSchema.optional(),
|
|
287
|
+
create: operationSchema.optional(),
|
|
288
|
+
delete: operationSchema.optional()
|
|
289
|
+
});
|
|
290
|
+
/**
|
|
291
|
+
* Default configuration applied to all operations
|
|
292
|
+
*/
|
|
293
|
+
const defaultsSchema = z.object({
|
|
294
|
+
baseUrl: z.string().optional(),
|
|
295
|
+
headers: z.record(z.string()).optional(),
|
|
296
|
+
timeout: z.number().optional()
|
|
297
|
+
});
|
|
298
|
+
/**
|
|
299
|
+
* Root mapping configuration schema
|
|
300
|
+
*/
|
|
301
|
+
const mappingConfigSchema = z.object({
|
|
302
|
+
name: z.string(),
|
|
303
|
+
version: z.string(),
|
|
304
|
+
description: z.string().optional(),
|
|
305
|
+
defaults: defaultsSchema.optional(),
|
|
306
|
+
include: z.array(z.string()).optional(),
|
|
307
|
+
routes: z.record(routeDefinitionSchema)
|
|
308
|
+
});
|
|
309
|
+
/**
|
|
310
|
+
* Partial mapping config for included files (no name/version required)
|
|
311
|
+
*/
|
|
312
|
+
const partialMappingConfigSchema = z.object({
|
|
313
|
+
routes: z.record(routeDefinitionSchema).optional(),
|
|
314
|
+
defaults: defaultsSchema.optional()
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
//#endregion
|
|
318
|
+
//#region src/parser.ts
|
|
319
|
+
/**
|
|
320
|
+
* YAML Parser for Mapping Configuration
|
|
321
|
+
*
|
|
322
|
+
* Parses YAML mapping files, handles includes, and validates against schema.
|
|
323
|
+
*/
|
|
324
|
+
/**
|
|
325
|
+
* Error thrown when parsing fails
|
|
326
|
+
*/
|
|
327
|
+
var MappingParseError = class extends Error {
|
|
328
|
+
constructor(message, filePath, cause) {
|
|
329
|
+
super(filePath ? `${message} (in ${filePath})` : message);
|
|
330
|
+
this.filePath = filePath;
|
|
331
|
+
this.cause = cause;
|
|
332
|
+
this.name = "MappingParseError";
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
/**
|
|
336
|
+
* Parser for mapping configuration files
|
|
337
|
+
*/
|
|
338
|
+
var MappingParser = class {
|
|
339
|
+
/**
|
|
340
|
+
* Parse a YAML string into a validated mapping config
|
|
341
|
+
*/
|
|
342
|
+
parseString(yaml, filePath) {
|
|
343
|
+
let parsed;
|
|
344
|
+
try {
|
|
345
|
+
parsed = parse(yaml);
|
|
346
|
+
} catch (e) {
|
|
347
|
+
throw new MappingParseError(`Invalid YAML: ${e instanceof Error ? e.message : String(e)}`, filePath);
|
|
348
|
+
}
|
|
349
|
+
const result = mappingConfigSchema.safeParse(parsed);
|
|
350
|
+
if (!result.success) throw new MappingParseError(`Schema validation failed: ${fromError(result.error).message}`, filePath);
|
|
351
|
+
return result.data;
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Parse a partial config string (for included files)
|
|
355
|
+
*/
|
|
356
|
+
parsePartialString(yaml, filePath) {
|
|
357
|
+
let parsed;
|
|
358
|
+
try {
|
|
359
|
+
parsed = parse(yaml);
|
|
360
|
+
} catch (e) {
|
|
361
|
+
throw new MappingParseError(`Invalid YAML: ${e instanceof Error ? e.message : String(e)}`, filePath);
|
|
362
|
+
}
|
|
363
|
+
const result = partialMappingConfigSchema.safeParse(parsed);
|
|
364
|
+
if (!result.success) throw new MappingParseError(`Schema validation failed: ${fromError(result.error).message}`, filePath);
|
|
365
|
+
return result.data;
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Parse a mapping file and resolve includes
|
|
369
|
+
*/
|
|
370
|
+
async parseFile(filePath, visitedPaths = /* @__PURE__ */ new Set()) {
|
|
371
|
+
const absolutePath = resolve(filePath);
|
|
372
|
+
if (visitedPaths.has(absolutePath)) throw new MappingParseError(`Circular include detected: ${absolutePath}`, filePath);
|
|
373
|
+
visitedPaths.add(absolutePath);
|
|
374
|
+
let content;
|
|
375
|
+
try {
|
|
376
|
+
content = await readFile(absolutePath, "utf-8");
|
|
377
|
+
} catch (e) {
|
|
378
|
+
throw new MappingParseError(`Failed to read file: ${e instanceof Error ? e.message : String(e)}`, absolutePath);
|
|
379
|
+
}
|
|
380
|
+
const config = this.parseString(content, absolutePath);
|
|
381
|
+
if (config.include && config.include.length > 0) {
|
|
382
|
+
const baseDir = dirname(absolutePath);
|
|
383
|
+
for (const includePath of config.include) {
|
|
384
|
+
const includeAbsPath = resolve(baseDir, includePath);
|
|
385
|
+
if (visitedPaths.has(includeAbsPath)) throw new MappingParseError(`Circular include detected: ${includeAbsPath}`, filePath);
|
|
386
|
+
const partial = await this.parsePartialFile(includeAbsPath, visitedPaths);
|
|
387
|
+
if (partial.routes) config.routes = {
|
|
388
|
+
...config.routes,
|
|
389
|
+
...partial.routes
|
|
390
|
+
};
|
|
391
|
+
if (partial.defaults) config.defaults = {
|
|
392
|
+
...partial.defaults,
|
|
393
|
+
...config.defaults,
|
|
394
|
+
headers: {
|
|
395
|
+
...partial.defaults.headers,
|
|
396
|
+
...config.defaults?.headers
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return config;
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Parse a partial mapping file (for includes)
|
|
405
|
+
*/
|
|
406
|
+
async parsePartialFile(filePath, visitedPaths = /* @__PURE__ */ new Set()) {
|
|
407
|
+
const absolutePath = resolve(filePath);
|
|
408
|
+
if (visitedPaths.has(absolutePath)) throw new MappingParseError(`Circular include detected: ${absolutePath}`, filePath);
|
|
409
|
+
visitedPaths.add(absolutePath);
|
|
410
|
+
let content;
|
|
411
|
+
try {
|
|
412
|
+
content = await readFile(absolutePath, "utf-8");
|
|
413
|
+
} catch (e) {
|
|
414
|
+
throw new MappingParseError(`Failed to read file: ${e instanceof Error ? e.message : String(e)}`, absolutePath);
|
|
415
|
+
}
|
|
416
|
+
const partial = this.parsePartialString(content, absolutePath);
|
|
417
|
+
const parsed = parse(content);
|
|
418
|
+
if (parsed.include && parsed.include.length > 0) {
|
|
419
|
+
const baseDir = dirname(absolutePath);
|
|
420
|
+
for (const includePath of parsed.include) {
|
|
421
|
+
const includeAbsPath = resolve(baseDir, includePath);
|
|
422
|
+
if (visitedPaths.has(includeAbsPath)) throw new MappingParseError(`Circular include detected: ${includeAbsPath}`, filePath);
|
|
423
|
+
const nestedPartial = await this.parsePartialFile(includeAbsPath, visitedPaths);
|
|
424
|
+
if (nestedPartial.routes) partial.routes = {
|
|
425
|
+
...partial.routes,
|
|
426
|
+
...nestedPartial.routes
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return partial;
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Parse a directory containing index.yaml and potentially included files
|
|
434
|
+
*/
|
|
435
|
+
async parseDirectory(dirPath) {
|
|
436
|
+
const absoluteDir = resolve(dirPath);
|
|
437
|
+
const indexPath = join(absoluteDir, "index.yaml");
|
|
438
|
+
try {
|
|
439
|
+
if (!(await stat(indexPath)).isFile()) throw new MappingParseError(`index.yaml is not a file`, indexPath);
|
|
440
|
+
} catch (e) {
|
|
441
|
+
if (e.code === "ENOENT") throw new MappingParseError(`Directory does not contain index.yaml`, absoluteDir);
|
|
442
|
+
throw e;
|
|
443
|
+
}
|
|
444
|
+
return this.parseFile(indexPath);
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
//#endregion
|
|
449
|
+
//#region src/projector.ts
|
|
450
|
+
/**
|
|
451
|
+
* Projects API response data to AFS entries
|
|
452
|
+
*/
|
|
453
|
+
var Projector = class {
|
|
454
|
+
binder = new ExpressionBinder();
|
|
455
|
+
/**
|
|
456
|
+
* Project data to AFS entries based on transform config
|
|
457
|
+
*/
|
|
458
|
+
project(data, transform, pathParams) {
|
|
459
|
+
if (transform.items) return this.projectList(data, transform, pathParams);
|
|
460
|
+
if (transform.entry) return [this.projectItem(data, transform, pathParams)];
|
|
461
|
+
return [];
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Project a list of items
|
|
465
|
+
*/
|
|
466
|
+
projectList(data, transform, pathParams) {
|
|
467
|
+
if (!transform.entry) return [];
|
|
468
|
+
let items;
|
|
469
|
+
if (transform.items === "$") {
|
|
470
|
+
if (!Array.isArray(data)) return [];
|
|
471
|
+
items = data;
|
|
472
|
+
} else {
|
|
473
|
+
const itemsPath = transform.items;
|
|
474
|
+
const itemsData = this.evaluateJsonPath(itemsPath, data);
|
|
475
|
+
if (itemsData === void 0 || itemsData === null) return [];
|
|
476
|
+
if (Array.isArray(itemsData)) items = itemsData;
|
|
477
|
+
else items = [itemsData];
|
|
478
|
+
}
|
|
479
|
+
return items.map((item) => this.projectItem(item, { entry: transform.entry }, pathParams));
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Project a single item to an AFS entry
|
|
483
|
+
*/
|
|
484
|
+
projectItem(data, transform, pathParams) {
|
|
485
|
+
if (!transform.entry) throw new Error("Transform must have entry configuration");
|
|
486
|
+
const entryConfig = transform.entry;
|
|
487
|
+
const entry = {
|
|
488
|
+
id: this.evaluateField(entryConfig.id, data, pathParams) ?? "",
|
|
489
|
+
path: this.interpolatePath(entryConfig.path ?? "", data, pathParams)
|
|
490
|
+
};
|
|
491
|
+
if (entryConfig.summary) {
|
|
492
|
+
const summary = this.evaluateField(entryConfig.summary, data, pathParams);
|
|
493
|
+
if (summary !== void 0) entry.summary = summary;
|
|
494
|
+
}
|
|
495
|
+
if (entryConfig.content) {
|
|
496
|
+
const content = this.evaluateField(entryConfig.content, data, pathParams);
|
|
497
|
+
if (content !== void 0) entry.content = content;
|
|
498
|
+
}
|
|
499
|
+
if (entryConfig.metadata) entry.metadata = this.projectMetadata(entryConfig.metadata, data, pathParams);
|
|
500
|
+
if (entryConfig.description) {
|
|
501
|
+
const description = this.evaluateField(entryConfig.description, data, pathParams);
|
|
502
|
+
if (description !== void 0) {
|
|
503
|
+
entry.metadata = entry.metadata || {};
|
|
504
|
+
entry.metadata.description = description;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return entry;
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Evaluate a single field expression
|
|
511
|
+
*/
|
|
512
|
+
evaluateField(expr, data, pathParams) {
|
|
513
|
+
if (!expr) return void 0;
|
|
514
|
+
const context = {
|
|
515
|
+
path: pathParams,
|
|
516
|
+
query: {},
|
|
517
|
+
input: {},
|
|
518
|
+
data
|
|
519
|
+
};
|
|
520
|
+
const result = this.binder.evaluate(expr, context);
|
|
521
|
+
return result === void 0 || result === null ? void 0 : String(result);
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Interpolate a path template with data and path params
|
|
525
|
+
* Handles both {name} for path params and {$.path} for JSONPath
|
|
526
|
+
*/
|
|
527
|
+
interpolatePath(template, data, pathParams) {
|
|
528
|
+
return template.replace(/\{([^}]+)\}/g, (match, expr) => {
|
|
529
|
+
if (expr.startsWith("$.")) {
|
|
530
|
+
const value = this.evaluateJsonPath(expr, data);
|
|
531
|
+
return value !== void 0 && value !== null ? String(value) : match;
|
|
532
|
+
}
|
|
533
|
+
return pathParams[expr] ?? match;
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Project metadata object recursively
|
|
538
|
+
*/
|
|
539
|
+
projectMetadata(config, data, pathParams) {
|
|
540
|
+
const result = {};
|
|
541
|
+
for (const [key, value] of Object.entries(config)) if (typeof value === "string") {
|
|
542
|
+
const context = {
|
|
543
|
+
path: pathParams,
|
|
544
|
+
query: {},
|
|
545
|
+
input: {},
|
|
546
|
+
data
|
|
547
|
+
};
|
|
548
|
+
result[key] = this.binder.evaluate(value, context);
|
|
549
|
+
} else if (typeof value === "object" && value !== null && !Array.isArray(value)) result[key] = this.projectMetadata(value, data, pathParams);
|
|
550
|
+
else result[key] = value;
|
|
551
|
+
return result;
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Evaluate a JSONPath expression
|
|
555
|
+
*/
|
|
556
|
+
evaluateJsonPath(jsonPath, data) {
|
|
557
|
+
if (!jsonPath) return void 0;
|
|
558
|
+
const context = {
|
|
559
|
+
path: {},
|
|
560
|
+
query: {},
|
|
561
|
+
input: {},
|
|
562
|
+
data
|
|
563
|
+
};
|
|
564
|
+
return this.binder.evaluate(jsonPath, context);
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
//#endregion
|
|
569
|
+
//#region src/resolver.ts
|
|
570
|
+
/**
|
|
571
|
+
* Represents a parsed path template
|
|
572
|
+
*/
|
|
573
|
+
var PathTemplate = class PathTemplate {
|
|
574
|
+
/** Original template string */
|
|
575
|
+
template;
|
|
576
|
+
/** Parsed segments */
|
|
577
|
+
segments;
|
|
578
|
+
/** Parameter names in order */
|
|
579
|
+
params;
|
|
580
|
+
constructor(template, segments, params) {
|
|
581
|
+
this.template = template;
|
|
582
|
+
this.segments = segments;
|
|
583
|
+
this.params = params;
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Parse a path template string
|
|
587
|
+
*/
|
|
588
|
+
static parse(template) {
|
|
589
|
+
const segments = (template.endsWith("/") && template !== "/" ? template.slice(0, -1) : template).split("/").filter(Boolean);
|
|
590
|
+
const params = [];
|
|
591
|
+
for (const segment of segments) if (segment.startsWith("{") && segment.endsWith("}")) {
|
|
592
|
+
const paramName = segment.slice(1, -1);
|
|
593
|
+
params.push(paramName);
|
|
594
|
+
}
|
|
595
|
+
return new PathTemplate(template, segments, params);
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Try to match a path against this template
|
|
599
|
+
*/
|
|
600
|
+
match(path) {
|
|
601
|
+
const pathSegments = (path.endsWith("/") && path !== "/" ? path.slice(0, -1) : path).split("/").filter(Boolean);
|
|
602
|
+
if (pathSegments.length !== this.segments.length) return null;
|
|
603
|
+
const params = {};
|
|
604
|
+
for (let i = 0; i < this.segments.length; i++) {
|
|
605
|
+
const templateSeg = this.segments[i];
|
|
606
|
+
const pathSeg = decodeURIComponent(pathSegments[i]);
|
|
607
|
+
if (templateSeg.startsWith("{") && templateSeg.endsWith("}")) {
|
|
608
|
+
const paramName = templateSeg.slice(1, -1);
|
|
609
|
+
params[paramName] = pathSeg;
|
|
610
|
+
} else if (templateSeg !== pathSeg) return null;
|
|
611
|
+
}
|
|
612
|
+
return { params };
|
|
613
|
+
}
|
|
614
|
+
};
|
|
615
|
+
/**
|
|
616
|
+
* Path resolver using Trie for O(k) lookup where k = path depth
|
|
617
|
+
*/
|
|
618
|
+
var PathResolver = class {
|
|
619
|
+
root;
|
|
620
|
+
constructor() {
|
|
621
|
+
this.root = this.createNode(false);
|
|
622
|
+
}
|
|
623
|
+
createNode(isParam, paramName) {
|
|
624
|
+
return {
|
|
625
|
+
children: /* @__PURE__ */ new Map(),
|
|
626
|
+
isParam,
|
|
627
|
+
paramName
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Add a route template with associated data
|
|
632
|
+
*/
|
|
633
|
+
addRoute(template, data) {
|
|
634
|
+
const parsed = PathTemplate.parse(template);
|
|
635
|
+
let current = this.root;
|
|
636
|
+
for (const segment of parsed.segments) {
|
|
637
|
+
const isParam = segment.startsWith("{") && segment.endsWith("}");
|
|
638
|
+
const key = isParam ? "{param}" : segment;
|
|
639
|
+
if (!current.children.has(key)) {
|
|
640
|
+
const paramName = isParam ? segment.slice(1, -1) : void 0;
|
|
641
|
+
current.children.set(key, this.createNode(isParam, paramName));
|
|
642
|
+
}
|
|
643
|
+
current = current.children.get(key);
|
|
644
|
+
}
|
|
645
|
+
current.data = data;
|
|
646
|
+
current.template = template;
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Resolve a path to a route
|
|
650
|
+
*/
|
|
651
|
+
resolve(path) {
|
|
652
|
+
const segments = (path.endsWith("/") && path !== "/" ? path.slice(0, -1) : path).split("/").filter(Boolean);
|
|
653
|
+
const params = {};
|
|
654
|
+
let current = this.root;
|
|
655
|
+
for (const segment of segments) {
|
|
656
|
+
const decodedSegment = decodeURIComponent(segment);
|
|
657
|
+
if (current.children.has(decodedSegment)) current = current.children.get(decodedSegment);
|
|
658
|
+
else if (current.children.has("{param}")) {
|
|
659
|
+
const paramNode = current.children.get("{param}");
|
|
660
|
+
if (paramNode.paramName) params[paramNode.paramName] = decodedSegment;
|
|
661
|
+
current = paramNode;
|
|
662
|
+
} else return null;
|
|
663
|
+
}
|
|
664
|
+
if (current.data === void 0 || current.template === void 0) return null;
|
|
665
|
+
return {
|
|
666
|
+
template: current.template,
|
|
667
|
+
params,
|
|
668
|
+
data: current.data
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Get all registered routes
|
|
673
|
+
*/
|
|
674
|
+
getAllRoutes() {
|
|
675
|
+
const routes = [];
|
|
676
|
+
this.collectRoutes(this.root, routes);
|
|
677
|
+
return routes;
|
|
678
|
+
}
|
|
679
|
+
collectRoutes(node, routes) {
|
|
680
|
+
if (node.data !== void 0 && node.template !== void 0) routes.push({
|
|
681
|
+
template: node.template,
|
|
682
|
+
data: node.data
|
|
683
|
+
});
|
|
684
|
+
for (const child of node.children.values()) this.collectRoutes(child, routes);
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
//#endregion
|
|
689
|
+
//#region src/compiler.ts
|
|
690
|
+
/**
|
|
691
|
+
* Compiled mapping ready for runtime use
|
|
692
|
+
*/
|
|
693
|
+
var CompiledMapping = class {
|
|
694
|
+
name;
|
|
695
|
+
version;
|
|
696
|
+
description;
|
|
697
|
+
defaults;
|
|
698
|
+
resolver;
|
|
699
|
+
binder = new ExpressionBinder();
|
|
700
|
+
projector = new Projector();
|
|
701
|
+
routes = /* @__PURE__ */ new Map();
|
|
702
|
+
constructor(config) {
|
|
703
|
+
this.name = config.name;
|
|
704
|
+
this.version = config.version;
|
|
705
|
+
this.description = config.description;
|
|
706
|
+
this.defaults = config.defaults;
|
|
707
|
+
this.resolver = new PathResolver();
|
|
708
|
+
for (const [template, routeDef] of Object.entries(config.routes)) {
|
|
709
|
+
this.resolver.addRoute(template, routeDef);
|
|
710
|
+
this.routes.set(template, routeDef);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Get number of routes
|
|
715
|
+
*/
|
|
716
|
+
get routeCount() {
|
|
717
|
+
return this.routes.size;
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Get total number of operations
|
|
721
|
+
*/
|
|
722
|
+
get operationCount() {
|
|
723
|
+
let count = 0;
|
|
724
|
+
for (const route of this.routes.values()) {
|
|
725
|
+
if (route.list) count++;
|
|
726
|
+
if (route.read) count++;
|
|
727
|
+
if (route.write) count++;
|
|
728
|
+
if (route.create) count++;
|
|
729
|
+
if (route.delete) count++;
|
|
730
|
+
}
|
|
731
|
+
return count;
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Resolve a path to route information
|
|
735
|
+
*/
|
|
736
|
+
resolve(path) {
|
|
737
|
+
const result = this.resolver.resolve(path);
|
|
738
|
+
if (!result) return null;
|
|
739
|
+
return {
|
|
740
|
+
template: result.template,
|
|
741
|
+
params: result.params,
|
|
742
|
+
operations: {
|
|
743
|
+
list: result.data.list,
|
|
744
|
+
read: result.data.read,
|
|
745
|
+
write: result.data.write,
|
|
746
|
+
create: result.data.create,
|
|
747
|
+
delete: result.data.delete
|
|
748
|
+
}
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Build an HTTP request for a given path and operation
|
|
753
|
+
*/
|
|
754
|
+
buildRequest(afsPath, operationType, options) {
|
|
755
|
+
const resolved = this.resolve(afsPath);
|
|
756
|
+
if (!resolved) return null;
|
|
757
|
+
const operation = resolved.operations[operationType];
|
|
758
|
+
if (!operation) return null;
|
|
759
|
+
const context = {
|
|
760
|
+
path: resolved.params,
|
|
761
|
+
query: options.query ?? {},
|
|
762
|
+
input: options.input ?? {},
|
|
763
|
+
data: {}
|
|
764
|
+
};
|
|
765
|
+
const boundParams = operation.params ? this.binder.bindAll(operation.params, context) : {};
|
|
766
|
+
const apiPath = this.interpolatePath(operation.path ?? "", context);
|
|
767
|
+
const headers = {
|
|
768
|
+
...this.defaults?.headers,
|
|
769
|
+
...operation.headers
|
|
770
|
+
};
|
|
771
|
+
let body;
|
|
772
|
+
if (operation.body) body = this.binder.bindAll(operation.body, context);
|
|
773
|
+
return {
|
|
774
|
+
method: operation.method ?? "GET",
|
|
775
|
+
path: apiPath,
|
|
776
|
+
params: boundParams,
|
|
777
|
+
headers: Object.keys(headers).length > 0 ? headers : void 0,
|
|
778
|
+
body
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Project API response to AFS entries
|
|
783
|
+
*/
|
|
784
|
+
projectResponse(afsPath, operationType, apiResponse) {
|
|
785
|
+
const resolved = this.resolve(afsPath);
|
|
786
|
+
if (!resolved) return [];
|
|
787
|
+
const operation = resolved.operations[operationType];
|
|
788
|
+
if (!operation?.transform) return [];
|
|
789
|
+
return this.projector.project(apiResponse, operation.transform, resolved.params);
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* Interpolate path template with context values
|
|
793
|
+
*/
|
|
794
|
+
interpolatePath(template, context) {
|
|
795
|
+
return template.replace(/\{([^}]+)\}/g, (match, key) => {
|
|
796
|
+
const value = context.path[key];
|
|
797
|
+
return value !== void 0 ? value : match;
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
};
|
|
801
|
+
/**
|
|
802
|
+
* Compiler for mapping configurations
|
|
803
|
+
*/
|
|
804
|
+
var MappingCompiler = class {
|
|
805
|
+
parser = new MappingParser();
|
|
806
|
+
/**
|
|
807
|
+
* Compile a mapping file
|
|
808
|
+
*/
|
|
809
|
+
async compileFile(filePath) {
|
|
810
|
+
return new CompiledMapping(await this.parser.parseFile(filePath));
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
813
|
+
* Compile a mapping directory
|
|
814
|
+
*/
|
|
815
|
+
async compileDirectory(dirPath) {
|
|
816
|
+
return new CompiledMapping(await this.parser.parseDirectory(dirPath));
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* Compile from a config object (for testing)
|
|
820
|
+
*/
|
|
821
|
+
compileConfig(config) {
|
|
822
|
+
return new CompiledMapping(config);
|
|
823
|
+
}
|
|
824
|
+
};
|
|
825
|
+
|
|
826
|
+
//#endregion
|
|
827
|
+
export { CompiledMapping, ExpressionBinder, MappingCompiler, MappingParseError, MappingParser, PathResolver, PathTemplate, Projector, defaultsSchema, mappingConfigSchema, operationSchema, partialMappingConfigSchema, routeDefinitionSchema, transformEntrySchema, transformSchema };
|
|
828
|
+
//# sourceMappingURL=index.mjs.map
|