@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 +136 -4
- package/dist/bin/atlas.mjs +177 -70
- package/dist/index.d.mts +23 -2
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/{routeBackendTopologyService-D_aIIBbX.mjs → routeBackendTopologyService-DkNyCtKt.mjs} +39 -16
- package/dist/rules/cleanObjectRule.d.mts +1 -1
- package/dist/rules/cmsI18nFieldRule.d.mts +1 -1
- package/dist/rules/dateConversionRule.d.mts +1 -1
- package/dist/rules/lodashGetRule.d.mts +1 -1
- package/dist/rules/mappingUtilityRule.d.mts +1 -1
- package/dist/rules/mappingUtilityRule.mjs.map +1 -1
- package/dist/rules/memberGetFieldRule.d.mts +1 -1
- package/dist/rules/memberGetFieldRule.mjs.map +1 -1
- package/dist/rules/quableI18nFieldRule.d.mts +1 -1
- package/dist/{types-3y34Gf8R.d.mts → types-smD5SZe9.d.mts} +18 -3
- package/dist/workers/routeBackendTopologyWorker.mjs +1 -1
- package/knowledges/cms-and-directus-indirect-routes.md +5 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -59,11 +59,143 @@ export default defineConfig({
|
|
|
59
59
|
});
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
Atlas reads `compilerOptions.paths` from the
|
|
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
|
|
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
|
package/dist/bin/atlas.mjs
CHANGED
|
@@ -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-
|
|
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
|
|
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
|
|
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
|
|
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/
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2171
|
-
|
|
2172
|
-
for (const warning of warnings)
|
|
2173
|
-
|
|
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
|
|
2388
|
-
const
|
|
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"} ${
|
|
2460
|
+
route: `${mapping.method || "CALL"} ${backendRoute}`,
|
|
2393
2461
|
source_file: mapping.source_file,
|
|
2394
2462
|
provenance: "code_analysis"
|
|
2395
2463
|
};
|
|
2396
|
-
backendRoutesByKey.set(backendRouteKey,
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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
|
package/dist/index.mjs.map
CHANGED
|
@@ -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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"}
|
package/dist/{routeBackendTopologyService-D_aIIBbX.mjs → routeBackendTopologyService-DkNyCtKt.mjs}
RENAMED
|
@@ -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
|
-
|
|
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(
|
|
20
|
-
const tsconfigPath = path.join(
|
|
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(
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
387
|
+
if (!declaration) return;
|
|
365
388
|
const callable = Node.isFunctionDeclaration(declaration) || Node.isMethodDeclaration(declaration) || Node.isVariableDeclaration(declaration) ? declaration : void 0;
|
|
366
|
-
if (!callable) return
|
|
367
|
-
if (Node.isVariableDeclaration(callable) && !isCallableVariable(callable)) return
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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 +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,
|
|
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 +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,
|
|
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"}
|
|
@@ -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,
|
|
102
|
-
//# sourceMappingURL=types-
|
|
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-
|
|
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(
|
|
101
|
-
|
|
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.
|
|
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"
|