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

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,175 @@ 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
+ When a string and a source share the same name, the last declaration wins. This lets a source enrich a list of backend names, for example `[..., Object.values(BackendTypes), quableBackendSource]`.
74
+
75
+ The resolver returns one of the following shapes. Document-oriented backends have no route; REST backends have no document.
76
+
77
+ ```ts
78
+ type BackendProperty =
79
+ | {
80
+ document: string;
81
+ field: string;
82
+ description?: string;
83
+ }
84
+ | {
85
+ route: string;
86
+ method: string;
87
+ field: string;
88
+ description?: string;
89
+ };
90
+ ```
91
+
92
+ 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`.
93
+
94
+ ### Quable
95
+
96
+ 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.
97
+
98
+ ```ts
99
+ import { defineBackendSource, httpClient, quableI18nFieldRule } from "@cmflow/atlas";
100
+
101
+ export const quable = defineBackendSource({
102
+ name: "QUABLE",
103
+ rules: [quableI18nFieldRule],
104
+ resolve: async () => {
105
+ const documentTypes = await httpClient.get<QuableDocumentType[]>("https://quable.example/document-types");
106
+
107
+ return documentTypes.flatMap((document) =>
108
+ document.properties.map((property) => ({
109
+ document: document.code,
110
+ field: property.code,
111
+ description: "récupéré via le backend resolver"
112
+ }))
113
+ );
114
+ }
115
+ });
116
+ ```
117
+
118
+ For example, the resolver above may return:
119
+
120
+ ```ts
121
+ {
122
+ backend: "QUABLE",
123
+ document: "products",
124
+ field: "product_geographical_area",
125
+ description: "récupéré via le backend resolver"
126
+ }
127
+ ```
128
+
129
+ ### CMS Directus
130
+
131
+ CMS Directus follows the same document-oriented contract. Its `cmsI18nFieldRule` is owned by the CMS source rather than registered as a global expression rule.
132
+
133
+ ```ts
134
+ import { defineBackendSource, cmsI18nFieldRule } from "@cmflow/atlas";
135
+
136
+ export const cmsDirectus = defineBackendSource({
137
+ name: "CMS",
138
+ rules: [cmsI18nFieldRule],
139
+ resolve: async () => directusResolver.listProperties()
140
+ });
141
+ ```
142
+
143
+ `directusResolver.listProperties()` returns items such as `{ document: "offers", field: "title", description: "…" }`.
144
+
145
+ ### CMS Legacy
146
+
147
+ CMS Legacy is configured identically, with its own resolver and backend identifier. It may reuse `cmsI18nFieldRule` when the backend uses `CmsI18n` helpers.
148
+
149
+ ```ts
150
+ import { defineBackendSource, cmsI18nFieldRule } from "@cmflow/atlas";
151
+
152
+ export const cmsLegacy = defineBackendSource({
153
+ name: "CMS_LEGACY",
154
+ rules: [cmsI18nFieldRule],
155
+ resolve: async () => cmsLegacyResolver.listProperties()
156
+ });
157
+ ```
158
+
159
+ ### Custom REST backend with OpenAPI
160
+
161
+ `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.
162
+
163
+ ```ts
164
+ import { constant, defineBackendSource, openapiSource } from "@cmflow/atlas";
165
+
166
+ export const xm = defineBackendSource({
167
+ name: "XM",
168
+ resolve: () =>
169
+ openapiSource({
170
+ url: constant<string>("XM_API_URL", "https://xm.example/openapi.json")
171
+ })
172
+ });
173
+ ```
174
+
175
+ `constant` resolves an environment variable directly from `process.env`, falling back to its second argument. No resolver context is required.
176
+
177
+ ### Environment constants
178
+
179
+ Use `constant` when a backend source needs a configurable value such as a URL or an access token. The value is resolved when the source runs:
180
+
181
+ 1. Atlas uses `process.env[name]` when it is set.
182
+ 2. Otherwise, it returns the supplied default value.
183
+
184
+ ```ts
185
+ import { constant, defineBackendSource, openapiSource } from "@cmflow/atlas";
186
+
187
+ export const xmBackendSource = defineBackendSource({
188
+ name: "XM",
189
+ resolve: () =>
190
+ openapiSource({
191
+ url: constant<string>("XM_API_URL", "https://xm.example/openapi.json")
192
+ })
193
+ });
194
+ ```
195
+
196
+ For example, override the default in CI or locally:
197
+
198
+ ```bash
199
+ XM_API_URL=https://xm.internal/openapi.json atlas backend-sources --backend XM
200
+ ```
201
+
202
+ This returns entries such as:
203
+
204
+ ```ts
205
+ {
206
+ backend: "XM",
207
+ route: "/path/to",
208
+ method: "GET",
209
+ field: "path.to.field",
210
+ description: "Description extraite du Swagger"
211
+ }
212
+ ```
213
+
214
+ Register strings and sources together:
215
+
216
+ ```ts
217
+ import { defineConfig } from "@cmflow/atlas";
218
+ import { cmsDirectus, cmsLegacy, quable, xm } from "./backend-sources";
219
+
220
+ export default defineConfig({
221
+ // …other Atlas options
222
+ analysis: {
223
+ backends: ["ICC", quable, cmsDirectus, cmsLegacy, xm]
224
+ }
225
+ });
226
+ ```
63
227
 
