@cmflow/atlas 3.4.0-beta.7 → 3.4.0-beta.8

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/README.md CHANGED
@@ -59,11 +59,143 @@ export default defineConfig({
59
59
  });
60
60
  ```
61
61
 
62
- Atlas reads `compilerOptions.paths` from the project's `tsconfig.json` and uses them as module aliases. Add `resolver.alias` only to override or complement those aliases.
62
+ Atlas reads `compilerOptions.paths` from the `tsconfig.json` in the directory where the command is run (`process.cwd()`) and uses them as module aliases. `repoRoot` only defines the source tree to analyze and may therefore point to a subdirectory. Add `resolver.alias` only to override or complement those aliases.
63
+
64
+ ## Backend sources
65
+
66
+ Atlas extracts a candidate mapping from code, then optionally consolidates it against a strict backend field reference. Configure these references in the same `analysis.backends` list used to declare backends.
67
+
68
+ A backend can be either a string or a `BackendSource` created with `defineBackendSource`:
69
+
70
+ - a string (for example `"ICC"`) has no field reference; Atlas keeps the current code-analysis and optional inference flow;
71
+ - a `BackendSource` provides a `resolve` function that returns the backend's known properties. Atlas assigns its `name` as `backend` on every resolved property.
72
+
73
+ The resolver returns one of the following shapes. Document-oriented backends have no route; REST backends have no document.
74
+
75
+ ```ts
76
+ type BackendProperty =
77
+ | {
78
+ document: string;
79
+ field: string;
80
+ description?: string;
81
+ }
82
+ | {
83
+ route: string;
84
+ method: string;
85
+ field: string;
86
+ description?: string;
87
+ };
88
+ ```
89
+
90
+ The generated mapping preserves this shape. In particular, `document` is a separate property: Atlas does not turn a Quable field into an artificial path such as `products.product_geographical_area`.
91
+
92
+ ### Quable
93
+
94
+ Attach rules that are specific to Quable directly to its source. They inherit the source backend automatically and their candidates are validated by the Quable property list.
95
+
96
+ ```ts
97
+ import { defineBackendSource, httpClient, quableI18nFieldRule } from "@cmflow/atlas";
98
+
99
+ export const quable = defineBackendSource({
100
+ name: "QUABLE",
101
+ rules: [quableI18nFieldRule],
102
+ resolve: async () => {
103
+ const documentTypes = await httpClient.get<QuableDocumentType[]>("https://quable.example/document-types");
104
+
105
+ return documentTypes.flatMap((document) =>
106
+ document.properties.map((property) => ({
107
+ document: document.code,
108
+ field: property.code,
109
+ description: "récupéré via le backend resolver"
110
+ }))
111
+ );
112
+ }
113
+ });
114
+ ```
115
+
116
+ For example, the resolver above may return:
117
+
118
+ ```ts
119
+ {
120
+ backend: "QUABLE",
121
+ document: "products",
122
+ field: "product_geographical_area",
123
+ description: "récupéré via le backend resolver"
124
+ }
125
+ ```
126
+
127
+ ### CMS Directus
128
+
129
+ CMS Directus follows the same document-oriented contract. Its `cmsI18nFieldRule` is owned by the CMS source rather than registered as a global expression rule.
130
+
131
+ ```ts
132
+ import { defineBackendSource, cmsI18nFieldRule } from "@cmflow/atlas";
133
+
134
+ export const cmsDirectus = defineBackendSource({
135
+ name: "CMS",
136
+ rules: [cmsI18nFieldRule],
137
+ resolve: async () => directusResolver.listProperties()
138
+ });
139
+ ```
140
+
141
+ `directusResolver.listProperties()` returns items such as `{ document: "offers", field: "title", description: "…" }`.
142
+
143
+ ### CMS Legacy
144
+
145
+ CMS Legacy is configured identically, with its own resolver and backend identifier. It may reuse `cmsI18nFieldRule` when the backend uses `CmsI18n` helpers.
146
+
147
+ ```ts
148
+ import { defineBackendSource, cmsI18nFieldRule } from "@cmflow/atlas";
149
+
150
+ export const cmsLegacy = defineBackendSource({
151
+ name: "CMS_LEGACY",
152
+ rules: [cmsI18nFieldRule],
153
+ resolve: async () => cmsLegacyResolver.listProperties()
154
+ });
155
+ ```
156
+
157
+ ### Custom REST backend with OpenAPI
158
+
159
+ `openapiSource` reuses Atlas's OpenAPI parsing utilities: downloading the document, resolving references, traversing operations, and extracting field descriptions. A project therefore only supplies the URL.
160
+
161
+ ```ts
162
+ import { defineBackendSource, openapiSource } from "@cmflow/atlas";
163
+
164
+ export const xm = defineBackendSource({
165
+ name: "XM",
166
+ resolve: () => openapiSource({ url: "https://xm.example/openapi.json" })
167
+ });
168
+ ```
169
+
170
+ This returns entries such as:
171
+
172
+ ```ts
173
+ {
174
+ backend: "XM",
175
+ route: "/path/to",
176
+ method: "GET",
177
+ field: "path.to.field",
178
+ description: "Description extraite du Swagger"
179
+ }
180
+ ```
181
+
182
+ Register strings and sources together:
183
+
184
+ ```ts
185
+ import { defineConfig } from "@cmflow/atlas";
186
+ import { cmsDirectus, cmsLegacy, quable, xm } from "./backend-sources";
187
+
188
+ export default defineConfig({
189
+ // …other Atlas options
190
+ analysis: {
191
+ backends: ["ICC", quable, cmsDirectus, cmsLegacy, xm]
192
+ }
193
+ });
194
+ ```
63
195
 
64
196
  ## Custom expression rules
65
197
 
66
- Use `defineExpressionRule` when a project-specific helper hides a backend field or wraps an expression that Atlas should follow. Add the rule to `analysis.rules` in `atlas.config.ts`.
198
+ Use `defineExpressionRule` when a project-specific helper hides a backend field or wraps an expression that Atlas should follow. Add a cross-backend rule to `analysis.rules` in `atlas.config.ts`. Attach a backend-specific rule, such as `quableI18nFieldRule` or `cmsI18nFieldRule`, to `defineBackendSource({ rules: [...] })` instead.
67
199
 
68
200
  `match` is a cheap predicate that selects the `ts-morph` expression. `parse` returns the information Atlas should use: a backend field, a transparent wrapper, or both.
69
201
 
@@ -73,8 +205,7 @@ import { Node } from "ts-morph";
73
205
 
74
206
  const localizedFieldRule = defineExpressionRule({
75
207
  name: "localized-field",
76
- match: (expression) =>
77
- Node.isCallExpression(expression) && expression.getExpression().getText() === "localizedField",
208
+ match: (expression) => Node.isCallExpression(expression) && expression.getExpression().getText() === "localizedField",
78
209
  parse: (expression) => {
79
210
  if (!Node.isCallExpression(expression)) return undefined;
80
211
 
@@ -151,6 +282,7 @@ Without `--write`, the command performs a dry run. Use `clean-orphans` to inspec
151
282
 
152
283
  ```text
153
284
  atlas init Create atlas.config.ts
285
+ atlas backend-sources Execute backend sources and display resolved metadata
154
286
  atlas generate:graph Trace API routes to backends
155
287
  atlas generate:catalog [route] Generate route review documents
156
288
  atlas generate:test Check configured coverage baselines
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import { a as isKnownBackendType, c as taskProgressService, d as setUserConfig, i as inferBackendNameFromFile, l as getUserConfig, n as generateBackendTopologyArtifactsInWorkers, o as filterAnalysisFiles, r as resolveModulePath, s as shouldKeepAnalysisFile, u as loadAtlasConfig } from "../routeBackendTopologyService-D_aIIBbX.mjs";
2
+ import { a as isKnownBackendType, c as taskProgressService, d as setUserConfig, i as inferBackendNameFromFile, l as getUserConfig, n as generateBackendTopologyArtifactsInWorkers, o as filterAnalysisFiles, r as resolveModulePath, s as shouldKeepAnalysisFile, u as loadAtlasConfig } from "../routeBackendTopologyService-DkNyCtKt.mjs";
3
3
  import { Command, InvalidArgumentError, Option } from "commander";
4
4
  import path from "node:path";
5
5
  import { Node, Project, SyntaxKind } from "ts-morph";
6
- import { cancel, intro, log, note, outro, progress } from "@clack/prompts";
6
+ import { cancel, intro, isCancel, log, note, outro, progress, select } from "@clack/prompts";
7
7
  import fs from "node:fs/promises";
8
8
  import { createDirectus, createItem, deleteItem, readItems, rest, staticToken, updateItem } from "@directus/sdk";
9
9
  import { globby } from "globby";
@@ -48,16 +48,16 @@ function getAggregationTarget(call) {
48
48
  let current = call;
49
49
  while (current) {
50
50
  const parent = current.getParent();
51
- if (!parent) return void 0;
51
+ if (!parent) return;
52
52
  if (Node.isVariableDeclaration(parent)) return normalizeCodeText(parent.getName());
53
53
  if (Node.isReturnStatement(parent)) return "return";
54
- if (Node.isStatement(parent)) return void 0;
54
+ if (Node.isStatement(parent)) return;
55
55
  current = parent;
56
56
  }
57
57
  }
