@aigne/afs-mapping 1.11.0-beta.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.
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
+ meta: z.record(z.string(), 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(), z.string()).optional(),
273
+ body: z.record(z.string(), z.string()).optional(),
274
+ headers: z.record(z.string(), z.string()).optional(),
275
+ query: z.string().optional(),
276
+ variables: z.record(z.string(), 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(), 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(z.string(), routeDefinitionSchema)
308
+ });
309
+ /**
310
+ * Partial mapping config for included files (no name/version required)
311
+ */
312
+ const partialMappingConfigSchema = z.object({
313
+ routes: z.record(z.string(), 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.meta) entry.meta = this.projectMetadata(entryConfig.meta, data, pathParams);
500
+ if (entryConfig.description) {
501
+ const description = this.evaluateField(entryConfig.description, data, pathParams);
502
+ if (description !== void 0) {
503
+ entry.meta = entry.meta || {};
504
+ entry.meta.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