64
228
  ## Custom expression rules
65
229
 
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`.
230
+ 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
231
 
68
232
  `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
233
 
@@ -73,8 +237,7 @@ import { Node } from "ts-morph";
73
237
 
74
238
  const localizedFieldRule = defineExpressionRule({
75
239
  name: "localized-field",
76
- match: (expression) =>
77
- Node.isCallExpression(expression) && expression.getExpression().getText() === "localizedField",
240
+ match: (expression) => Node.isCallExpression(expression) && expression.getExpression().getText() === "localizedField",
78
241
  parse: (expression) => {
79
242
  if (!Node.isCallExpression(expression)) return undefined;
80
243
 
@@ -151,6 +314,7 @@ Without `--write`, the command performs a dry run. Use `clean-orphans` to inspec
151
314
 
152
315
  ```text
153
316
  atlas init Create atlas.config.ts
317
+ atlas backend-sources Execute backend sources and display resolved metadata
154
318
  atlas generate:graph Trace API routes to backends
155
319
  atlas generate:catalog [route] Generate route review documents
156
320
  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-DElirHSh.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,44 @@ 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 resolvableSources = sources.filter((source) => Boolean(source.resolve));
2804
+ const selectedSources = backend ? resolvableSources.filter((source) => source.name === backend) : resolvableSources;
2805
+ if (backend && !selectedSources.length) throw new Error(`Unknown backend source with a resolver: ${backend}`);
2806
+ if (!backend && !selectedSources.length) throw new Error("No backend source with a resolver is configured");
2807
+ const propertiesByBackend = await resolveBackendProperties(selectedSources);
2808
+ return selectedSources.map((source) => ({
2809
+ backend: source.name,
2810
+ properties: propertiesByBackend.get(source.name) || []
2811
+ }));
2812
+ }
2813
+ 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) => {
2814
+ intro("Atlas backend source metadata");
2815
+ try {
2816
+ const sources = getUserConfig().analysis.backends.filter((source) => Boolean(source.resolve));
2817
+ const selectedBackend = options.backend || await select({
2818
+ message: "Which backend source do you want to execute?",
2819
+ options: sources.map((source) => ({
2820
+ value: source.name,
2821
+ label: source.name,
2822
+ hint: "resolver configured"
2823
+ }))
2824
+ });
2825
+ if (isCancel(selectedBackend)) {
2826
+ cancel("Backend source execution cancelled");
2827
+ process.exitCode = 1;
2828
+ return;
2829
+ }
2830
+ const metadata = await listBackendSourceMetadata(sources, selectedBackend);
2831
+ log.message(JSON.stringify(metadata, null, 2));
2832
+ outro("Backend source metadata generated");
2833
+ } catch (error) {
2834
+ cancel(error instanceof Error ? error.message : String(error));
2835
+ process.exitCode = 1;
2836
+ }
2837
+ });
2838
+ //#endregion
2731
2839
  //#region src/services/analysisProfileService.ts
2732
2840
  function createAnalysisProfile(enabled) {
2733
2841
  const durations = /* @__PURE__ */ new Map();
@@ -3042,7 +3150,7 @@ var generateTest_default = (program) => void program.command("generate:test").de
3042
3150
  }
3043
3151
  });
3044
3152
  //#endregion
3045
- //#region src/services/aiSdkClient.ts
3153
+ //#region src/services/ai/aiSdkClient.ts
3046
3154
  var AISdkClient = class {
3047
3155
  #config;
3048
3156
  constructor(config) {
@@ -3073,7 +3181,7 @@ var AISdkClient = class {
3073
3181
  }
3074
3182
  };
3075
3183
  //#endregion
3076
- //#region src/services/inferenceService.ts
3184
+ //#region src/services/ai/inferenceService.ts
3077
3185
  const MAPPING_OUTPUT_SCHEMA = z.object({ mappings: z.array(z.object({
3078
3186
  property_index: z.number().int(),
3079
3187
  candidate_index: z.number().int(),
@@ -4209,6 +4317,7 @@ program.name("atlas").description("Manage the API-to-backend mapping catalogue")
4209
4317
  setUserConfig(config);
4210
4318
  });
4211
4319
  init_default(program, program);
4320
+ backendSources_default(program);
4212
4321
  generate_default(program);
4213
4322
  generateGraph_default(program);
4214
4323
  generateTest_default(program);
@@ -1 +1 @@
1
- {"version":3,"file":"defineExpressionRule-Dfvzj6n2.mjs","names":[],"sources":["../src/utils/defineExpressionRule.ts"],"sourcesContent":["import type { Expression } from \"ts-morph\";\nimport type { UserConfig } from \"../models/types\";\n\nexport type FieldExtractionRuleResult = {\n backendField?: string;\n transparent?: boolean;\n mapperType?: string;\n apiMapping?: boolean;\n};\n\nexport type FieldExtractionRule = {\n name: string;\n match: (expression: Expression, config: UserConfig) => boolean;\n parse: (expression: Expression, config: UserConfig) => FieldExtractionRuleResult | undefined;\n};\n\nexport function defineExpressionRule(rule: FieldExtractionRule): FieldExtractionRule {\n return rule;\n}\n"],"mappings":"AAgBA,SAAgB,EAAqB,EAAgD,CACnF,OAAO,CACT"}
1
+ {"version":3,"file":"defineExpressionRule-Dfvzj6n2.mjs","names":[],"sources":["../src/utils/defineExpressionRule.ts"],"sourcesContent":["import type { Expression } from \"ts-morph\";\nimport type { UserConfig } from \"../models/types\";\n\nexport type FieldExtractionRuleResult = {\n backendField?: string;\n transparent?: boolean;\n mapperType?: string;\n apiMapping?: boolean;\n};\n\nexport type FieldExtractionRule = {\n name: string;\n /** Higher-priority rules are evaluated first. Defaults to 0. */\n priority?: number;\n match: (expression: Expression, config: UserConfig) => boolean;\n parse: (expression: Expression, config: UserConfig) => FieldExtractionRuleResult | undefined;\n};\n\nexport function defineExpressionRule(rule: FieldExtractionRule): FieldExtractionRule {\n return rule;\n}\n"],"mappings":"AAkBA,SAAgB,EAAqB,EAAgD,CACnF,OAAO,CACT"}
package/dist/index.d.mts CHANGED
@@ -1,6 +1,36 @@
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-cYprdLUO.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/utils/constant.d.ts
11
+ /**
12
+ * Resolves an environment variable, falling back to the supplied default value.
13
+ *
14
+ * This is intended for values declared directly in an Atlas configuration, such
15
+ * as URLs or tokens used by a backend source.
16
+ */
17
+ declare function constant<T extends string = string>(name: string, defaultValue: T): string;
18
+ //#endregion
19
+ //#region src/services/http/httpClient.d.ts
20
+ declare class HttpClient {
21
+ fetch(url: string, init: RequestInit & {
22
+ onProgress: (content: string) => void;
23
+ }): Promise<string | unknown>;
24
+ fetch(url: string, init?: RequestInit): Promise<Response>;
25
+ get<T>(url: string, init?: RequestInit): Promise<T>;
26
+ }
27
+ declare const httpClient: HttpClient;
28
+ //#endregion
29
+ //#region src/services/openapi/openapiSource.d.ts
30
+ declare function openapiSource(params: {
31
+ url: string;
32
+ timeoutMs?: number;
33
+ }): Promise<BackendProperty[]>;
34
+ //#endregion
35
+ export { type BackendProperty, type BackendSource, type UserConfig, cmsI18nFieldRule, constant, defineBackendSource, defineConfig, defineExpressionRule, httpClient, openapiSource, quableI18nFieldRule };
6
36
  //# 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}function s(e,t){return process.env[e]??t}const c=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 l(e){return e<1e3?`${e}ms`:`${(e/1e3).toFixed(1)}s`}const u=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=l(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=l(Date.now()-n.startedAt);t.stop(`${e} (${r})`),n=void 0}}}};async function d(e,t){try{let n=await c.fetch(e,{signal:t?AbortSignal.timeout(t):void 0,onProgress(e){u.log(`Downloading (${(e.length/1048576).toFixed(1)} MB)`)}});return!n||typeof n==`object`?n:(u.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 f(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 p(e){return/^\d+$/.test(e)&&Number(e)>=200&&Number(e)<=299}function m(e,t){if(!e||typeof e!=`object`)return;if(e[`application/json`]?.schema)return f(e[`application/json`].schema,t);let n=Object.values(e).find(e=>e?.schema);return n?f(n.schema,t):void 0}function h(e,t,n=``){let r=f(e,t);if(!r||typeof r!=`object`)return[];if(Array.isArray(r.allOf))return r.allOf.flatMap(e=>h(e,t,n));if(r.type===`array`||r.items){let e=n?`${n}[]`:`[]`;return h(r.items,t,e)}let i=r.properties||{};return Object.keys(i).length?Object.entries(i).flatMap(([e,r])=>h(r,t,n?`${n}.${e}`:e)):n?[{path:n,description:r.description,deprecated:r.deprecated}]:[]}function g(e){return e===`query`?`QUERY`:e===`path`?`PATH`:e===`header`?`HEADER`:null}function _(e,t){let n=new Map,r=Array.isArray(e.parameters)?e.parameters:[];for(let e of r){let r=f(e,t),i=g(r?.in);if(!r||!i||!r.name)continue;let a=h(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=f(e.requestBody,t);if(i?.content){let e=m(i.content,t);for(let r of h(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 v(e,t){let n=new Map,r=e?.responses||{};for(let[e,i]of Object.entries(r)){if(!p(e))continue;let r=f(i,t),a=m(r?.content,t);for(let e of h(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 y(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[..._(a,e),...v(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 b(e){return y(await d(e.url,e.timeoutMs))}export{t as cmsI18nFieldRule,s as constant,o as defineBackendSource,a as defineConfig,e as defineExpressionRule,c as httpClient,b 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/utils/constant.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","/**\n * Resolves an environment variable, falling back to the supplied default value.\n *\n * This is intended for values declared directly in an Atlas configuration, such\n * as URLs or tokens used by a backend source.\n */\nexport function constant<T extends string = string>(name: string, defaultValue: T): string {\n return process.env[name] ?? defaultValue;\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,37 @@ 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: normalizeExpressionRules([...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 sourcesByName = /* @__PURE__ */ new Map();
26
+ for (const source of sources) {
27
+ if (!source.name.trim()) throw new Error("Backend source names must not be empty");
28
+ sourcesByName.set(source.name, source);
29
+ }
30
+ return [...sourcesByName.values()];
31
+ }
32
+ function normalizeExpressionRules(rules) {
33
+ return [...new Set(rules)].map((rule, index) => ({
34
+ rule,
35
+ index
36
+ })).sort((left, right) => (right.rule.priority || 0) - (left.rule.priority || 0) || left.index - right.index).map(({ rule }) => rule);
14
37
  }
15
38
  function getUserConfig() {
16
39
  if (!_config) throw new Error("Atlas config not loaded. Run Atlas from a directory containing atlas.config.ts or pass --config.");
17
40
  return _config;
18
41
  }
19
- function getTsconfigAliases(repoRoot) {
20
- const tsconfigPath = path.join(repoRoot, "tsconfig.json");
42
+ function getTsconfigAliases(workingDirectory = process.cwd()) {
43
+ const tsconfigPath = path.join(workingDirectory, "tsconfig.json");
21
44
  if (!fs.existsSync(tsconfigPath)) return {};
22
45
  const paths = new Project({
23
46
  tsConfigFilePath: tsconfigPath,
@@ -39,12 +62,17 @@ async function loadAtlasConfig(configPath, projectRoot) {
39
62
  } else mod = await import(pathToFileURL(filePath).href);
40
63
  const config = mod.default ?? mod;
41
64
  const repoRoot = projectRoot || config.repoRoot || process.cwd();
65
+ const backends = normalizeBackendSources(config.analysis.backends);
42
66
  return {
43
67
  ...config,
44
68
  repoRoot,
45
69
  cwd: repoRoot,
70
+ analysis: {
71
+ ...config.analysis,
72
+ backends
73
+ },
46
74
  resolver: { alias: {
47
- ...getTsconfigAliases(repoRoot),
75
+ ...getTsconfigAliases(),
48
76
  ...config.resolver?.alias
49
77
  } }
50
78
  };
@@ -53,7 +81,7 @@ async function loadAtlasConfig(configPath, projectRoot) {
53
81
  }
54
82
  }
55
83
  //#endregion
56
- //#region src/services/taskProgressService.ts
84
+ //#region src/services/tasks/taskProgressService.ts
57
85
  function formatDuration(durationMs) {
58
86
  if (durationMs < 1e3) return `${durationMs}ms`;
59
87
  return `${(durationMs / 1e3).toFixed(1)}s`;
@@ -138,7 +166,7 @@ function filterAnalysisFiles(filePaths) {
138
166
  //#endregion
139
167
  //#region src/utils/isKnowBackendType.ts
140
168
  function isKnownBackendType(value) {
141
- return getUserConfig().analysis.backends.includes(value);
169
+ return getUserConfig().analysis.backends.some((backend) => backend.name === value);
142
170
  }
143
171
  //#endregion
144
172
  //#region src/services/backendSourceService.ts
@@ -234,14 +262,14 @@ function findLocalCallable(sourceFile, symbol) {
234
262
  }
235
263
  function resolveSourceFile(project, owner, moduleSpecifier, resolvePath) {
236
264
  const resolvedPath = resolvePath(owner.getFilePath(), moduleSpecifier);
237
- if (!resolvedPath) return void 0;
265
+ if (!resolvedPath) return;
238
266
  const existing = project.getSourceFile(resolvedPath);
239
267
  if (existing) return existing;
240
268
  return isFilePath(resolvedPath) ? project.addSourceFileAtPathIfExists(resolvedPath) : void 0;
241
269
  }
242
270
  function resolveExportedCallable(project, sourceFile, symbol, resolvePath, seen = /* @__PURE__ */ new Set()) {
243
271
  const key = `${sourceFile.getFilePath()}:${symbol}`;
244
- if (seen.has(key)) return void 0;
272
+ if (seen.has(key)) return;
245
273
  seen.add(key);
246
274
  const local = findLocalCallable(sourceFile, symbol);
247
275
  if (local) return {
@@ -263,7 +291,7 @@ function resolveExportedCallable(project, sourceFile, symbol, resolvePath, seen
263
291
  }
264
292
  function resolveConstructedMember(project, sourceFile, variableName, memberName, resolvePath) {
265
293
  const initializer = sourceFile.getVariableDeclaration(variableName)?.getInitializer();
266
- if (!initializer || !Node.isNewExpression(initializer)) return void 0;
294
+ if (!initializer || !Node.isNewExpression(initializer)) return;
267
295
  const constructorName = initializer.getExpression().getText();
268
296
  const localMethod = sourceFile.getClass(constructorName)?.getInstanceMethod(memberName);
269
297
  if (localMethod) return {
@@ -324,7 +352,7 @@ function resolveTypedPropertyMethod(project, sourceFile, expression, resolvePath
324
352
  const receiver = expression.getExpression();
325
353
  if (!Node.isPropertyAccessExpression(receiver) || receiver.getExpression().getText() !== "this") return;
326
354
  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;
355
+ if (!propertyType) return;
328
356
  for (const importDeclaration of sourceFile.getImportDeclarations()) {
329
357
  const namedImport = importDeclaration.getNamedImports().find((item) => (item.getAliasNode()?.getText() || item.getName()) === propertyType);
330
358
  if (!namedImport) continue;
@@ -361,10 +389,10 @@ function resolveReference(project, sourceFile, expression, resolvePath) {
361
389
  const imported = resolveImportedReference(project, sourceFile, expressionText, resolvePath);
362
390
  if (imported) return imported;
363
391
  const declaration = expression.getSymbol()?.getAliasedSymbol()?.getDeclarations()[0] || expression.getSymbol()?.getDeclarations()[0];
364
- if (!declaration) return void 0;
392
+ if (!declaration) return;
365
393
  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;
394
+ if (!callable) return;
395
+ if (Node.isVariableDeclaration(callable) && !isCallableVariable(callable)) return;
368
396
  const targetFile = callable.getSourceFile();
369
397
  return {
370
398
  declaration: callable,
@@ -404,7 +432,7 @@ function mappingDirection(symbol, layer) {
404
432
  function callableFromTopologyNode(cwd, project, node) {
405
433
  const filePath = path.resolve(cwd, node.source);
406
434
  const sourceFile = project.getSourceFile(filePath) || project.addSourceFileAtPathIfExists(filePath);
407
- if (!sourceFile) return void 0;
435
+ if (!sourceFile) return;
408
436
  const declaration = [
409
437
  ...sourceFile.getFunctions(),
410
438
  ...sourceFile.getDescendantsOfKind(SyntaxKind.MethodDeclaration),
@@ -574,13 +602,13 @@ function traceBackendPaths(params) {
574
602
  }
575
603
  function stringProperty(object, name) {
576
604
  const property = object.getProperty(name);
577
- if (!property || !Node.isPropertyAssignment(property)) return void 0;
605
+ if (!property || !Node.isPropertyAssignment(property)) return;
578
606
  const initializer = property.getInitializer();
579
607
  return initializer && (Node.isStringLiteral(initializer) || Node.isNoSubstitutionTemplateLiteral(initializer)) ? initializer.getLiteralValue() : void 0;
580
608
  }
581
609
  function handlerProperty(object) {
582
610
  const property = object.getProperty("handler");
583
- if (!property || !Node.isPropertyAssignment(property)) return void 0;
611
+ if (!property || !Node.isPropertyAssignment(property)) return;
584
612
  const initializer = property.getInitializer();
585
613
  return initializer && (Node.isIdentifier(initializer) || Node.isPropertyAccessExpression(initializer)) ? initializer.getText() : void 0;
586
614
  }
@@ -1,4 +1,4 @@
1
- import { n as FieldExtractionRule } from "../types-3y34Gf8R.mjs";
1
+ import { i as FieldExtractionRule } from "../types-cYprdLUO.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-cYprdLUO.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-cYprdLUO.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-cYprdLUO.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-cYprdLUO.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-cYprdLUO.mjs";
2
2
  //#region src/rules/memberGetFieldRule.d.ts
3
3
  declare const memberGetFieldRule: FieldExtractionRule;
4
4
  //#endregion
@@ -1,2 +1,2 @@
1
- import{t as e}from"../defineExpressionRule-Dfvzj6n2.mjs";import{Node as t}from"ts-morph";const n=e({name:`member-get-field`,match:e=>{if(!t.isCallExpression(e))return!1;let n=e.getExpression(),r=e.getArguments().find(e=>t.isStringLiteral(e)||t.isNoSubstitutionTemplateLiteral(e));return t.isPropertyAccessExpression(n)&&[`get`,`getAll`].includes(n.getName())&&!!r},parse:e=>{if(!t.isCallExpression(e))return;let n=e.getExpression(),r=e.getArguments().find(e=>t.isStringLiteral(e)||t.isNoSubstitutionTemplateLiteral(e));if(!(!t.isPropertyAccessExpression(n)||!t.isStringLiteral(r)&&!t.isNoSubstitutionTemplateLiteral(r)))return{backendField:`${n.getExpression().getText()}.${r.getLiteralValue()}`}}});export{n as memberGetFieldRule};
1
+ import{t as e}from"../defineExpressionRule-Dfvzj6n2.mjs";import{Node as t}from"ts-morph";const n=e({name:`member-get-field`,priority:-100,match:e=>{if(!t.isCallExpression(e))return!1;let n=e.getExpression(),r=e.getArguments().find(e=>t.isStringLiteral(e)||t.isNoSubstitutionTemplateLiteral(e));return t.isPropertyAccessExpression(n)&&[`get`,`getAll`].includes(n.getName())&&!!r},parse:e=>{if(!t.isCallExpression(e))return;let n=e.getExpression(),r=e.getArguments().find(e=>t.isStringLiteral(e)||t.isNoSubstitutionTemplateLiteral(e));if(!(!t.isPropertyAccessExpression(n)||!t.isStringLiteral(r)&&!t.isNoSubstitutionTemplateLiteral(r)))return{backendField:`${n.getExpression().getText()}.${r.getLiteralValue()}`}}});export{n as memberGetFieldRule};
2
2
  //# sourceMappingURL=memberGetFieldRule.mjs.map
@@ -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 priority: -100,\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,SAAU,KACV,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-cYprdLUO.mjs";
2
2
  //#region src/rules/quableI18nFieldRule.d.ts
3
3
  declare const quableI18nFieldRule: FieldExtractionRule;
4
4
  //#endregion
@@ -8,6 +8,8 @@ type FieldExtractionRuleResult = {
8
8
  };
9
9
  type FieldExtractionRule = {
10
10
  name: string;
11
+ /** Higher-priority rules are evaluated first. Defaults to 0. */
12
+ priority?: number;
11
13
  match: (expression: Expression, config: UserConfig) => boolean;
12
14
  parse: (expression: Expression, config: UserConfig) => FieldExtractionRuleResult | undefined;
13
15
  };
@@ -18,6 +20,21 @@ interface NeutralExpressionMatcher {
18
20
  prefix: string;
19
21
  apiMapping?: boolean;
20
22
  }
23
+ type BackendProperty = {
24
+ document: string;
25
+ field: string;
26
+ description?: string;
27
+ } | {
28
+ route: string;
29
+ method: string;
30
+ field: string;
31
+ description?: string;
32
+ };
33
+ interface BackendSource {
34
+ name: string;
35
+ resolve?: () => Promise<BackendProperty[]>;
36
+ rules?: FieldExtractionRule[];
37
+ }
21
38
  interface UserConfig {
22
39
  /**
23
40
  * Absolute path to the root of the repository to analyze (e.g. the digital-api repo).
@@ -59,7 +76,7 @@ interface UserConfig {
59
76
  */
60
77
  analysis: {
61
78
  /** Backend identifiers used by the API project. */
62
- backends: string[];
79
+ backends: Array<string | BackendSource>;
63
80
  /**
64
81
  * Glob patterns excluded from `analysis_files`.
65
82
  * Use this to hide technical plumbing files that add noise to route review documents.
@@ -98,5 +115,5 @@ interface UserConfig {
98
115
  };
99
116
  }
100
117
  //#endregion
101
- export { FieldExtractionRule as n, defineExpressionRule as r, UserConfig as t };
102
- //# sourceMappingURL=types-3y34Gf8R.d.mts.map
118
+ export { defineExpressionRule as a, FieldExtractionRule as i, BackendSource as n, UserConfig as r, BackendProperty as t };
119
+ //# sourceMappingURL=types-cYprdLUO.d.mts.map
@@ -1,4 +1,4 @@
1
- import { t as generateBackendTopologyArtifacts } from "../routeBackendTopologyService-D_aIIBbX.mjs";
1
+ import { t as generateBackendTopologyArtifacts } from "../routeBackendTopologyService-DElirHSh.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.9",
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"