58
58
  function getAggregationCondition(call) {
59
59
  const callback = call.getArguments().find((argument) => Node.isArrowFunction(argument) || Node.isFunctionExpression(argument));
60
- if (!callback || !Node.isArrowFunction(callback) && !Node.isFunctionExpression(callback)) return void 0;
60
+ if (!callback || !Node.isArrowFunction(callback) && !Node.isFunctionExpression(callback)) return;
61
61
  const body = callback.getBody();
62
62
  if (!Node.isBlock(body)) return normalizeCodeText(body.getText());
63
63
  const condition = body.getDescendantsOfKind(SyntaxKind.IfStatement)[0]?.getExpression();
@@ -135,29 +135,80 @@ var AnalysisCacheService = class {
135
135
  };
136
136
  const analysisCacheService = new AnalysisCacheService();
137
137
  //#endregion
138
- //#region src/services/openapi/downloadService.ts
139
- async function downloadOpenApiDocument(url, timeoutMs = getUserConfig().openapiTimeoutMs) {
140
- const response = await fetch(url, {
141
- headers: { accept: "application/json" },
142
- signal: AbortSignal.timeout(timeoutMs)
138
+ //#region src/services/backendPropertyService.ts
139
+ const propertiesBySource = /* @__PURE__ */ new WeakMap();
140
+ async function resolveBackendProperties(sources) {
141
+ const resolved = await Promise.all(sources.map(async (source) => {
142
+ if (!source.resolve) return [source.name, void 0];
143
+ let properties = propertiesBySource.get(source);
144
+ if (!properties) {
145
+ properties = source.resolve().then((result) => result.map((property) => ({
146
+ ...property,
147
+ backend: source.name
148
+ })));
149
+ propertiesBySource.set(source, properties);
150
+ }
151
+ return [source.name, await properties];
152
+ }));
153
+ return new Map(resolved.filter((entry) => entry[1] !== void 0).map(([backend, properties]) => [backend, properties]));
154
+ }
155
+ function matchBackendProperty(field, properties) {
156
+ if (!properties) return;
157
+ const normalized = normalizeField(field);
158
+ return properties.find((property) => {
159
+ const reference = normalizeField(property.field);
160
+ return normalized === reference || normalized.endsWith(reference) || reference.endsWith(normalized);
143
161
  });
144
- if (!response.ok) throw new Error(`Unable to fetch OpenAPI document from ${url}: ${response.status} ${response.statusText}`);
145
- const reader = response.body?.getReader();
146
- if (!reader) return response.json();
147
- const decoder = new TextDecoder();
148
- let content = "";
149
- while (true) {
150
- const { done, value } = await reader.read();
151
- if (done) break;
152
- content += decoder.decode(value, { stream: true });
153
- taskProgressService.log(`Downloading (${(content.length / 1048576).toFixed(1)} MB)`);
154
- }
155
- taskProgressService.log("Parsing document");
156
- return JSON.parse(content + decoder.decode());
157
162
  }
163
+ function normalizeField(value) {
164
+ return value.replace(/\[locale\]/g, "").replace(/\[\]/g, "").replace(/[^a-zA-Z0-9]/g, "").toLowerCase();
165
+ }
166
+ //#endregion
167
+ //#region src/services/http/httpClient.ts
168
+ var HttpClient = class {
169
+ async fetch(url, init) {
170
+ const headers = {
171
+ accept: "application/json",
172
+ ...init?.headers
173
+ };
174
+ const response = await fetch(url, {
175
+ ...init,
176
+ headers
177
+ });
178
+ if (!response.ok) throw new Error(`Unable to fetch ${url}: ${response.status} ${response.statusText}`);
179
+ if (init?.onProgress) {
180
+ const reader = response.body?.getReader();
181
+ if (!reader) return response.json();
182
+ const decoder = new TextDecoder();
183
+ let content = "";
184
+ while (true) {
185
+ const { done, value } = await reader.read();
186
+ if (done) break;
187
+ content += decoder.decode(value, { stream: true });
188
+ init.onProgress(content);
189
+ }
190
+ return content + decoder.decode();
191
+ }
192
+ return response;
193
+ }
194
+ async get(url, init) {
195
+ return (await this.fetch(url, init)).json();
196
+ }
197
+ };
198
+ const httpClient = new HttpClient();
199
+ //#endregion
200
+ //#region src/services/openapi/loadOpenApiDocument.ts
158
201
  async function loadOpenApiDocument(url, timeoutMs) {
159
202
  try {
160
- return await downloadOpenApiDocument(url, timeoutMs);
203
+ const content = await httpClient.fetch(url, {
204
+ signal: timeoutMs ? AbortSignal.timeout(timeoutMs) : void 0,
205
+ onProgress(content) {
206
+ taskProgressService.log(`Downloading (${(content.length / 1048576).toFixed(1)} MB)`);
207
+ }
208
+ });
209
+ if (!content || typeof content === "object") return content;
210
+ taskProgressService.log("Parsing document");
211
+ return JSON.parse(content);
161
212
  } catch (error) {
162
213
  if (error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError")) throw new Error(`OpenAPI download timed out after ${timeoutMs}ms: ${url}`);
163
214
  throw new Error(`Unable to fetch OpenAPI document from ${url}: ${error instanceof Error ? error.message : String(error)}`);
@@ -1188,6 +1239,7 @@ async function collectDependencyFiles(project, entryFilePath, routeLabel, resolv
1188
1239
  }
1189
1240
  async function analyzeCodebaseRouteContracts(params) {
1190
1241
  const { cwd, openApiDocument: swagger, selectedRouteKeys, routeContracts, onDocument, routeAnalysisScope } = params;
1242
+ const backendProperties = await resolveBackendProperties(userConfig.analysis.backends);
1191
1243
  taskProgressService.log("Discovering route files");
1192
1244
  const routeFiles = await globby(["app/_api/**/routes.@(js|ts)", "app/legacy/**/routes.@(js|ts)"], {
1193
1245
  cwd,
@@ -1277,7 +1329,15 @@ async function analyzeCodebaseRouteContracts(params) {
1277
1329
  }), (item) => `${item.backend}:${item.method}:${item.route}:${item.sourceFile}`);
1278
1330
  const backendGraphKey = `${backendSourceFiles.map((file) => file.getFilePath()).sort().join("|")}::${[...reachableBackendFunctionNames].sort().join("|")}`;
1279
1331
  const backendFieldCandidates = backendFieldsByGraph.get(backendGraphKey) || (() => {
1280
- const candidates = extractRouteBackendFieldCandidates(backendSourceFiles, reachableBackendFunctionNames, [...apiSourceFiles, ...useCaseSourceFiles]);
1332
+ const candidates = extractRouteBackendFieldCandidates(backendSourceFiles, reachableBackendFunctionNames, [...apiSourceFiles, ...useCaseSourceFiles]).flatMap((candidate) => {
1333
+ const properties = backendProperties.get(candidate.backend);
1334
+ if (!properties) return [candidate];
1335
+ const resolvedProperty = matchBackendProperty(candidate.backendField, properties);
1336
+ return resolvedProperty ? [{
1337
+ ...candidate,
1338
+ resolvedProperty
1339
+ }] : [];
1340
+ });
1281
1341
  backendFieldsByGraph.set(backendGraphKey, candidates);
1282
1342
  return candidates;
1283
1343
  })();
@@ -1426,6 +1486,31 @@ function resolveBackendRouteCandidate(document, match) {
1426
1486
  const sameBackendCandidates = document.backendRouteCandidates.filter((candidate) => candidate.backend === match.backend);
1427
1487
  return sameBackendCandidates.find((candidate) => candidate.sourceFile === match.sourceFile) || sameBackendCandidates[0];
1428
1488
  }
1489
+ function toBackendMapping(document, match) {
1490
+ const resolved = match.resolvedProperty;
1491
+ if (resolved && "document" in resolved) return {
1492
+ backend: match.backend,
1493
+ document: resolved.document,
1494
+ field: resolved.field,
1495
+ description: resolved.description,
1496
+ source_file: backendFieldSource(match),
1497
+ confidence: match.confidence,
1498
+ mapper_type: match.mapperType,
1499
+ reason: match.reviewReason
1500
+ };
1501
+ const backendRouteCandidate = resolved && "route" in resolved ? resolved : resolveBackendRouteCandidate(document, match);
1502
+ return {
1503
+ backend: match.backend,
1504
+ method: backendRouteCandidate?.method || null,
1505
+ route: backendRouteCandidate?.route || "unknown",
1506
+ field: resolved?.field || match.backendField,
1507
+ description: resolved?.description,
1508
+ source_file: backendFieldSource(match),
1509
+ confidence: match.confidence,
1510
+ mapper_type: match.mapperType,
1511
+ reason: match.reviewReason
1512
+ };
1513
+ }
1429
1514
  async function buildCatalogue(documents) {
1430
1515
  const routes = documents.map((document) => ({
1431
1516
  key: document.key,
@@ -1467,19 +1552,7 @@ async function buildCatalogue(documents) {
1467
1552
  const backendNames = dedupeByKey(matches.map((candidate) => candidate.backend), (value) => value);
1468
1553
  const isTransverseWithoutMapping = isTransversalInput(property) && !matches.length;
1469
1554
  const evidenceStatus = isTransverseWithoutMapping ? "confirmed" : evidenceStatusFromCandidates(matches);
1470
- const backendMappings = dedupeByKey(matches.map((match) => {
1471
- const backendRouteCandidate = resolveBackendRouteCandidate(document, match);
1472
- return {
1473
- backend: match.backend,
1474
- method: backendRouteCandidate?.method || null,
1475
- route: backendRouteCandidate?.route || "unknown",
1476
- field: match.backendField,
1477
- source_file: backendFieldSource(match),
1478
- confidence: match.confidence,
1479
- mapper_type: match.mapperType,
1480
- reason: match.reviewReason
1481
- };
1482
- }), (mapping) => `${mapping.backend}:${mapping.method || "CALL"}:${mapping.route}:${mapping.field}:${mapping.source_file}`);
1555
+ const backendMappings = dedupeByKey(matches.map((match) => toBackendMapping(document, match)), (mapping) => `${mapping.backend}:${mapping.document || ""}:${mapping.method || "CALL"}:${mapping.route || ""}:${mapping.field}:${mapping.source_file}`);
1483
1556
  routeInputProperties.push({
1484
1557
  key: apiPropertyKey,
1485
1558
  route_key: document.key,
@@ -1510,12 +1583,14 @@ async function buildCatalogue(documents) {
1510
1583
  route: "unknown",
1511
1584
  sourceFile: match.sourceFile
1512
1585
  }).key;
1513
- const backendPropertyKey = stableKey$2(backendRouteKey, "input", match.backendField);
1586
+ const backendPropertyKey = stableKey$2(backendRouteKey, "input", match.resolvedProperty?.field || match.backendField);
1514
1587
  backendPropertiesByKey.set(backendPropertyKey, {
1515
1588
  key: backendPropertyKey,
1516
1589
  backend_route_key: backendRouteKey,
1517
1590
  backend: match.backend,
1518
- field: match.backendField,
1591
+ field: match.resolvedProperty?.field || match.backendField,
1592
+ document: match.resolvedProperty && "document" in match.resolvedProperty ? match.resolvedProperty.document : void 0,
1593
+ description: match.resolvedProperty?.description,
1519
1594
  direction: "input",
1520
1595
  source_file: backendFieldSource(match),
1521
1596
  provenance: "code_analysis"
@@ -1539,19 +1614,7 @@ async function buildCatalogue(documents) {
1539
1614
  const apiPropertyKey = stableKey$2(document.key, "output", property.path);
1540
1615
  const backendNames = dedupeByKey(matches.map((candidate) => candidate.backend), (value) => value);
1541
1616
  const evidenceStatus = evidenceStatusFromCandidates(matches);
1542
- const backendMappings = dedupeByKey(matches.map((match) => {
1543
- const backendRouteCandidate = resolveBackendRouteCandidate(document, match);
1544
- return {
1545
- backend: match.backend,
1546
- method: backendRouteCandidate?.method || null,
1547
- route: backendRouteCandidate?.route || "unknown",
1548
- field: match.backendField,
1549
- source_file: backendFieldSource(match),
1550
- confidence: match.confidence,
1551
- mapper_type: match.mapperType,
1552
- reason: match.reviewReason
1553
- };
1554
- }), (mapping) => `${mapping.backend}:${mapping.method || "CALL"}:${mapping.route}:${mapping.field}:${mapping.source_file}`);
1617
+ const backendMappings = dedupeByKey(matches.map((match) => toBackendMapping(document, match)), (mapping) => `${mapping.backend}:${mapping.document || ""}:${mapping.method || "CALL"}:${mapping.route || ""}:${mapping.field}:${mapping.source_file}`);
1555
1618
  routeOutputProperties.push({
1556
1619
  key: apiPropertyKey,
1557
1620
  route_key: document.key,
@@ -1582,12 +1645,14 @@ async function buildCatalogue(documents) {
1582
1645
  route: "unknown",
1583
1646
  sourceFile: match.sourceFile
1584
1647
  }).key;
1585
- const backendPropertyKey = stableKey$2(backendRouteKey, "output", match.backendField);
1648
+ const backendPropertyKey = stableKey$2(backendRouteKey, "output", match.resolvedProperty?.field || match.backendField);
1586
1649
  backendPropertiesByKey.set(backendPropertyKey, {
1587
1650
  key: backendPropertyKey,
1588
1651
  backend_route_key: backendRouteKey,
1589
1652
  backend: match.backend,
1590
- field: match.backendField,
1653
+ field: match.resolvedProperty?.field || match.backendField,
1654
+ document: match.resolvedProperty && "document" in match.resolvedProperty ? match.resolvedProperty.document : void 0,
1655
+ description: match.resolvedProperty?.description,
1591
1656
  direction: "output",
1592
1657
  source_file: backendFieldSource(match),
1593
1658
  provenance: "code_analysis"
@@ -1631,7 +1696,7 @@ async function buildCatalogue(documents) {
1631
1696
  };
1632
1697
  }
1633
1698
  //#endregion
1634
- //#region src/services/directusSyncService.ts
1699
+ //#region src/services/directus/directusSyncService.ts
1635
1700
  const ROUTE_STATUSES = { published: "published" };
1636
1701
  const LOCAL_SOURCE_FILE = "digital-api:tools/datasource-catalogue";
1637
1702
  const DIRECTUS_RETRY_MAX_ATTEMPTS = 6;
@@ -2167,10 +2232,10 @@ async function pushCatalogueToDirectus(catalogue, options) {
2167
2232
  const removedOrphanedLinks = await cleanupOrphanedPropertyLinks(client);
2168
2233
  taskProgressService.report("Finalizing Directus synchronization");
2169
2234
  if (warnings.size) {
2170
- console.warn("========================================");
2171
- console.warn("BIG WARNING: missing predefined values or unresolved Directus links");
2172
- for (const warning of warnings) console.warn(`- ${warning}`);
2173
- console.warn("========================================");
2235
+ taskProgressService.log("========================================");
2236
+ taskProgressService.log("BIG WARNING: missing predefined values or unresolved Directus links");
2237
+ for (const warning of warnings) taskProgressService.log(`- ${warning}`);
2238
+ taskProgressService.log("========================================");
2174
2239
  }
2175
2240
  return {
2176
2241
  pushedCollections,
@@ -2210,9 +2275,11 @@ function buildReviewProperty(property) {
2210
2275
  const backends = property.backend_mappings.map((mapping) => ({
2211
2276
  type: mapping.backend,
2212
2277
  field: mapping.field,
2278
+ document: mapping.document,
2279
+ description: mapping.description,
2213
2280
  source: mapping.source_file,
2214
- ...mapping.route !== "unknown" || mapping.method ? { operation: {
2215
- method: mapping.method,
2281
+ ...mapping.route && (mapping.route !== "unknown" || mapping.method) ? { operation: {
2282
+ method: mapping.method || null,
2216
2283
  route: mapping.route
2217
2284
  } } : {},
2218
2285
  confidence: mapping.confidence,
@@ -2384,16 +2451,17 @@ async function writeCatalogueArtifacts(catalogue, outputDir, repoRoot = process.
2384
2451
  };
2385
2452
  }
2386
2453
  function addBackendMapping(routeKey, direction, apiPropertyKey, mapping, backendRoutesByKey, backendPropertiesByKey, mappingEvidence) {
2387
- const backendRouteKey = stableKey$1(routeKey, mapping.backend, mapping.route);
2388
- const backendRoute = backendRoutesByKey.get(backendRouteKey) || {
2454
+ const backendRoute = mapping.route || (mapping.document ? `DOCUMENT ${mapping.document}` : "unknown");
2455
+ const backendRouteKey = stableKey$1(routeKey, mapping.backend, backendRoute);
2456
+ const backendRouteRecord = backendRoutesByKey.get(backendRouteKey) || {
2389
2457
  key: backendRouteKey,
2390
2458
  backend: mapping.backend,
2391
2459
  route_key: routeKey,
2392
- route: `${mapping.method || "CALL"} ${mapping.route}`,
2460
+ route: `${mapping.method || "CALL"} ${backendRoute}`,
2393
2461
  source_file: mapping.source_file,
2394
2462
  provenance: "code_analysis"
2395
2463
  };
2396
- backendRoutesByKey.set(backendRouteKey, backendRoute);
2464
+ backendRoutesByKey.set(backendRouteKey, backendRouteRecord);
2397
2465
  const backendPropertyKey = stableKey$1(backendRouteKey, direction, mapping.field);
2398
2466
  backendPropertiesByKey.set(backendPropertyKey, {
2399
2467
  key: backendPropertyKey,
@@ -2419,8 +2487,10 @@ function toCatalogueBackendMapping(mapping) {
2419
2487
  return {
2420
2488
  backend: mapping.type,
2421
2489
  method: mapping.operation?.method || null,
2422
- route: mapping.operation?.route || "unknown",
2490
+ ...mapping.operation?.route ? { route: mapping.operation.route } : {},
2423
2491
  field: mapping.field,
2492
+ document: mapping.document,
2493
+ description: mapping.description,
2424
2494
  source_file: mapping.source,
2425
2495
  confidence: mapping.confidence,
2426
2496
  mapper_type: mapping.mapper_type,
@@ -2559,7 +2629,7 @@ async function readCatalogueFromDirectory(inputDir) {
2559
2629
  };
2560
2630
  }
2561
2631
  //#endregion
2562
- //#region src/services/directusPushService.ts
2632
+ //#region src/services/directus/directusPushService.ts
2563
2633
  async function pushCatalogueDirectoryToDirectus(params) {
2564
2634
  const inputPath = path.resolve(params.cwd, params.catalogueDirectory);
2565
2635
  taskProgressService.report("Loading and reconstructing route YAML documents");
@@ -2728,6 +2798,42 @@ var cleanOrphans_default = (program) => void program.command("clean-orphans").op
2728
2798
  }
2729
2799
  });
2730
2800
  //#endregion
2801
+ //#region src/commands/backendSources.ts
2802
+ async function listBackendSourceMetadata(sources, backend) {
2803
+ const selectedSources = backend ? sources.filter((source) => source.name === backend) : sources;
2804
+ if (backend && !selectedSources.length) throw new Error(`Unknown backend source: ${backend}`);
2805
+ const propertiesByBackend = await resolveBackendProperties(selectedSources);
2806
+ return selectedSources.map((source) => ({
2807
+ backend: source.name,
2808
+ properties: propertiesByBackend.get(source.name) || []
2809
+ }));
2810
+ }
2811
+ var backendSources_default = (program) => void program.command("backend-sources").option("--backend <name>", "Only execute one backend source").description("Execute configured backend sources and display their resolved property metadata").action(async (options) => {
2812
+ intro("Atlas backend source metadata");
2813
+ try {
2814
+ const sources = getUserConfig().analysis.backends;
2815
+ const selectedBackend = options.backend || await select({
2816
+ message: "Which backend source do you want to execute?",
2817
+ options: sources.map((source) => ({
2818
+ value: source.name,
2819
+ label: source.name,
2820
+ hint: source.resolve ? "resolver configured" : "no resolver configured"
2821
+ }))
2822
+ });
2823
+ if (isCancel(selectedBackend)) {
2824
+ cancel("Backend source execution cancelled");
2825
+ process.exitCode = 1;
2826
+ return;
2827
+ }
2828
+ const metadata = await listBackendSourceMetadata(sources, selectedBackend);
2829
+ log.message(JSON.stringify(metadata, null, 2));
2830
+ outro("Backend source metadata generated");
2831
+ } catch (error) {
2832
+ cancel(error instanceof Error ? error.message : String(error));
2833
+ process.exitCode = 1;
2834
+ }
2835
+ });
2836
+ //#endregion
2731
2837
  //#region src/services/analysisProfileService.ts
2732
2838
  function createAnalysisProfile(enabled) {
2733
2839
  const durations = /* @__PURE__ */ new Map();
@@ -3042,7 +3148,7 @@ var generateTest_default = (program) => void program.command("generate:test").de
3042
3148
  }
3043
3149
  });
3044
3150
  //#endregion
3045
- //#region src/services/aiSdkClient.ts
3151
+ //#region src/services/ai/aiSdkClient.ts
3046
3152
  var AISdkClient = class {
3047
3153
  #config;
3048
3154
  constructor(config) {
@@ -3073,7 +3179,7 @@ var AISdkClient = class {
3073
3179
  }
3074
3180
  };
3075
3181
  //#endregion
3076
- //#region src/services/inferenceService.ts
3182
+ //#region src/services/ai/inferenceService.ts
3077
3183
  const MAPPING_OUTPUT_SCHEMA = z.object({ mappings: z.array(z.object({
3078
3184
  property_index: z.number().int(),
3079
3185
  candidate_index: z.number().int(),
@@ -4209,6 +4315,7 @@ program.name("atlas").description("Manage the API-to-backend mapping catalogue")
4209
4315
  setUserConfig(config);
4210
4316
  });
4211
4317
  init_default(program, program);
4318
+ backendSources_default(program);
4212
4319
  generate_default(program);
4213
4320
  generateGraph_default(program);
4214
4321
  generateTest_default(program);
package/dist/index.d.mts CHANGED
@@ -1,6 +1,27 @@
1
- import { r as defineExpressionRule, t as UserConfig } from "./types-3y34Gf8R.mjs";
1
+ import { a as defineExpressionRule, n as BackendSource, r as UserConfig, t as BackendProperty } from "./types-smD5SZe9.mjs";
2
+ import { cmsI18nFieldRule } from "./rules/cmsI18nFieldRule.mjs";
3
+ import { quableI18nFieldRule } from "./rules/quableI18nFieldRule.mjs";
2
4
  //#region src/utils/defineConfig.d.ts
3
5
  declare function defineConfig(config: UserConfig): UserConfig;
4
6
  //#endregion
5
- export { type UserConfig, defineConfig, defineExpressionRule };
7
+ //#region src/utils/defineBackendSource.d.ts
8
+ declare function defineBackendSource(source: BackendSource): BackendSource;
9
+ //#endregion
10
+ //#region src/services/http/httpClient.d.ts
11
+ declare class HttpClient {
12
+ fetch(url: string, init: RequestInit & {
13
+ onProgress: (content: string) => void;
14
+ }): Promise<string | unknown>;
15
+ fetch(url: string, init?: RequestInit): Promise<Response>;
16
+ get<T>(url: string, init?: RequestInit): Promise<T>;
17
+ }
18
+ declare const httpClient: HttpClient;
19
+ //#endregion
20
+ //#region src/services/openapi/openapiSource.d.ts
21
+ declare function openapiSource(params: {
22
+ url: string;
23
+ timeoutMs?: number;
24
+ }): Promise<BackendProperty[]>;
25
+ //#endregion
26
+ export { type BackendProperty, type BackendSource, type UserConfig, cmsI18nFieldRule, defineBackendSource, defineConfig, defineExpressionRule, httpClient, openapiSource, quableI18nFieldRule };
6
27
  //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import{t as e}from"./defineExpressionRule-Dfvzj6n2.mjs";function t(e){return e}export{t as defineConfig,e as defineExpressionRule};
1
+ import{t as e}from"./defineExpressionRule-Dfvzj6n2.mjs";import{cmsI18nFieldRule as t}from"./rules/cmsI18nFieldRule.mjs";import{quableI18nFieldRule as n}from"./rules/quableI18nFieldRule.mjs";import{AsyncLocalStorage as r}from"node:async_hooks";import{spinner as i}from"@clack/prompts";function a(e){return e}function o(e){return e}const s=new class{async fetch(e,t){let n={accept:`application/json`,...t?.headers},r=await fetch(e,{...t,headers:n});if(!r.ok)throw Error(`Unable to fetch ${e}: ${r.status} ${r.statusText}`);if(t?.onProgress){let e=r.body?.getReader();if(!e)return r.json();let n=new TextDecoder,i=``;for(;;){let{done:r,value:a}=await e.read();if(r)break;i+=n.decode(a,{stream:!0}),t.onProgress(i)}return i+n.decode()}return r}async get(e,t){return(await this.fetch(e,t)).json()}};function c(e){return e<1e3?`${e}ms`:`${(e/1e3).toFixed(1)}s`}const l=new class{#e=new r;attach(e,t){return this.#e.run(e,t)}log(e){this.#e.getStore()?.log(e)}report(e){let t=this.#e.getStore();(t?.report||t?.log)?.(e)}createStepProgress(e=e=>({id:e,title:e})){let t=i(),n,r=e=>{if(!n)return;let r=c(Date.now()-n.startedAt);t.stop(`${e||n.completedTitle||`${n.title} completed`} (${r})`),n=void 0},a=e=>{if(n?.id===e.id){t.message(e.detail?`${e.title}: ${e.detail}`:e.title);return}r(),n={id:e.id,title:e.title,completedTitle:e.completedTitle,startedAt:Date.now()},t.start(e.detail?`${e.title}: ${e.detail}`:e.title)},o=r=>this.attach({log:e=>t.message(n?`${n.title}: ${e}`:e),report:t=>a(e(t))},r);return{report(t){a(e(t))},start:a,execute:o,run:async(e,t)=>{a(e),await new Promise(e=>setImmediate(e));let n=await o(t);return r(),n},finish(e){r(e)},fail(e){if(!n)return;let r=c(Date.now()-n.startedAt);t.stop(`${e} (${r})`),n=void 0}}}};async function u(e,t){try{let n=await s.fetch(e,{signal:t?AbortSignal.timeout(t):void 0,onProgress(e){l.log(`Downloading (${(e.length/1048576).toFixed(1)} MB)`)}});return!n||typeof n==`object`?n:(l.log(`Parsing document`),JSON.parse(n))}catch(n){throw n instanceof Error&&(n.name===`AbortError`||n.name===`TimeoutError`)?Error(`OpenAPI download timed out after ${t}ms: ${e}`):Error(`Unable to fetch OpenAPI document from ${e}: ${n instanceof Error?n.message:String(n)}`)}}function d(e,t){if(!e||typeof e!=`object`||typeof e.$ref!=`string`||!e.$ref.startsWith(`#/`))return e;let n=t;for(let t of e.$ref.replace(`#/`,``).split(`/`).map(e=>e.replace(/~1/g,`/`).replace(/~0/g,`~`)))if(n=n?.[t],n===void 0)return e;return n}function f(e){return/^\d+$/.test(e)&&Number(e)>=200&&Number(e)<=299}function p(e,t){if(!e||typeof e!=`object`)return;if(e[`application/json`]?.schema)return d(e[`application/json`].schema,t);let n=Object.values(e).find(e=>e?.schema);return n?d(n.schema,t):void 0}function m(e,t,n=``){let r=d(e,t);if(!r||typeof r!=`object`)return[];if(Array.isArray(r.allOf))return r.allOf.flatMap(e=>m(e,t,n));if(r.type===`array`||r.items){let e=n?`${n}[]`:`[]`;return m(r.items,t,e)}let i=r.properties||{};return Object.keys(i).length?Object.entries(i).flatMap(([e,r])=>m(r,t,n?`${n}.${e}`:e)):n?[{path:n,description:r.description,deprecated:r.deprecated}]:[]}function h(e){return e===`query`?`QUERY`:e===`path`?`PATH`:e===`header`?`HEADER`:null}function g(e,t){let n=new Map,r=Array.isArray(e.parameters)?e.parameters:[];for(let e of r){let r=d(e,t),i=h(r?.in);if(!r||!i||!r.name)continue;let a=m(r.schema,t,r.name),o=a.length?a:[{path:r.name,description:r.description,deprecated:r.deprecated}];for(let e of o)n.set(`${i}:${e.path}`,{path:e.path,description:e.description||r.description,deprecated:e.deprecated??r.deprecated??!1,type:i})}let i=d(e.requestBody,t);if(i?.content){let e=p(i.content,t);for(let r of m(e,t))n.set(`BODY:${r.path}`,{path:r.path,description:r.description||i.description,deprecated:r.deprecated??!1,type:`BODY`})}return[...n.values()]}function _(e,t){let n=new Map,r=e?.responses||{};for(let[e,i]of Object.entries(r)){if(!f(e))continue;let r=d(i,t),a=p(r?.content,t);for(let e of m(a,t))n.set(e.path,{path:e.path,description:e.description||r?.description,deprecated:e.deprecated??!1,type:`RESPONSE_BODY`})}return[...n.values()]}function v(e){let t=new Map;for(let[n,r]of Object.entries(e.paths||{}))for(let[i,a]of Object.entries(r||{})){if(!/^(get|post|put|patch|delete|head|options)$/i.test(i))continue;let r=i.toUpperCase();for(let i of[...g(a,e),..._(a,e)]){let e=`${r}:${n}:${i.path}`;t.has(e)||t.set(e,{route:n,method:r,field:i.path,description:i.description})}}return[...t.values()]}async function y(e){return v(await u(e.url,e.timeoutMs))}export{t as cmsI18nFieldRule,o as defineBackendSource,a as defineConfig,e as defineExpressionRule,s as httpClient,y as openapiSource,n as quableI18nFieldRule};
2
2
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/utils/defineConfig.ts"],"sourcesContent":["import type { UserConfig } from \"../models/types\";\n\nexport function defineConfig(config: UserConfig): UserConfig {\n return config;\n}\n"],"mappings":"wDAEA,SAAgB,EAAa,EAAgC,CAC3D,OAAO,CACT"}
1
+ {"version":3,"file":"index.mjs","names":["#storage"],"sources":["../src/utils/defineConfig.ts","../src/utils/defineBackendSource.ts","../src/services/http/httpClient.ts","../src/services/tasks/taskProgressService.ts","../src/services/openapi/loadOpenApiDocument.ts","../src/services/openapi/schemaService.ts","../src/services/openapi/propertyExtractionService.ts","../src/services/openapi/openapiSource.ts"],"sourcesContent":["import type { UserConfig } from \"../models/types\";\n\nexport function defineConfig(config: UserConfig): UserConfig {\n return config;\n}\n","import type { BackendSource } from \"../models/types\";\n\nexport function defineBackendSource(source: BackendSource): BackendSource {\n return source;\n}\n","class HttpClient {\n async fetch(url: string, init: RequestInit & { onProgress: (content: string) => void }): Promise<string | unknown>;\n async fetch(url: string, init?: RequestInit): Promise<Response>;\n async fetch(url: string, init?: RequestInit & { onProgress?: (content: string) => void }): Promise<Response | string | unknown> {\n const headers = { accept: \"application/json\", ...init?.headers };\n\n const response = await fetch(url, {\n ...init,\n headers\n });\n\n if (!response.ok) {\n throw new Error(`Unable to fetch ${url}: ${response.status} ${response.statusText}`);\n }\n\n if (init?.onProgress) {\n const reader = response.body?.getReader();\n\n if (!reader) {\n return response.json() as unknown;\n }\n\n const decoder = new TextDecoder();\n let content = \"\";\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) {\n break;\n }\n\n content += decoder.decode(value, { stream: true });\n\n init.onProgress(content);\n }\n\n return content + decoder.decode();\n }\n\n return response;\n }\n\n async get<T>(url: string, init?: RequestInit): Promise<T> {\n const response = await this.fetch(url, init);\n return response.json() as Promise<T>;\n }\n}\n\nexport const httpClient = new HttpClient();\n","import { AsyncLocalStorage } from \"node:async_hooks\";\nimport { spinner } from \"@clack/prompts\";\n\nexport type StepDescriptor = {\n id: string;\n title: string;\n completedTitle?: string;\n detail?: string;\n};\n\ntype TaskProgressSink = {\n log: (message: string) => void;\n report?: (message: string) => void;\n};\n\nfunction formatDuration(durationMs: number): string {\n if (durationMs < 1_000) {\n return `${durationMs}ms`;\n }\n return `${(durationMs / 1_000).toFixed(1)}s`;\n}\n\nclass TaskProgressService {\n readonly #storage = new AsyncLocalStorage<TaskProgressSink>();\n\n attach<T>(sink: TaskProgressSink, task: () => Promise<T>): Promise<T> {\n return this.#storage.run(sink, task);\n }\n\n log(message: string): void {\n this.#storage.getStore()?.log(message);\n }\n\n report(message: string): void {\n const sink = this.#storage.getStore();\n const reporter = sink?.report || sink?.log;\n reporter?.(message);\n }\n\n createStepProgress(classify: (message: string) => StepDescriptor = (message) => ({ id: message, title: message })) {\n const progress = spinner();\n let active: { id: string; title: string; completedTitle?: string; startedAt: number } | undefined;\n\n const finishActive = (label?: string) => {\n if (!active) {\n return;\n }\n const duration = formatDuration(Date.now() - active.startedAt);\n progress.stop(`${label || active.completedTitle || `${active.title} completed`} (${duration})`);\n active = undefined;\n };\n\n const start = (step: StepDescriptor) => {\n if (active?.id === step.id) {\n progress.message(step.detail ? `${step.title}: ${step.detail}` : step.title);\n return;\n }\n\n finishActive();\n active = {\n id: step.id,\n title: step.title,\n completedTitle: step.completedTitle,\n startedAt: Date.now()\n };\n progress.start(step.detail ? `${step.title}: ${step.detail}` : step.title);\n };\n\n const execute = <T>(task: () => Promise<T>): Promise<T> =>\n this.attach(\n {\n log: (message) => progress.message(active ? `${active.title}: ${message}` : message),\n report: (message) => start(classify(message))\n },\n task\n );\n\n return {\n report(message: string) {\n start(classify(message));\n },\n start,\n execute,\n run: async <T>(step: StepDescriptor, task: () => Promise<T>): Promise<T> => {\n start(step);\n await new Promise<void>((resolve) => setImmediate(resolve));\n const result = await execute(task);\n finishActive();\n return result;\n },\n finish(label?: string) {\n finishActive(label);\n },\n fail(label: string) {\n if (!active) {\n return;\n }\n const duration = formatDuration(Date.now() - active.startedAt);\n progress.stop(`${label} (${duration})`);\n active = undefined;\n }\n };\n }\n}\n\nexport const taskProgressService = new TaskProgressService();\n","import { httpClient } from \"../http/httpClient\";\nimport { taskProgressService } from \"../tasks/taskProgressService\";\n\nexport type OpenApiDocument = {\n info?: { version?: string };\n paths?: Record<string, Record<string, any>>;\n components?: Record<string, any>;\n};\n\nexport async function loadOpenApiDocument(url: string, timeoutMs?: number): Promise<OpenApiDocument> {\n try {\n const content = await httpClient.fetch(url, {\n signal: timeoutMs ? AbortSignal.timeout(timeoutMs) : undefined,\n onProgress(content) {\n taskProgressService.log(`Downloading (${(content.length / 1_048_576).toFixed(1)} MB)`);\n }\n });\n\n if (!content || typeof content === \"object\") {\n return content as OpenApiDocument;\n }\n\n taskProgressService.log(\"Parsing document\");\n\n return JSON.parse(content as string) as OpenApiDocument;\n } catch (error) {\n if (error instanceof Error && (error.name === \"AbortError\" || error.name === \"TimeoutError\")) {\n throw new Error(`OpenAPI download timed out after ${timeoutMs}ms: ${url}`);\n }\n\n throw new Error(`Unable to fetch OpenAPI document from ${url}: ${error instanceof Error ? error.message : String(error)}`);\n }\n}\n","import type { OpenApiDocument } from \"./loadOpenApiDocument\";\n\nexport function resolveOpenApiReference(value: any, swagger: OpenApiDocument): any {\n if (!value || typeof value !== \"object\" || typeof value.$ref !== \"string\" || !value.$ref.startsWith(\"#/\")) {\n return value;\n }\n let current: any = swagger;\n for (const segment of value.$ref\n .replace(\"#/\", \"\")\n .split(\"/\")\n .map((part: string) => part.replace(/~1/g, \"/\").replace(/~0/g, \"~\"))) {\n current = current?.[segment];\n if (current === undefined) {\n return value;\n }\n }\n return current;\n}\n","import type { BackendProperty, ExtractedApiProperty } from \"../../models/types\";\nimport type { OpenApiDocument } from \"./loadOpenApiDocument\";\nimport { resolveOpenApiReference } from \"./schemaService\";\n\ntype OpenApiLeafProperty = {\n path: string;\n description?: string;\n deprecated?: boolean;\n};\n\nfunction isSuccessStatusCode(statusCode: string): boolean {\n return /^\\d+$/.test(statusCode) && Number(statusCode) >= 200 && Number(statusCode) <= 299;\n}\n\nfunction selectPreferredSchema(content: any, swagger: OpenApiDocument): any {\n if (!content || typeof content !== \"object\") {\n return undefined;\n }\n\n if (content[\"application/json\"]?.schema) {\n return resolveOpenApiReference(content[\"application/json\"].schema, swagger);\n }\n\n const firstSchema = Object.values(content).find((entry: any) => entry?.schema) as any;\n return firstSchema ? resolveOpenApiReference(firstSchema.schema, swagger) : undefined;\n}\n\nfunction extractLeafProperties(schema: any, swagger: OpenApiDocument, currentPath = \"\"): OpenApiLeafProperty[] {\n const resolvedSchema = resolveOpenApiReference(schema, swagger);\n\n if (!resolvedSchema || typeof resolvedSchema !== \"object\") {\n return [];\n }\n\n if (Array.isArray(resolvedSchema.allOf)) {\n return resolvedSchema.allOf.flatMap((item: any) => extractLeafProperties(item, swagger, currentPath));\n }\n\n if (resolvedSchema.type === \"array\" || resolvedSchema.items) {\n const arrayPath = currentPath ? `${currentPath}[]` : \"[]\";\n return extractLeafProperties(resolvedSchema.items, swagger, arrayPath);\n }\n\n const properties = resolvedSchema.properties || {};\n if (!Object.keys(properties).length) {\n return currentPath\n ? [\n {\n path: currentPath,\n description: resolvedSchema.description,\n deprecated: resolvedSchema.deprecated\n }\n ]\n : [];\n }\n\n return Object.entries(properties).flatMap(([propertyName, propertySchema]: [string, any]) => {\n const nextPath = currentPath ? `${currentPath}.${propertyName}` : propertyName;\n return extractLeafProperties(propertySchema, swagger, nextPath);\n });\n}\n\nfunction mapInputType(inType?: string): ExtractedApiProperty[\"type\"] | null {\n if (inType === \"query\") {\n return \"QUERY\";\n }\n if (inType === \"path\") {\n return \"PATH\";\n }\n if (inType === \"header\") {\n return \"HEADER\";\n }\n return null;\n}\n\nexport function extractOpenApiInputProperties(operation: any, swagger: OpenApiDocument): ExtractedApiProperty[] {\n const map = new Map<string, ExtractedApiProperty>();\n const parameters = Array.isArray(operation.parameters) ? operation.parameters : [];\n\n for (const rawParameter of parameters) {\n const parameter = resolveOpenApiReference(rawParameter, swagger);\n const inputType = mapInputType(parameter?.in);\n if (!parameter || !inputType || !parameter.name) {\n continue;\n }\n\n const properties = extractLeafProperties(parameter.schema, swagger, parameter.name);\n const resolvedProperties = properties.length\n ? properties\n : [\n {\n path: parameter.name,\n description: parameter.description,\n deprecated: parameter.deprecated\n }\n ];\n\n for (const property of resolvedProperties) {\n map.set(`${inputType}:${property.path}`, {\n path: property.path,\n description: property.description || parameter.description,\n deprecated: property.deprecated ?? parameter.deprecated ?? false,\n type: inputType\n });\n }\n }\n\n const requestBody = resolveOpenApiReference(operation.requestBody, swagger);\n if (requestBody?.content) {\n const schema = selectPreferredSchema(requestBody.content, swagger);\n for (const property of extractLeafProperties(schema, swagger)) {\n map.set(`BODY:${property.path}`, {\n path: property.path,\n description: property.description || requestBody.description,\n deprecated: property.deprecated ?? false,\n type: \"BODY\"\n });\n }\n }\n\n return [...map.values()];\n}\n\nexport function extractOpenApiOutputProperties(operation: any, swagger: OpenApiDocument): ExtractedApiProperty[] {\n const map = new Map<string, ExtractedApiProperty>();\n const responses = operation?.responses || {};\n\n for (const [statusCode, rawResponse] of Object.entries(responses)) {\n if (!isSuccessStatusCode(statusCode)) {\n continue;\n }\n\n const response = resolveOpenApiReference(rawResponse, swagger);\n const schema = selectPreferredSchema(response?.content, swagger);\n for (const property of extractLeafProperties(schema, swagger)) {\n map.set(property.path, {\n path: property.path,\n description: property.description || response?.description,\n deprecated: property.deprecated ?? false,\n type: \"RESPONSE_BODY\"\n });\n }\n }\n\n return [...map.values()];\n}\n\nexport function extractOpenApiBackendProperties(swagger: OpenApiDocument): BackendProperty[] {\n const properties = new Map<string, BackendProperty>();\n for (const [route, pathItem] of Object.entries(swagger.paths || {})) {\n for (const [rawMethod, operation] of Object.entries(pathItem || {})) {\n if (!/^(get|post|put|patch|delete|head|options)$/i.test(rawMethod)) {\n continue;\n }\n const method = rawMethod.toUpperCase();\n for (const property of [\n ...extractOpenApiInputProperties(operation, swagger),\n ...extractOpenApiOutputProperties(operation, swagger)\n ]) {\n const key = `${method}:${route}:${property.path}`;\n if (!properties.has(key)) {\n properties.set(key, {\n route,\n method,\n field: property.path,\n description: property.description\n });\n }\n }\n }\n }\n return [...properties.values()];\n}\n","import type { BackendProperty } from \"../../models/types\";\nimport { loadOpenApiDocument } from \"./loadOpenApiDocument\";\nimport { extractOpenApiBackendProperties } from \"./propertyExtractionService\";\n\nexport async function openapiSource(params: { url: string; timeoutMs?: number }): Promise<BackendProperty[]> {\n const document = await loadOpenApiDocument(params.url, params.timeoutMs);\n return extractOpenApiBackendProperties(document);\n}\n"],"mappings":"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"}
@@ -10,14 +10,32 @@ import { Worker } from "node:worker_threads";
10
10
  //#region src/utils/config.ts
11
11
  let _config;
12
12
  function setUserConfig(config) {
13
- _config = config;
13
+ const backends = normalizeBackendSources(config.analysis.backends);
14
+ _config = {
15
+ ...config,
16
+ analysis: {
17
+ ...config.analysis,
18
+ backends,
19
+ rules: [.../* @__PURE__ */ new Set([...config.analysis.rules, ...backends.flatMap((backend) => backend.rules || [])])]
20
+ }
21
+ };
22
+ }
23
+ function normalizeBackendSources(backends) {
24
+ const sources = backends.map((backend) => typeof backend === "string" ? { name: backend } : backend);
25
+ const names = /* @__PURE__ */ new Set();
26
+ for (const source of sources) {
27
+ if (!source.name.trim()) throw new Error("Backend source names must not be empty");
28
+ if (names.has(source.name)) throw new Error(`Backend source names must be unique: ${source.name}`);
29
+ names.add(source.name);
30
+ }
31
+ return sources;
14
32
  }
15
33
  function getUserConfig() {
16
34
  if (!_config) throw new Error("Atlas config not loaded. Run Atlas from a directory containing atlas.config.ts or pass --config.");
17
35
  return _config;
18
36
  }
19
- function getTsconfigAliases(repoRoot) {
20
- const tsconfigPath = path.join(repoRoot, "tsconfig.json");
37
+ function getTsconfigAliases(workingDirectory = process.cwd()) {
38
+ const tsconfigPath = path.join(workingDirectory, "tsconfig.json");
21
39
  if (!fs.existsSync(tsconfigPath)) return {};
22
40
  const paths = new Project({
23
41
  tsConfigFilePath: tsconfigPath,
@@ -39,12 +57,17 @@ async function loadAtlasConfig(configPath, projectRoot) {
39
57
  } else mod = await import(pathToFileURL(filePath).href);
40
58
  const config = mod.default ?? mod;
41
59
  const repoRoot = projectRoot || config.repoRoot || process.cwd();
60
+ const backends = normalizeBackendSources(config.analysis.backends);
42
61
  return {
43
62
  ...config,
44
63
  repoRoot,
45
64
  cwd: repoRoot,
65
+ analysis: {
66
+ ...config.analysis,
67
+ backends
68
+ },
46
69
  resolver: { alias: {
47
- ...getTsconfigAliases(repoRoot),
70
+ ...getTsconfigAliases(),
48
71
  ...config.resolver?.alias
49
72
  } }
50
73
  };
@@ -53,7 +76,7 @@ async function loadAtlasConfig(configPath, projectRoot) {
53
76
  }
54
77
  }
55
78
  //#endregion
56
- //#region src/services/taskProgressService.ts
79
+ //#region src/services/tasks/taskProgressService.ts
57
80
  function formatDuration(durationMs) {
58
81
  if (durationMs < 1e3) return `${durationMs}ms`;
59
82
  return `${(durationMs / 1e3).toFixed(1)}s`;
@@ -138,7 +161,7 @@ function filterAnalysisFiles(filePaths) {
138
161
  //#endregion
139
162
  //#region src/utils/isKnowBackendType.ts
140
163
  function isKnownBackendType(value) {
141
- return getUserConfig().analysis.backends.includes(value);
164
+ return getUserConfig().analysis.backends.some((backend) => backend.name === value);
142
165
  }
143
166
  //#endregion
144
167
  //#region src/services/backendSourceService.ts
@@ -234,14 +257,14 @@ function findLocalCallable(sourceFile, symbol) {
234
257
  }
235
258
  function resolveSourceFile(project, owner, moduleSpecifier, resolvePath) {
236
259
  const resolvedPath = resolvePath(owner.getFilePath(), moduleSpecifier);
237
- if (!resolvedPath) return void 0;
260
+ if (!resolvedPath) return;
238
261
  const existing = project.getSourceFile(resolvedPath);
239
262
  if (existing) return existing;
240
263
  return isFilePath(resolvedPath) ? project.addSourceFileAtPathIfExists(resolvedPath) : void 0;
241
264
  }
242
265
  function resolveExportedCallable(project, sourceFile, symbol, resolvePath, seen = /* @__PURE__ */ new Set()) {
243
266
  const key = `${sourceFile.getFilePath()}:${symbol}`;
244
- if (seen.has(key)) return void 0;
267
+ if (seen.has(key)) return;
245
268
  seen.add(key);
246
269
  const local = findLocalCallable(sourceFile, symbol);
247
270
  if (local) return {
@@ -263,7 +286,7 @@ function resolveExportedCallable(project, sourceFile, symbol, resolvePath, seen
263
286
  }
264
287
  function resolveConstructedMember(project, sourceFile, variableName, memberName, resolvePath) {
265
288
  const initializer = sourceFile.getVariableDeclaration(variableName)?.getInitializer();
266
- if (!initializer || !Node.isNewExpression(initializer)) return void 0;
289
+ if (!initializer || !Node.isNewExpression(initializer)) return;
267
290
  const constructorName = initializer.getExpression().getText();
268
291
  const localMethod = sourceFile.getClass(constructorName)?.getInstanceMethod(memberName);
269
292
  if (localMethod) return {
@@ -324,7 +347,7 @@ function resolveTypedPropertyMethod(project, sourceFile, expression, resolvePath
324
347
  const receiver = expression.getExpression();
325
348
  if (!Node.isPropertyAccessExpression(receiver) || receiver.getExpression().getText() !== "this") return;
326
349
  const propertyType = expression.getFirstAncestorByKind(SyntaxKind.ClassDeclaration)?.getProperty(receiver.getName())?.getTypeNode()?.getText().match(/[A-Za-z_$][A-Za-z0-9_$]*/)?.[0];
327
- if (!propertyType) return void 0;
350
+ if (!propertyType) return;
328
351
  for (const importDeclaration of sourceFile.getImportDeclarations()) {
329
352
  const namedImport = importDeclaration.getNamedImports().find((item) => (item.getAliasNode()?.getText() || item.getName()) === propertyType);
330
353
  if (!namedImport) continue;
@@ -361,10 +384,10 @@ function resolveReference(project, sourceFile, expression, resolvePath) {
361
384
  const imported = resolveImportedReference(project, sourceFile, expressionText, resolvePath);
362
385
  if (imported) return imported;
363
386
  const declaration = expression.getSymbol()?.getAliasedSymbol()?.getDeclarations()[0] || expression.getSymbol()?.getDeclarations()[0];
364
- if (!declaration) return void 0;
387
+ if (!declaration) return;
365
388
  const callable = Node.isFunctionDeclaration(declaration) || Node.isMethodDeclaration(declaration) || Node.isVariableDeclaration(declaration) ? declaration : void 0;
366
- if (!callable) return void 0;
367
- if (Node.isVariableDeclaration(callable) && !isCallableVariable(callable)) return void 0;
389
+ if (!callable) return;
390
+ if (Node.isVariableDeclaration(callable) && !isCallableVariable(callable)) return;
368
391
  const targetFile = callable.getSourceFile();
369
392
  return {
370
393
  declaration: callable,
@@ -404,7 +427,7 @@ function mappingDirection(symbol, layer) {
404
427
  function callableFromTopologyNode(cwd, project, node) {
405
428
  const filePath = path.resolve(cwd, node.source);
406
429
  const sourceFile = project.getSourceFile(filePath) || project.addSourceFileAtPathIfExists(filePath);
407
- if (!sourceFile) return void 0;
430
+ if (!sourceFile) return;
408
431
  const declaration = [
409
432
  ...sourceFile.getFunctions(),
410
433
  ...sourceFile.getDescendantsOfKind(SyntaxKind.MethodDeclaration),
@@ -574,13 +597,13 @@ function traceBackendPaths(params) {
574
597
  }
575
598
  function stringProperty(object, name) {
576
599
  const property = object.getProperty(name);
577
- if (!property || !Node.isPropertyAssignment(property)) return void 0;
600
+ if (!property || !Node.isPropertyAssignment(property)) return;
578
601
  const initializer = property.getInitializer();
579
602
  return initializer && (Node.isStringLiteral(initializer) || Node.isNoSubstitutionTemplateLiteral(initializer)) ? initializer.getLiteralValue() : void 0;
580
603
  }
581
604
  function handlerProperty(object) {
582
605
  const property = object.getProperty("handler");
583
- if (!property || !Node.isPropertyAssignment(property)) return void 0;
606
+ if (!property || !Node.isPropertyAssignment(property)) return;
584
607
  const initializer = property.getInitializer();
585
608
  return initializer && (Node.isIdentifier(initializer) || Node.isPropertyAccessExpression(initializer)) ? initializer.getText() : void 0;
586
609
  }
@@ -1,4 +1,4 @@
1
- import { n as FieldExtractionRule } from "../types-3y34Gf8R.mjs";
1
+ import { i as FieldExtractionRule } from "../types-smD5SZe9.mjs";
2
2
  //#region src/rules/cleanObjectRule.d.ts
3
3
  declare const cleanObjectRule: FieldExtractionRule;
4
4
  //#endregion
@@ -1,4 +1,4 @@
1
- import { n as FieldExtractionRule } from "../types-3y34Gf8R.mjs";
1
+ import { i as FieldExtractionRule } from "../types-smD5SZe9.mjs";
2
2
  //#region src/rules/cmsI18nFieldRule.d.ts
3
3
  /**
4
4
  * `CmsI18n#get`/`getAll` reads a localized field off the translations array passed to the
@@ -1,4 +1,4 @@
1
- import { n as FieldExtractionRule } from "../types-3y34Gf8R.mjs";
1
+ import { i as FieldExtractionRule } from "../types-smD5SZe9.mjs";
2
2
  //#region src/rules/dateConversionRule.d.ts
3
3
  declare const dateConversionRule: FieldExtractionRule;
4
4
  //#endregion
@@ -1,4 +1,4 @@
1
- import { n as FieldExtractionRule } from "../types-3y34Gf8R.mjs";
1
+ import { i as FieldExtractionRule } from "../types-smD5SZe9.mjs";
2
2
  //#region src/rules/lodashGetRule.d.ts
3
3
  declare const lodashGetRule: FieldExtractionRule;
4
4
  //#endregion
@@ -1,4 +1,4 @@
1
- import { n as FieldExtractionRule } from "../types-3y34Gf8R.mjs";
1
+ import { i as FieldExtractionRule } from "../types-smD5SZe9.mjs";
2
2
  //#region src/rules/mappingUtilityRule.d.ts
3
3
  declare const mappingUtilityRule: FieldExtractionRule;
4
4
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"mappingUtilityRule.mjs","names":[],"sources":["../../src/rules/mappingUtilityRule.ts"],"sourcesContent":["import { Node } from \"ts-morph\";\nimport { defineExpressionRule } from \"../utils/defineExpressionRule\";\n\nconst mapperTypes = new Set([\"string\", \"number\", \"boolean\", \"date\", \"datetime\", \"enum\", \"array\", \"object\"]);\n\nexport const mappingUtilityRule = defineExpressionRule({\n name: \"mapping-utility-wrapper\",\n match: (expression, config) => {\n if (!Node.isCallExpression(expression)) return false;\n\n const callee = expression.getExpression().getText();\n return config.analysis.neutralExpressionMatchers.some((matcher) => callee.startsWith(matcher.prefix));\n },\n parse: (expression, config) => {\n if (!Node.isCallExpression(expression)) return undefined;\n\n const callee = expression.getExpression().getText();\n const matcher = config.analysis.neutralExpressionMatchers.find((item) => callee.startsWith(item.prefix));\n const mapperType = callee.split(\".\").find((segment) => mapperTypes.has(segment));\n\n return {\n transparent: true,\n mapperType,\n apiMapping: matcher?.apiMapping ?? false\n };\n }\n});\n"],"mappings":"yFAGA,MAAM,EAAc,IAAI,IAAI,CAAC,SAAU,SAAU,UAAW,OAAQ,WAAY,OAAQ,QAAS,QAAQ,CAAC,EAE7F,EAAqB,EAAqB,CACrD,KAAM,0BACN,OAAQ,EAAY,IAAW,CAC7B,GAAI,CAAC,EAAK,iBAAiB,CAAU,EAAG,MAAO,GAE/C,IAAM,EAAS,EAAW,cAAc,CAAC,CAAC,QAAQ,EAClD,OAAO,EAAO,SAAS,0BAA0B,KAAM,GAAY,EAAO,WAAW,EAAQ,MAAM,CAAC,CACtG,EACA,OAAQ,EAAY,IAAW,CAC7B,GAAI,CAAC,EAAK,iBAAiB,CAAU,EAAG,OAExC,IAAM,EAAS,EAAW,cAAc,CAAC,CAAC,QAAQ,EAC5C,EAAU,EAAO,SAAS,0BAA0B,KAAM,GAAS,EAAO,WAAW,EAAK,MAAM,CAAC,EAGvG,MAAO,CACL,YAAa,GACb,WAJiB,EAAO,MAAM,GAAG,CAAC,CAAC,KAAM,GAAY,EAAY,IAAI,CAAO,CAInE,EACT,WAAY,GAAS,YAAc,EACrC,CACF,CACF,CAAC"}
1
+ {"version":3,"file":"mappingUtilityRule.mjs","names":[],"sources":["../../src/rules/mappingUtilityRule.ts"],"sourcesContent":["import { Node } from \"ts-morph\";\nimport { defineExpressionRule } from \"../utils/defineExpressionRule\";\n\nconst mapperTypes = new Set([\"string\", \"number\", \"boolean\", \"date\", \"datetime\", \"enum\", \"array\", \"object\"]);\n\nexport const mappingUtilityRule = defineExpressionRule({\n name: \"mapping-utility-wrapper\",\n match: (expression, config) => {\n if (!Node.isCallExpression(expression)) {\n return false;\n }\n\n const callee = expression.getExpression().getText();\n return config.analysis.neutralExpressionMatchers.some((matcher) => callee.startsWith(matcher.prefix));\n },\n parse: (expression, config) => {\n if (!Node.isCallExpression(expression)) {\n return undefined;\n }\n\n const callee = expression.getExpression().getText();\n const matcher = config.analysis.neutralExpressionMatchers.find((item) => callee.startsWith(item.prefix));\n const mapperType = callee.split(\".\").find((segment) => mapperTypes.has(segment));\n\n return {\n transparent: true,\n mapperType,\n apiMapping: matcher?.apiMapping ?? false\n };\n }\n});\n"],"mappings":"yFAGA,MAAM,EAAc,IAAI,IAAI,CAAC,SAAU,SAAU,UAAW,OAAQ,WAAY,OAAQ,QAAS,QAAQ,CAAC,EAE7F,EAAqB,EAAqB,CACrD,KAAM,0BACN,OAAQ,EAAY,IAAW,CAC7B,GAAI,CAAC,EAAK,iBAAiB,CAAU,EACnC,MAAO,GAGT,IAAM,EAAS,EAAW,cAAc,CAAC,CAAC,QAAQ,EAClD,OAAO,EAAO,SAAS,0BAA0B,KAAM,GAAY,EAAO,WAAW,EAAQ,MAAM,CAAC,CACtG,EACA,OAAQ,EAAY,IAAW,CAC7B,GAAI,CAAC,EAAK,iBAAiB,CAAU,EACnC,OAGF,IAAM,EAAS,EAAW,cAAc,CAAC,CAAC,QAAQ,EAC5C,EAAU,EAAO,SAAS,0BAA0B,KAAM,GAAS,EAAO,WAAW,EAAK,MAAM,CAAC,EAGvG,MAAO,CACL,YAAa,GACb,WAJiB,EAAO,MAAM,GAAG,CAAC,CAAC,KAAM,GAAY,EAAY,IAAI,CAAO,CAInE,EACT,WAAY,GAAS,YAAc,EACrC,CACF,CACF,CAAC"}
@@ -1,4 +1,4 @@
1
- import { n as FieldExtractionRule } from "../types-3y34Gf8R.mjs";
1
+ import { i as FieldExtractionRule } from "../types-smD5SZe9.mjs";
2
2
  //#region src/rules/memberGetFieldRule.d.ts
3
3
  declare const memberGetFieldRule: FieldExtractionRule;
4
4
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"memberGetFieldRule.mjs","names":[],"sources":["../../src/rules/memberGetFieldRule.ts"],"sourcesContent":["import { Node } from \"ts-morph\";\nimport { defineExpressionRule } from \"../utils/defineExpressionRule\";\n\nexport const memberGetFieldRule = defineExpressionRule({\n name: \"member-get-field\",\n match: (expression) => {\n if (!Node.isCallExpression(expression)) return false;\n\n const target = expression.getExpression();\n const field = expression\n .getArguments()\n .find((argument) => Node.isStringLiteral(argument) || Node.isNoSubstitutionTemplateLiteral(argument));\n return Node.isPropertyAccessExpression(target) && [\"get\", \"getAll\"].includes(target.getName()) && Boolean(field);\n },\n parse: (expression) => {\n if (!Node.isCallExpression(expression)) return undefined;\n\n const target = expression.getExpression();\n const field = expression\n .getArguments()\n .find((argument) => Node.isStringLiteral(argument) || Node.isNoSubstitutionTemplateLiteral(argument));\n if (!Node.isPropertyAccessExpression(target) || (!Node.isStringLiteral(field) && !Node.isNoSubstitutionTemplateLiteral(field))) {\n return undefined;\n }\n\n return { backendField: `${target.getExpression().getText()}.${field.getLiteralValue()}` };\n }\n});\n"],"mappings":"yFAGA,MAAa,EAAqB,EAAqB,CACrD,KAAM,mBACN,MAAQ,GAAe,CACrB,GAAI,CAAC,EAAK,iBAAiB,CAAU,EAAG,MAAO,GAE/C,IAAM,EAAS,EAAW,cAAc,EAClC,EAAQ,EACX,aAAa,CAAC,CACd,KAAM,GAAa,EAAK,gBAAgB,CAAQ,GAAK,EAAK,gCAAgC,CAAQ,CAAC,EACtG,OAAO,EAAK,2BAA2B,CAAM,GAAK,CAAC,MAAO,QAAQ,CAAC,CAAC,SAAS,EAAO,QAAQ,CAAC,GAAK,EAAQ,CAC5G,EACA,MAAQ,GAAe,CACrB,GAAI,CAAC,EAAK,iBAAiB,CAAU,EAAG,OAExC,IAAM,EAAS,EAAW,cAAc,EAClC,EAAQ,EACX,aAAa,CAAC,CACd,KAAM,GAAa,EAAK,gBAAgB,CAAQ,GAAK,EAAK,gCAAgC,CAAQ,CAAC,EAClG,MAAC,EAAK,2BAA2B,CAAM,GAAM,CAAC,EAAK,gBAAgB,CAAK,GAAK,CAAC,EAAK,gCAAgC,CAAK,GAI5H,MAAO,CAAE,aAAc,GAAG,EAAO,cAAc,CAAC,CAAC,QAAQ,EAAE,GAAG,EAAM,gBAAgB,GAAI,CAC1F,CACF,CAAC"}
1
+ {"version":3,"file":"memberGetFieldRule.mjs","names":[],"sources":["../../src/rules/memberGetFieldRule.ts"],"sourcesContent":["import { Node } from \"ts-morph\";\nimport { defineExpressionRule } from \"../utils/defineExpressionRule\";\n\nexport const memberGetFieldRule = defineExpressionRule({\n name: \"member-get-field\",\n match: (expression) => {\n if (!Node.isCallExpression(expression)) {\n return false;\n }\n\n const target = expression.getExpression();\n const field = expression\n .getArguments()\n .find((argument) => Node.isStringLiteral(argument) || Node.isNoSubstitutionTemplateLiteral(argument));\n return Node.isPropertyAccessExpression(target) && [\"get\", \"getAll\"].includes(target.getName()) && Boolean(field);\n },\n parse: (expression) => {\n if (!Node.isCallExpression(expression)) {\n return undefined;\n }\n\n const target = expression.getExpression();\n const field = expression\n .getArguments()\n .find((argument) => Node.isStringLiteral(argument) || Node.isNoSubstitutionTemplateLiteral(argument));\n if (!Node.isPropertyAccessExpression(target) || (!Node.isStringLiteral(field) && !Node.isNoSubstitutionTemplateLiteral(field))) {\n return undefined;\n }\n\n return { backendField: `${target.getExpression().getText()}.${field.getLiteralValue()}` };\n }\n});\n"],"mappings":"yFAGA,MAAa,EAAqB,EAAqB,CACrD,KAAM,mBACN,MAAQ,GAAe,CACrB,GAAI,CAAC,EAAK,iBAAiB,CAAU,EACnC,MAAO,GAGT,IAAM,EAAS,EAAW,cAAc,EAClC,EAAQ,EACX,aAAa,CAAC,CACd,KAAM,GAAa,EAAK,gBAAgB,CAAQ,GAAK,EAAK,gCAAgC,CAAQ,CAAC,EACtG,OAAO,EAAK,2BAA2B,CAAM,GAAK,CAAC,MAAO,QAAQ,CAAC,CAAC,SAAS,EAAO,QAAQ,CAAC,GAAK,EAAQ,CAC5G,EACA,MAAQ,GAAe,CACrB,GAAI,CAAC,EAAK,iBAAiB,CAAU,EACnC,OAGF,IAAM,EAAS,EAAW,cAAc,EAClC,EAAQ,EACX,aAAa,CAAC,CACd,KAAM,GAAa,EAAK,gBAAgB,CAAQ,GAAK,EAAK,gCAAgC,CAAQ,CAAC,EAClG,MAAC,EAAK,2BAA2B,CAAM,GAAM,CAAC,EAAK,gBAAgB,CAAK,GAAK,CAAC,EAAK,gCAAgC,CAAK,GAI5H,MAAO,CAAE,aAAc,GAAG,EAAO,cAAc,CAAC,CAAC,QAAQ,EAAE,GAAG,EAAM,gBAAgB,GAAI,CAC1F,CACF,CAAC"}
@@ -1,4 +1,4 @@
1
- import { n as FieldExtractionRule } from "../types-3y34Gf8R.mjs";
1
+ import { i as FieldExtractionRule } from "../types-smD5SZe9.mjs";
2
2
  //#region src/rules/quableI18nFieldRule.d.ts
3
3
  declare const quableI18nFieldRule: FieldExtractionRule;
4
4
  //#endregion
@@ -18,6 +18,21 @@ interface NeutralExpressionMatcher {
18
18
  prefix: string;
19
19
  apiMapping?: boolean;
20
20
  }
21
+ type BackendProperty = {
22
+ document: string;
23
+ field: string;
24
+ description?: string;
25
+ } | {
26
+ route: string;
27
+ method: string;
28
+ field: string;
29
+ description?: string;
30
+ };
31
+ interface BackendSource {
32
+ name: string;
33
+ resolve?: () => Promise<BackendProperty[]>;
34
+ rules?: FieldExtractionRule[];
35
+ }
21
36
  interface UserConfig {
22
37
  /**
23
38
  * Absolute path to the root of the repository to analyze (e.g. the digital-api repo).
@@ -59,7 +74,7 @@ interface UserConfig {
59
74
  */
60
75
  analysis: {
61
76
  /** Backend identifiers used by the API project. */
62
- backends: string[];
77
+ backends: Array<string | BackendSource>;
63
78
  /**
64
79
  * Glob patterns excluded from `analysis_files`.
65
80
  * Use this to hide technical plumbing files that add noise to route review documents.
@@ -98,5 +113,5 @@ interface UserConfig {
98
113
  };
99
114
  }
100
115
  //#endregion
101
- export { FieldExtractionRule as n, defineExpressionRule as r, UserConfig as t };
102
- //# sourceMappingURL=types-3y34Gf8R.d.mts.map
116
+ export { defineExpressionRule as a, FieldExtractionRule as i, BackendSource as n, UserConfig as r, BackendProperty as t };
117
+ //# sourceMappingURL=types-smD5SZe9.d.mts.map
@@ -1,4 +1,4 @@
1
- import { t as generateBackendTopologyArtifacts } from "../routeBackendTopologyService-D_aIIBbX.mjs";
1
+ import { t as generateBackendTopologyArtifacts } from "../routeBackendTopologyService-DkNyCtKt.mjs";
2
2
  import { parentPort, workerData } from "node:worker_threads";
3
3
  //#region src/workers/routeBackendTopologyWorker.ts
4
4
  const data = workerData;
@@ -97,9 +97,11 @@ Cette limite explique les valeurs `method: null` et `route: unknown` observées
97
97
  Avec `@directus/sdk`, le code métier ne manipule généralement pas de route HTTP explicite. Il décrit une opération :
98
98
 
99
99
  ```ts
100
- client.request(readItems("offers", {
101
- fields: ["id", "label"]
102
- }));
100
+ client.request(
101
+ readItems("offers", {
102
+ fields: ["id", "label"]
103
+ })
104
+ );
103
105
  ```
104
106
 
105
107
  La méthode et l'URL HTTP sont construites par le SDK. Chercher uniquement un littéral de route dans le code ne peut donc pas produire un résultat fiable.
package/package.json CHANGED
@@ -1,10 +1,9 @@
1
1
  {
2
2
  "name": "@cmflow/atlas",
3
- "version": "3.4.0-beta.7",
3
+ "version": "3.4.0-beta.8",
4
4
  "description": "API-to-backend mapping catalogue for Club Med Flow",
5
5
  "license": "MIT",
6
6
  "author": "romakita",
7
- "email": "rom.lenzotti@gmail.com",
8
7
  "bin": "./dist/bin/atlas.mjs",
9
8
  "type": "module",
10
9
  "main": "./dist/index.mjs",
@@ -46,6 +45,7 @@
46
45
  "typescript": "^7.0.2",
47
46
  "vitest": "^3.0.8"
48
47
  },
48
+ "email": "rom.lenzotti@gmail.com",
49
49
  "peerDependencies": {},
50
50
  "publishConfig": {
51
51
  "tag": "beta"