@cmflow/atlas 3.4.0-beta.6 → 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 +139 -7
- package/dist/bin/atlas.mjs +203 -386
- package/dist/index.d.mts +23 -2
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/routeBackendTopologyService-DkNyCtKt.mjs +815 -0
- 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 +29 -0
- package/knowledges/cms-and-directus-indirect-routes.md +5 -3
- package/package.json +2 -2
package/dist/bin/atlas.mjs
CHANGED
|
@@ -1,156 +1,23 @@
|
|
|
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-DkNyCtKt.mjs";
|
|
2
3
|
import { Command, InvalidArgumentError, Option } from "commander";
|
|
3
4
|
import path from "node:path";
|
|
4
|
-
import fs from "node:fs";
|
|
5
|
-
import { pathToFileURL } from "node:url";
|
|
6
5
|
import { Node, Project, SyntaxKind } from "ts-morph";
|
|
7
|
-
import { cancel, intro, log, note, outro, progress,
|
|
8
|
-
import fs
|
|
6
|
+
import { cancel, intro, isCancel, log, note, outro, progress, select } from "@clack/prompts";
|
|
7
|
+
import fs from "node:fs/promises";
|
|
9
8
|
import { createDirectus, createItem, deleteItem, readItems, rest, staticToken, updateItem } from "@directus/sdk";
|
|
10
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
11
9
|
import { globby } from "globby";
|
|
12
|
-
import { minimatch } from "minimatch";
|
|
13
10
|
import { createHash } from "node:crypto";
|
|
14
11
|
import { parse, stringify } from "yaml";
|
|
15
|
-
import { Worker } from "node:worker_threads";
|
|
16
12
|
import { NoObjectGeneratedError, Output, generateText } from "ai";
|
|
17
13
|
import { z } from "zod";
|
|
18
14
|
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
|
19
|
-
//#region src/utils/config.ts
|
|
20
|
-
let _config;
|
|
21
|
-
function setUserConfig(config) {
|
|
22
|
-
_config = config;
|
|
23
|
-
}
|
|
24
|
-
function getUserConfig() {
|
|
25
|
-
if (!_config) throw new Error("Atlas config not loaded. Run Atlas from a directory containing atlas.config.ts or pass --config.");
|
|
26
|
-
return _config;
|
|
27
|
-
}
|
|
28
|
-
function getTsconfigAliases(repoRoot) {
|
|
29
|
-
const tsconfigPath = path.join(repoRoot, "tsconfig.json");
|
|
30
|
-
if (!fs.existsSync(tsconfigPath)) return {};
|
|
31
|
-
const paths = new Project({
|
|
32
|
-
tsConfigFilePath: tsconfigPath,
|
|
33
|
-
skipAddingFilesFromTsConfig: true
|
|
34
|
-
}).getCompilerOptions().paths ?? {};
|
|
35
|
-
return Object.fromEntries(Object.entries(paths).flatMap(([alias, targets]) => {
|
|
36
|
-
const target = targets?.[0];
|
|
37
|
-
if (!target) return [];
|
|
38
|
-
return [[alias.replace(/\/\*$/, ""), target.replace(/^\.\//, "").replace(/\/\*$/, "")]];
|
|
39
|
-
}));
|
|
40
|
-
}
|
|
41
|
-
async function loadAtlasConfig(configPath, projectRoot) {
|
|
42
|
-
const filePath = path.resolve(configPath);
|
|
43
|
-
try {
|
|
44
|
-
let mod;
|
|
45
|
-
if (filePath.endsWith(".ts")) {
|
|
46
|
-
const { tsImport } = await import("tsx/esm/api");
|
|
47
|
-
mod = await tsImport(filePath, import.meta.url);
|
|
48
|
-
} else mod = await import(pathToFileURL(filePath).href);
|
|
49
|
-
const config = mod.default ?? mod;
|
|
50
|
-
const repoRoot = projectRoot || config.repoRoot || process.cwd();
|
|
51
|
-
return {
|
|
52
|
-
...config,
|
|
53
|
-
repoRoot,
|
|
54
|
-
cwd: repoRoot,
|
|
55
|
-
resolver: { alias: {
|
|
56
|
-
...getTsconfigAliases(repoRoot),
|
|
57
|
-
...config.resolver?.alias
|
|
58
|
-
} }
|
|
59
|
-
};
|
|
60
|
-
} catch (error) {
|
|
61
|
-
throw new Error(`Unable to load Atlas configuration at ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
//#endregion
|
|
65
|
-
//#region src/services/taskProgressService.ts
|
|
66
|
-
function formatDuration(durationMs) {
|
|
67
|
-
if (durationMs < 1e3) return `${durationMs}ms`;
|
|
68
|
-
return `${(durationMs / 1e3).toFixed(1)}s`;
|
|
69
|
-
}
|
|
70
|
-
var TaskProgressService = class {
|
|
71
|
-
#storage = new AsyncLocalStorage();
|
|
72
|
-
attach(sink, task) {
|
|
73
|
-
return this.#storage.run(sink, task);
|
|
74
|
-
}
|
|
75
|
-
log(message) {
|
|
76
|
-
this.#storage.getStore()?.log(message);
|
|
77
|
-
}
|
|
78
|
-
report(message) {
|
|
79
|
-
const sink = this.#storage.getStore();
|
|
80
|
-
(sink?.report || sink?.log)?.(message);
|
|
81
|
-
}
|
|
82
|
-
createStepProgress(classify = (message) => ({
|
|
83
|
-
id: message,
|
|
84
|
-
title: message
|
|
85
|
-
})) {
|
|
86
|
-
const progress = spinner();
|
|
87
|
-
let active;
|
|
88
|
-
const finishActive = (label) => {
|
|
89
|
-
if (!active) return;
|
|
90
|
-
const duration = formatDuration(Date.now() - active.startedAt);
|
|
91
|
-
progress.stop(`${label || active.completedTitle || `${active.title} completed`} (${duration})`);
|
|
92
|
-
active = void 0;
|
|
93
|
-
};
|
|
94
|
-
const start = (step) => {
|
|
95
|
-
if (active?.id === step.id) {
|
|
96
|
-
progress.message(step.detail ? `${step.title}: ${step.detail}` : step.title);
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
finishActive();
|
|
100
|
-
active = {
|
|
101
|
-
id: step.id,
|
|
102
|
-
title: step.title,
|
|
103
|
-
completedTitle: step.completedTitle,
|
|
104
|
-
startedAt: Date.now()
|
|
105
|
-
};
|
|
106
|
-
progress.start(step.detail ? `${step.title}: ${step.detail}` : step.title);
|
|
107
|
-
};
|
|
108
|
-
const execute = (task) => this.attach({
|
|
109
|
-
log: (message) => progress.message(active ? `${active.title}: ${message}` : message),
|
|
110
|
-
report: (message) => start(classify(message))
|
|
111
|
-
}, task);
|
|
112
|
-
return {
|
|
113
|
-
report(message) {
|
|
114
|
-
start(classify(message));
|
|
115
|
-
},
|
|
116
|
-
start,
|
|
117
|
-
execute,
|
|
118
|
-
run: async (step, task) => {
|
|
119
|
-
start(step);
|
|
120
|
-
await new Promise((resolve) => setImmediate(resolve));
|
|
121
|
-
const result = await execute(task);
|
|
122
|
-
finishActive();
|
|
123
|
-
return result;
|
|
124
|
-
},
|
|
125
|
-
finish(label) {
|
|
126
|
-
finishActive(label);
|
|
127
|
-
},
|
|
128
|
-
fail(label) {
|
|
129
|
-
if (!active) return;
|
|
130
|
-
const duration = formatDuration(Date.now() - active.startedAt);
|
|
131
|
-
progress.stop(`${label} (${duration})`);
|
|
132
|
-
active = void 0;
|
|
133
|
-
}
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
};
|
|
137
|
-
const taskProgressService = new TaskProgressService();
|
|
138
|
-
//#endregion
|
|
139
15
|
//#region src/utils/catalogueStats.ts
|
|
140
16
|
function calculateNeedsReviewPercentage(inputProperties, outputProperties, needsReview) {
|
|
141
17
|
const properties = inputProperties + outputProperties;
|
|
142
18
|
return properties ? Number((needsReview / properties * 100).toFixed(2)) : 0;
|
|
143
19
|
}
|
|
144
20
|
//#endregion
|
|
145
|
-
//#region src/services/analysisFileService.ts
|
|
146
|
-
function shouldKeepAnalysisFile(filePath) {
|
|
147
|
-
const normalizedProjectPath = filePath.replaceAll(path.sep, "/").replace(/^.*?(app\/)/, "app/");
|
|
148
|
-
return !getUserConfig().analysis.excluded.some((pattern) => minimatch(normalizedProjectPath, pattern, { dot: true }));
|
|
149
|
-
}
|
|
150
|
-
function filterAnalysisFiles(filePaths) {
|
|
151
|
-
return filePaths.filter(shouldKeepAnalysisFile);
|
|
152
|
-
}
|
|
153
|
-
//#endregion
|
|
154
21
|
//#region src/services/useCaseAnalysisService.ts
|
|
155
22
|
function normalizeCodeText(value) {
|
|
156
23
|
return value.replace(/\s+/g, " ").replace(/;$/, "").trim();
|
|
@@ -181,16 +48,16 @@ function getAggregationTarget(call) {
|
|
|
181
48
|
let current = call;
|
|
182
49
|
while (current) {
|
|
183
50
|
const parent = current.getParent();
|
|
184
|
-
if (!parent) return
|
|
51
|
+
if (!parent) return;
|
|
185
52
|
if (Node.isVariableDeclaration(parent)) return normalizeCodeText(parent.getName());
|
|
186
53
|
if (Node.isReturnStatement(parent)) return "return";
|
|
187
|
-
if (Node.isStatement(parent)) return
|
|
54
|
+
if (Node.isStatement(parent)) return;
|
|
188
55
|
current = parent;
|
|
189
56
|
}
|
|
190
57
|
}
|
|
191
58
|
function getAggregationCondition(call) {
|
|
192
59
|
const callback = call.getArguments().find((argument) => Node.isArrowFunction(argument) || Node.isFunctionExpression(argument));
|
|
193
|
-
if (!callback || !Node.isArrowFunction(callback) && !Node.isFunctionExpression(callback)) return
|
|
60
|
+
if (!callback || !Node.isArrowFunction(callback) && !Node.isFunctionExpression(callback)) return;
|
|
194
61
|
const body = callback.getBody();
|
|
195
62
|
if (!Node.isBlock(body)) return normalizeCodeText(body.getText());
|
|
196
63
|
const condition = body.getDescendantsOfKind(SyntaxKind.IfStatement)[0]?.getExpression();
|
|
@@ -268,29 +135,80 @@ var AnalysisCacheService = class {
|
|
|
268
135
|
};
|
|
269
136
|
const analysisCacheService = new AnalysisCacheService();
|
|
270
137
|
//#endregion
|
|
271
|
-
//#region src/services/
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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);
|
|
276
161
|
});
|
|
277
|
-
if (!response.ok) throw new Error(`Unable to fetch OpenAPI document from ${url}: ${response.status} ${response.statusText}`);
|
|
278
|
-
const reader = response.body?.getReader();
|
|
279
|
-
if (!reader) return response.json();
|
|
280
|
-
const decoder = new TextDecoder();
|
|
281
|
-
let content = "";
|
|
282
|
-
while (true) {
|
|
283
|
-
const { done, value } = await reader.read();
|
|
284
|
-
if (done) break;
|
|
285
|
-
content += decoder.decode(value, { stream: true });
|
|
286
|
-
taskProgressService.log(`Downloading (${(content.length / 1048576).toFixed(1)} MB)`);
|
|
287
|
-
}
|
|
288
|
-
taskProgressService.log("Parsing document");
|
|
289
|
-
return JSON.parse(content + decoder.decode());
|
|
290
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
|
|
291
201
|
async function loadOpenApiDocument(url, timeoutMs) {
|
|
292
202
|
try {
|
|
293
|
-
|
|
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);
|
|
294
212
|
} catch (error) {
|
|
295
213
|
if (error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError")) throw new Error(`OpenAPI download timed out after ${timeoutMs}ms: ${url}`);
|
|
296
214
|
throw new Error(`Unable to fetch OpenAPI document from ${url}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -391,22 +309,6 @@ function extractOpenApiOutputProperties(operation, swagger) {
|
|
|
391
309
|
return [...map.values()];
|
|
392
310
|
}
|
|
393
311
|
//#endregion
|
|
394
|
-
//#region src/utils/isKnowBackendType.ts
|
|
395
|
-
function isKnownBackendType(value) {
|
|
396
|
-
return getUserConfig().analysis.backends.includes(value);
|
|
397
|
-
}
|
|
398
|
-
//#endregion
|
|
399
|
-
//#region src/services/backendSourceService.ts
|
|
400
|
-
function inferBackendNameFromFile(sourceFile) {
|
|
401
|
-
const declaredType = sourceFile.getFullText().match(/BackendTypes\.([A-Z0-9_]+)/)?.[1];
|
|
402
|
-
if (declaredType) return isKnownBackendType(declaredType) ? declaredType : null;
|
|
403
|
-
const parts = sourceFile.getFilePath().split(path.sep);
|
|
404
|
-
const backIndex = parts.lastIndexOf("back");
|
|
405
|
-
const infrastructureIndex = parts.lastIndexOf("_infra");
|
|
406
|
-
const candidate = backIndex >= 0 ? parts[backIndex + 1] : infrastructureIndex >= 0 ? parts[infrastructureIndex + 2] : void 0;
|
|
407
|
-
return candidate && isKnownBackendType(candidate.toUpperCase()) ? candidate.toUpperCase() : null;
|
|
408
|
-
}
|
|
409
|
-
//#endregion
|
|
410
312
|
//#region src/services/backendRouteExtractionService.ts
|
|
411
313
|
function looksLikeBackendRoute(value) {
|
|
412
314
|
return value.startsWith("/") || /^https?:\/\//.test(value) || /^graphql$/i.test(value);
|
|
@@ -457,51 +359,6 @@ function normalizeDescription(description) {
|
|
|
457
359
|
return description?.trim() || "";
|
|
458
360
|
}
|
|
459
361
|
//#endregion
|
|
460
|
-
//#region src/utils/resolveAliasPath.ts
|
|
461
|
-
function resolveAliasPath(moduleSpecifier, aliases, rootPath) {
|
|
462
|
-
for (const [alias, target] of Object.entries(aliases)) {
|
|
463
|
-
if (moduleSpecifier !== alias && !moduleSpecifier.startsWith(`${alias}/`)) continue;
|
|
464
|
-
const modulePath = moduleSpecifier.slice(alias.length).replace(/^\//, "");
|
|
465
|
-
return path.join(rootPath, target, modulePath);
|
|
466
|
-
}
|
|
467
|
-
return null;
|
|
468
|
-
}
|
|
469
|
-
//#endregion
|
|
470
|
-
//#region src/utils/tryResolveWithExtensions.ts
|
|
471
|
-
function tryResolveWithExtensions(basePath) {
|
|
472
|
-
const ext = path.extname(basePath);
|
|
473
|
-
const withoutExt = ext ? basePath.slice(0, -ext.length) : basePath;
|
|
474
|
-
const candidates = [
|
|
475
|
-
basePath,
|
|
476
|
-
ext === ".js" ? `${withoutExt}.ts` : null,
|
|
477
|
-
ext === ".ts" ? `${withoutExt}.js` : null,
|
|
478
|
-
ext === ".mjs" ? `${withoutExt}.mts` : null,
|
|
479
|
-
ext === ".mts" ? `${withoutExt}.mjs` : null,
|
|
480
|
-
`${basePath}.ts`,
|
|
481
|
-
`${basePath}.js`,
|
|
482
|
-
`${basePath}.mts`,
|
|
483
|
-
`${basePath}.mjs`,
|
|
484
|
-
path.join(withoutExt, "index.ts"),
|
|
485
|
-
path.join(withoutExt, "index.js"),
|
|
486
|
-
path.join(basePath, "index.ts"),
|
|
487
|
-
path.join(basePath, "index.js")
|
|
488
|
-
].filter((candidate) => Boolean(candidate));
|
|
489
|
-
for (const candidate of candidates) try {
|
|
490
|
-
const normalized = path.normalize(candidate);
|
|
491
|
-
if (fs.existsSync(normalized)) return normalized;
|
|
492
|
-
} catch {
|
|
493
|
-
continue;
|
|
494
|
-
}
|
|
495
|
-
return null;
|
|
496
|
-
}
|
|
497
|
-
//#endregion
|
|
498
|
-
//#region src/utils/resolveModulePath.ts
|
|
499
|
-
function resolveModulePath(sourceFilePath, moduleSpecifier, aliases, rootPath) {
|
|
500
|
-
if (moduleSpecifier.startsWith(".")) return tryResolveWithExtensions(path.resolve(path.dirname(sourceFilePath), moduleSpecifier));
|
|
501
|
-
const aliased = resolveAliasPath(moduleSpecifier, aliases, rootPath);
|
|
502
|
-
return aliased ? tryResolveWithExtensions(aliased) : null;
|
|
503
|
-
}
|
|
504
|
-
//#endregion
|
|
505
362
|
//#region src/utils/dedupeByKey.ts
|
|
506
363
|
function dedupeByKey(items, keyFn) {
|
|
507
364
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -1382,6 +1239,7 @@ async function collectDependencyFiles(project, entryFilePath, routeLabel, resolv
|
|
|
1382
1239
|
}
|
|
1383
1240
|
async function analyzeCodebaseRouteContracts(params) {
|
|
1384
1241
|
const { cwd, openApiDocument: swagger, selectedRouteKeys, routeContracts, onDocument, routeAnalysisScope } = params;
|
|
1242
|
+
const backendProperties = await resolveBackendProperties(userConfig.analysis.backends);
|
|
1385
1243
|
taskProgressService.log("Discovering route files");
|
|
1386
1244
|
const routeFiles = await globby(["app/_api/**/routes.@(js|ts)", "app/legacy/**/routes.@(js|ts)"], {
|
|
1387
1245
|
cwd,
|
|
@@ -1443,11 +1301,12 @@ async function analyzeCodebaseRouteContracts(params) {
|
|
|
1443
1301
|
}) : void 0;
|
|
1444
1302
|
if (routeAnalysisScope && !topologyScope) continue;
|
|
1445
1303
|
taskProgressService.log(topologyScope ? `${routeLabel} ... Loading dependencies from backend graph` : `${routeLabel} ... Collecting dependencies`);
|
|
1446
|
-
const
|
|
1304
|
+
const dependencyFiles = topologyScope ? topologyScope.analysisFiles : handler.handlerFile ? dependencyFilesByHandler.get(handler.handlerFile) || (() => {
|
|
1447
1305
|
const files = collectDependencyFiles(project, handler.handlerFile, routeLabel, resolveCachedModulePath);
|
|
1448
1306
|
dependencyFilesByHandler.set(handler.handlerFile, files);
|
|
1449
1307
|
return files;
|
|
1450
|
-
})() : [routeDeclaration.file]
|
|
1308
|
+
})() : [routeDeclaration.file];
|
|
1309
|
+
const analysisFiles = filterAnalysisFiles(dedupeByKey(await dependencyFiles, (value) => value));
|
|
1451
1310
|
taskProgressService.log(`${routeLabel} ... Extracting mappings from ${analysisFiles.length} relevant files`);
|
|
1452
1311
|
await yieldToEventLoop();
|
|
1453
1312
|
const backendSourceFiles = analysisFiles.filter((file) => file.includes(`${path.sep}app${path.sep}_infra${path.sep}back${path.sep}`)).map((file) => project.getSourceFile(file) || project.addSourceFileAtPath(file)).filter((sourceFile) => {
|
|
@@ -1470,7 +1329,15 @@ async function analyzeCodebaseRouteContracts(params) {
|
|
|
1470
1329
|
}), (item) => `${item.backend}:${item.method}:${item.route}:${item.sourceFile}`);
|
|
1471
1330
|
const backendGraphKey = `${backendSourceFiles.map((file) => file.getFilePath()).sort().join("|")}::${[...reachableBackendFunctionNames].sort().join("|")}`;
|
|
1472
1331
|
const backendFieldCandidates = backendFieldsByGraph.get(backendGraphKey) || (() => {
|
|
1473
|
-
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
|
+
});
|
|
1474
1341
|
backendFieldsByGraph.set(backendGraphKey, candidates);
|
|
1475
1342
|
return candidates;
|
|
1476
1343
|
})();
|
|
@@ -1619,6 +1486,31 @@ function resolveBackendRouteCandidate(document, match) {
|
|
|
1619
1486
|
const sameBackendCandidates = document.backendRouteCandidates.filter((candidate) => candidate.backend === match.backend);
|
|
1620
1487
|
return sameBackendCandidates.find((candidate) => candidate.sourceFile === match.sourceFile) || sameBackendCandidates[0];
|
|
1621
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
|
+
}
|
|
1622
1514
|
async function buildCatalogue(documents) {
|
|
1623
1515
|
const routes = documents.map((document) => ({
|
|
1624
1516
|
key: document.key,
|
|
@@ -1660,19 +1552,7 @@ async function buildCatalogue(documents) {
|
|
|
1660
1552
|
const backendNames = dedupeByKey(matches.map((candidate) => candidate.backend), (value) => value);
|
|
1661
1553
|
const isTransverseWithoutMapping = isTransversalInput(property) && !matches.length;
|
|
1662
1554
|
const evidenceStatus = isTransverseWithoutMapping ? "confirmed" : evidenceStatusFromCandidates(matches);
|
|
1663
|
-
const backendMappings = dedupeByKey(matches.map((match) => {
|
|
1664
|
-
const backendRouteCandidate = resolveBackendRouteCandidate(document, match);
|
|
1665
|
-
return {
|
|
1666
|
-
backend: match.backend,
|
|
1667
|
-
method: backendRouteCandidate?.method || null,
|
|
1668
|
-
route: backendRouteCandidate?.route || "unknown",
|
|
1669
|
-
field: match.backendField,
|
|
1670
|
-
source_file: backendFieldSource(match),
|
|
1671
|
-
confidence: match.confidence,
|
|
1672
|
-
mapper_type: match.mapperType,
|
|
1673
|
-
reason: match.reviewReason
|
|
1674
|
-
};
|
|
1675
|
-
}), (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}`);
|
|
1676
1556
|
routeInputProperties.push({
|
|
1677
1557
|
key: apiPropertyKey,
|
|
1678
1558
|
route_key: document.key,
|
|
@@ -1703,12 +1583,14 @@ async function buildCatalogue(documents) {
|
|
|
1703
1583
|
route: "unknown",
|
|
1704
1584
|
sourceFile: match.sourceFile
|
|
1705
1585
|
}).key;
|
|
1706
|
-
const backendPropertyKey = stableKey$2(backendRouteKey, "input", match.backendField);
|
|
1586
|
+
const backendPropertyKey = stableKey$2(backendRouteKey, "input", match.resolvedProperty?.field || match.backendField);
|
|
1707
1587
|
backendPropertiesByKey.set(backendPropertyKey, {
|
|
1708
1588
|
key: backendPropertyKey,
|
|
1709
1589
|
backend_route_key: backendRouteKey,
|
|
1710
1590
|
backend: match.backend,
|
|
1711
|
-
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,
|
|
1712
1594
|
direction: "input",
|
|
1713
1595
|
source_file: backendFieldSource(match),
|
|
1714
1596
|
provenance: "code_analysis"
|
|
@@ -1732,19 +1614,7 @@ async function buildCatalogue(documents) {
|
|
|
1732
1614
|
const apiPropertyKey = stableKey$2(document.key, "output", property.path);
|
|
1733
1615
|
const backendNames = dedupeByKey(matches.map((candidate) => candidate.backend), (value) => value);
|
|
1734
1616
|
const evidenceStatus = evidenceStatusFromCandidates(matches);
|
|
1735
|
-
const backendMappings = dedupeByKey(matches.map((match) => {
|
|
1736
|
-
const backendRouteCandidate = resolveBackendRouteCandidate(document, match);
|
|
1737
|
-
return {
|
|
1738
|
-
backend: match.backend,
|
|
1739
|
-
method: backendRouteCandidate?.method || null,
|
|
1740
|
-
route: backendRouteCandidate?.route || "unknown",
|
|
1741
|
-
field: match.backendField,
|
|
1742
|
-
source_file: backendFieldSource(match),
|
|
1743
|
-
confidence: match.confidence,
|
|
1744
|
-
mapper_type: match.mapperType,
|
|
1745
|
-
reason: match.reviewReason
|
|
1746
|
-
};
|
|
1747
|
-
}), (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}`);
|
|
1748
1618
|
routeOutputProperties.push({
|
|
1749
1619
|
key: apiPropertyKey,
|
|
1750
1620
|
route_key: document.key,
|
|
@@ -1775,12 +1645,14 @@ async function buildCatalogue(documents) {
|
|
|
1775
1645
|
route: "unknown",
|
|
1776
1646
|
sourceFile: match.sourceFile
|
|
1777
1647
|
}).key;
|
|
1778
|
-
const backendPropertyKey = stableKey$2(backendRouteKey, "output", match.backendField);
|
|
1648
|
+
const backendPropertyKey = stableKey$2(backendRouteKey, "output", match.resolvedProperty?.field || match.backendField);
|
|
1779
1649
|
backendPropertiesByKey.set(backendPropertyKey, {
|
|
1780
1650
|
key: backendPropertyKey,
|
|
1781
1651
|
backend_route_key: backendRouteKey,
|
|
1782
1652
|
backend: match.backend,
|
|
1783
|
-
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,
|
|
1784
1656
|
direction: "output",
|
|
1785
1657
|
source_file: backendFieldSource(match),
|
|
1786
1658
|
provenance: "code_analysis"
|
|
@@ -1824,7 +1696,7 @@ async function buildCatalogue(documents) {
|
|
|
1824
1696
|
};
|
|
1825
1697
|
}
|
|
1826
1698
|
//#endregion
|
|
1827
|
-
//#region src/services/directusSyncService.ts
|
|
1699
|
+
//#region src/services/directus/directusSyncService.ts
|
|
1828
1700
|
const ROUTE_STATUSES = { published: "published" };
|
|
1829
1701
|
const LOCAL_SOURCE_FILE = "digital-api:tools/datasource-catalogue";
|
|
1830
1702
|
const DIRECTUS_RETRY_MAX_ATTEMPTS = 6;
|
|
@@ -2356,14 +2228,14 @@ async function pushCatalogueToDirectus(catalogue, options) {
|
|
|
2356
2228
|
}
|
|
2357
2229
|
}
|
|
2358
2230
|
}
|
|
2359
|
-
if (options.outputPath) await fs
|
|
2231
|
+
if (options.outputPath) await fs.appendFile(options.outputPath, "", "utf8");
|
|
2360
2232
|
const removedOrphanedLinks = await cleanupOrphanedPropertyLinks(client);
|
|
2361
2233
|
taskProgressService.report("Finalizing Directus synchronization");
|
|
2362
2234
|
if (warnings.size) {
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
for (const warning of warnings)
|
|
2366
|
-
|
|
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("========================================");
|
|
2367
2239
|
}
|
|
2368
2240
|
return {
|
|
2369
2241
|
pushedCollections,
|
|
@@ -2403,9 +2275,11 @@ function buildReviewProperty(property) {
|
|
|
2403
2275
|
const backends = property.backend_mappings.map((mapping) => ({
|
|
2404
2276
|
type: mapping.backend,
|
|
2405
2277
|
field: mapping.field,
|
|
2278
|
+
document: mapping.document,
|
|
2279
|
+
description: mapping.description,
|
|
2406
2280
|
source: mapping.source_file,
|
|
2407
|
-
...mapping.route !== "unknown" || mapping.method ? { operation: {
|
|
2408
|
-
method: mapping.method,
|
|
2281
|
+
...mapping.route && (mapping.route !== "unknown" || mapping.method) ? { operation: {
|
|
2282
|
+
method: mapping.method || null,
|
|
2409
2283
|
route: mapping.route
|
|
2410
2284
|
} } : {},
|
|
2411
2285
|
confidence: mapping.confidence,
|
|
@@ -2552,12 +2426,12 @@ async function writeRouteReviewDocument(catalogue, outputDir, repoRoot, routeKey
|
|
|
2552
2426
|
if (!route) throw new Error(`Unable to write review document for unknown route key ${routeKey}`);
|
|
2553
2427
|
const document = relativizeRouteReviewDocument(buildRouteReviewDocument(catalogue, route.key), repoRoot);
|
|
2554
2428
|
const filePath = path.join(outputDir, buildRouteReviewRelativePath(route.method, route.path));
|
|
2555
|
-
await fs
|
|
2556
|
-
await fs
|
|
2429
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
2430
|
+
await fs.writeFile(filePath, `${stringify(document)}\n`, "utf8");
|
|
2557
2431
|
return filePath;
|
|
2558
2432
|
}
|
|
2559
2433
|
async function writeRouteReviewDocuments(catalogue, outputDir, repoRoot) {
|
|
2560
|
-
await fs
|
|
2434
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
2561
2435
|
for (const [index, route] of catalogue.routes.entries()) {
|
|
2562
2436
|
taskProgressService.log(`[${index + 1}/${catalogue.routes.length}] - ${route.method} ${route.path} ...`);
|
|
2563
2437
|
await writeRouteReviewDocument(catalogue, outputDir, repoRoot, route.key);
|
|
@@ -2565,8 +2439,8 @@ async function writeRouteReviewDocuments(catalogue, outputDir, repoRoot) {
|
|
|
2565
2439
|
return outputDir;
|
|
2566
2440
|
}
|
|
2567
2441
|
async function prepareCatalogueOutputDirectory(outputDir) {
|
|
2568
|
-
await fs
|
|
2569
|
-
await Promise.all(["catalogue.yaml", "catalogue.yml"].map((fileName) => fs
|
|
2442
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
2443
|
+
await Promise.all(["catalogue.yaml", "catalogue.yml"].map((fileName) => fs.rm(path.join(outputDir, fileName), { force: true })));
|
|
2570
2444
|
}
|
|
2571
2445
|
async function writeCatalogueArtifacts(catalogue, outputDir, repoRoot = process.cwd()) {
|
|
2572
2446
|
await prepareCatalogueOutputDirectory(outputDir);
|
|
@@ -2577,16 +2451,17 @@ async function writeCatalogueArtifacts(catalogue, outputDir, repoRoot = process.
|
|
|
2577
2451
|
};
|
|
2578
2452
|
}
|
|
2579
2453
|
function addBackendMapping(routeKey, direction, apiPropertyKey, mapping, backendRoutesByKey, backendPropertiesByKey, mappingEvidence) {
|
|
2580
|
-
const
|
|
2581
|
-
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) || {
|
|
2582
2457
|
key: backendRouteKey,
|
|
2583
2458
|
backend: mapping.backend,
|
|
2584
2459
|
route_key: routeKey,
|
|
2585
|
-
route: `${mapping.method || "CALL"} ${
|
|
2460
|
+
route: `${mapping.method || "CALL"} ${backendRoute}`,
|
|
2586
2461
|
source_file: mapping.source_file,
|
|
2587
2462
|
provenance: "code_analysis"
|
|
2588
2463
|
};
|
|
2589
|
-
backendRoutesByKey.set(backendRouteKey,
|
|
2464
|
+
backendRoutesByKey.set(backendRouteKey, backendRouteRecord);
|
|
2590
2465
|
const backendPropertyKey = stableKey$1(backendRouteKey, direction, mapping.field);
|
|
2591
2466
|
backendPropertiesByKey.set(backendPropertyKey, {
|
|
2592
2467
|
key: backendPropertyKey,
|
|
@@ -2612,8 +2487,10 @@ function toCatalogueBackendMapping(mapping) {
|
|
|
2612
2487
|
return {
|
|
2613
2488
|
backend: mapping.type,
|
|
2614
2489
|
method: mapping.operation?.method || null,
|
|
2615
|
-
route: mapping.operation
|
|
2490
|
+
...mapping.operation?.route ? { route: mapping.operation.route } : {},
|
|
2616
2491
|
field: mapping.field,
|
|
2492
|
+
document: mapping.document,
|
|
2493
|
+
description: mapping.description,
|
|
2617
2494
|
source_file: mapping.source,
|
|
2618
2495
|
confidence: mapping.confidence,
|
|
2619
2496
|
mapper_type: mapping.mapper_type,
|
|
@@ -2744,7 +2621,7 @@ async function readCatalogueFromDirectory(inputDir) {
|
|
|
2744
2621
|
if (!files.length) throw new Error(`No route YAML documents found in ${inputDir}`);
|
|
2745
2622
|
return {
|
|
2746
2623
|
catalogue: mergeRouteReviewDocuments(await Promise.all(files.sort().map(async (file) => {
|
|
2747
|
-
const parsed = parse(await fs
|
|
2624
|
+
const parsed = parse(await fs.readFile(file, "utf8"));
|
|
2748
2625
|
if (parsed?.schema_version !== 1 || !parsed?.route?.method || !parsed?.route?.path) throw new Error(`Invalid route YAML document: ${file}`);
|
|
2749
2626
|
return parsed;
|
|
2750
2627
|
}))),
|
|
@@ -2752,7 +2629,7 @@ async function readCatalogueFromDirectory(inputDir) {
|
|
|
2752
2629
|
};
|
|
2753
2630
|
}
|
|
2754
2631
|
//#endregion
|
|
2755
|
-
//#region src/services/directusPushService.ts
|
|
2632
|
+
//#region src/services/directus/directusPushService.ts
|
|
2756
2633
|
async function pushCatalogueDirectoryToDirectus(params) {
|
|
2757
2634
|
const inputPath = path.resolve(params.cwd, params.catalogueDirectory);
|
|
2758
2635
|
taskProgressService.report("Loading and reconstructing route YAML documents");
|
|
@@ -2921,6 +2798,42 @@ var cleanOrphans_default = (program) => void program.command("clean-orphans").op
|
|
|
2921
2798
|
}
|
|
2922
2799
|
});
|
|
2923
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
|
|
2924
2837
|
//#region src/services/analysisProfileService.ts
|
|
2925
2838
|
function createAnalysisProfile(enabled) {
|
|
2926
2839
|
const durations = /* @__PURE__ */ new Map();
|
|
@@ -2938,8 +2851,8 @@ function buildBackendTopologyRelativePath(method, routePath) {
|
|
|
2938
2851
|
}
|
|
2939
2852
|
async function writeBackendTopologyArtifact(artifact, outputDir) {
|
|
2940
2853
|
const filePath = path.join(outputDir, buildBackendTopologyRelativePath(artifact.route.method, artifact.route.path));
|
|
2941
|
-
await fs
|
|
2942
|
-
await fs
|
|
2854
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
2855
|
+
await fs.writeFile(filePath, `${stringify(artifact, { aliasDuplicateObjects: false })}\n`, "utf8");
|
|
2943
2856
|
return filePath;
|
|
2944
2857
|
}
|
|
2945
2858
|
function isRecord(value) {
|
|
@@ -2958,7 +2871,7 @@ async function loadBackendTopologyAnalysisScope(outputDir, cwd, method, routePat
|
|
|
2958
2871
|
const filePath = path.join(outputDir, buildBackendTopologyRelativePath(method, routePath));
|
|
2959
2872
|
let content;
|
|
2960
2873
|
try {
|
|
2961
|
-
content = await fs
|
|
2874
|
+
content = await fs.readFile(filePath, "utf8");
|
|
2962
2875
|
} catch (error) {
|
|
2963
2876
|
if (isRecord(error) && error.code === "ENOENT") throw new Error(`Backend graph artifact not found for ${method} ${routePath}: ${filePath}. Run generate:graph first.`);
|
|
2964
2877
|
throw error;
|
|
@@ -2979,7 +2892,7 @@ async function loadBackendTopologyAnalysisScope(outputDir, cwd, method, routePat
|
|
|
2979
2892
|
const missingFiles = [];
|
|
2980
2893
|
await Promise.all(analysisFiles.map(async (analysisFile) => {
|
|
2981
2894
|
try {
|
|
2982
|
-
await fs
|
|
2895
|
+
await fs.access(analysisFile);
|
|
2983
2896
|
} catch {
|
|
2984
2897
|
missingFiles.push(path.relative(cwd, analysisFile));
|
|
2985
2898
|
}
|
|
@@ -3079,7 +2992,7 @@ async function generateHandler(cwd, routeArgument, options) {
|
|
|
3079
2992
|
process.exit(1);
|
|
3080
2993
|
}
|
|
3081
2994
|
}
|
|
3082
|
-
var generate_default = (program) => void program.command("generate:
|
|
2995
|
+
var generate_default = (program) => void program.command("generate:catalog").argument("[route]", "Route selector in the format \"GET /v1/products\"").option("-o, --output <directory>", "Directory where catalogue artifacts are generated", ".tmp/datasource-catalogue").option("--openapi-url <url>", "OpenAPI document URL").option("--profile", "Display elapsed time by generation stage").description("Analyze the codebase and generate one YAML review document per route").action((routeArgument, options) => {
|
|
3083
2996
|
const config = getUserConfig();
|
|
3084
2997
|
return generateHandler(config.cwd, routeArgument, {
|
|
3085
2998
|
...options,
|
|
@@ -3087,104 +3000,6 @@ var generate_default = (program) => void program.command("generate:catalogue").a
|
|
|
3087
3000
|
});
|
|
3088
3001
|
});
|
|
3089
3002
|
//#endregion
|
|
3090
|
-
//#region src/services/routeBackendTopologyService.ts
|
|
3091
|
-
function stringProperty(object, name) {
|
|
3092
|
-
const property = object.getProperty(name);
|
|
3093
|
-
if (!property || !Node.isPropertyAssignment(property)) return void 0;
|
|
3094
|
-
const initializer = property.getInitializer();
|
|
3095
|
-
return initializer && (Node.isStringLiteral(initializer) || Node.isNoSubstitutionTemplateLiteral(initializer)) ? initializer.getLiteralValue() : void 0;
|
|
3096
|
-
}
|
|
3097
|
-
function handlerProperty(object) {
|
|
3098
|
-
const property = object.getProperty("handler");
|
|
3099
|
-
if (!property || !Node.isPropertyAssignment(property)) return void 0;
|
|
3100
|
-
const initializer = property.getInitializer();
|
|
3101
|
-
return initializer && (Node.isIdentifier(initializer) || Node.isPropertyAccessExpression(initializer)) ? initializer.getText() : void 0;
|
|
3102
|
-
}
|
|
3103
|
-
function extractRouteDeclarations(sourceFile) {
|
|
3104
|
-
const initializer = sourceFile.getVariableDeclaration("routes")?.getInitializer();
|
|
3105
|
-
let expression = initializer;
|
|
3106
|
-
if (initializer && (Node.isAsExpression(initializer) || Node.isSatisfiesExpression(initializer))) expression = initializer.getExpression();
|
|
3107
|
-
if (!expression || !Node.isArrayLiteralExpression(expression)) return [];
|
|
3108
|
-
return expression.getElements().flatMap((element) => {
|
|
3109
|
-
if (!Node.isObjectLiteralExpression(element)) return [];
|
|
3110
|
-
const method = stringProperty(element, "method");
|
|
3111
|
-
const routePath = stringProperty(element, "path");
|
|
3112
|
-
const handlerRef = handlerProperty(element);
|
|
3113
|
-
return method && routePath && handlerRef ? [{
|
|
3114
|
-
method: method.toUpperCase(),
|
|
3115
|
-
path: routePath,
|
|
3116
|
-
sourceFile,
|
|
3117
|
-
handlerRef
|
|
3118
|
-
}] : [];
|
|
3119
|
-
});
|
|
3120
|
-
}
|
|
3121
|
-
async function discoverBackendTopologyRouteSelectors(cwd) {
|
|
3122
|
-
const project = new Project({
|
|
3123
|
-
skipAddingFilesFromTsConfig: true,
|
|
3124
|
-
compilerOptions: {
|
|
3125
|
-
allowJs: true,
|
|
3126
|
-
checkJs: false
|
|
3127
|
-
}
|
|
3128
|
-
});
|
|
3129
|
-
return (await globby(["app/_api/**/routes.@(js|ts)", "app/legacy/**/routes.@(js|ts)"], {
|
|
3130
|
-
cwd,
|
|
3131
|
-
absolute: true
|
|
3132
|
-
})).flatMap((routeFile) => extractRouteDeclarations(project.addSourceFileAtPath(routeFile))).map((route) => ({
|
|
3133
|
-
method: route.method,
|
|
3134
|
-
path: route.path
|
|
3135
|
-
}));
|
|
3136
|
-
}
|
|
3137
|
-
async function generateBackendTopologyArtifactsInWorkers(params) {
|
|
3138
|
-
const selectors = params.routeSelector ? [params.routeSelector] : await discoverBackendTopologyRouteSelectors(params.cwd);
|
|
3139
|
-
params.onRoutesDiscovered?.(selectors.length);
|
|
3140
|
-
const workerCount = Math.min(params.workers || 2, selectors.length);
|
|
3141
|
-
const chunks = Array.from({ length: workerCount }, () => []);
|
|
3142
|
-
selectors.forEach((selector, index) => chunks[index % workerCount].push(selector));
|
|
3143
|
-
const artifacts = [];
|
|
3144
|
-
let completed = 0;
|
|
3145
|
-
let writeQueue = Promise.resolve();
|
|
3146
|
-
const workers = [];
|
|
3147
|
-
try {
|
|
3148
|
-
await Promise.all(chunks.map((routeSelectors) => new Promise((resolve, reject) => {
|
|
3149
|
-
const worker = new Worker(new URL("./routeBackendTopologyWorker.ts", import.meta.url), {
|
|
3150
|
-
workerData: {
|
|
3151
|
-
cwd: params.cwd,
|
|
3152
|
-
routeSelectors
|
|
3153
|
-
},
|
|
3154
|
-
execArgv: process.execArgv
|
|
3155
|
-
});
|
|
3156
|
-
workers.push(worker);
|
|
3157
|
-
worker.on("message", (message) => {
|
|
3158
|
-
if (message.type === "error") {
|
|
3159
|
-
reject(new Error(message.message));
|
|
3160
|
-
return;
|
|
3161
|
-
}
|
|
3162
|
-
writeQueue = writeQueue.then(async () => {
|
|
3163
|
-
if (message.type === "artifact") {
|
|
3164
|
-
await params.onArtifact(message.artifact);
|
|
3165
|
-
artifacts.push(message.artifact);
|
|
3166
|
-
return;
|
|
3167
|
-
}
|
|
3168
|
-
completed += 1;
|
|
3169
|
-
params.onRouteProgress?.({
|
|
3170
|
-
current: completed,
|
|
3171
|
-
total: selectors.length,
|
|
3172
|
-
route: message.route,
|
|
3173
|
-
stage: "completed"
|
|
3174
|
-
});
|
|
3175
|
-
}).catch(reject);
|
|
3176
|
-
});
|
|
3177
|
-
worker.once("error", reject);
|
|
3178
|
-
worker.once("exit", (code) => code === 0 ? resolve() : reject(/* @__PURE__ */ new Error(`Topology worker exited with code ${code}`)));
|
|
3179
|
-
})));
|
|
3180
|
-
await writeQueue;
|
|
3181
|
-
return artifacts;
|
|
3182
|
-
} catch (error) {
|
|
3183
|
-
await Promise.all(workers.map((worker) => worker.terminate().catch(() => void 0)));
|
|
3184
|
-
throw error;
|
|
3185
|
-
}
|
|
3186
|
-
}
|
|
3187
|
-
//#endregion
|
|
3188
3003
|
//#region src/commands/generateGraph.ts
|
|
3189
3004
|
function parseWorkerCount$1(value) {
|
|
3190
3005
|
const workers = Number(value);
|
|
@@ -3333,7 +3148,7 @@ var generateTest_default = (program) => void program.command("generate:test").de
|
|
|
3333
3148
|
}
|
|
3334
3149
|
});
|
|
3335
3150
|
//#endregion
|
|
3336
|
-
//#region src/services/aiSdkClient.ts
|
|
3151
|
+
//#region src/services/ai/aiSdkClient.ts
|
|
3337
3152
|
var AISdkClient = class {
|
|
3338
3153
|
#config;
|
|
3339
3154
|
constructor(config) {
|
|
@@ -3364,7 +3179,7 @@ var AISdkClient = class {
|
|
|
3364
3179
|
}
|
|
3365
3180
|
};
|
|
3366
3181
|
//#endregion
|
|
3367
|
-
//#region src/services/inferenceService.ts
|
|
3182
|
+
//#region src/services/ai/inferenceService.ts
|
|
3368
3183
|
const MAPPING_OUTPUT_SCHEMA = z.object({ mappings: z.array(z.object({
|
|
3369
3184
|
property_index: z.number().int(),
|
|
3370
3185
|
candidate_index: z.number().int(),
|
|
@@ -3439,7 +3254,7 @@ async function readSourceExcerpts(document) {
|
|
|
3439
3254
|
const excerpts = [];
|
|
3440
3255
|
let totalCharacters = 0;
|
|
3441
3256
|
for (const location of uniqueLocations) try {
|
|
3442
|
-
const lines = (await fs
|
|
3257
|
+
const lines = (await fs.readFile(location.sourceFile, "utf8")).split("\n");
|
|
3443
3258
|
const startLine = Math.max(1, location.line - EXCERPT_RADIUS);
|
|
3444
3259
|
const endLine = Math.min(lines.length, location.line + EXCERPT_RADIUS);
|
|
3445
3260
|
const code = lines.slice(startLine - 1, endLine).map((line, index) => `${String(startLine + index).padStart(5)} | ${line}`).join("\n");
|
|
@@ -4205,18 +4020,18 @@ var init_default = (program, datasourceCommand) => void program.command("init").
|
|
|
4205
4020
|
const configDirectory = path.dirname(configPath);
|
|
4206
4021
|
intro("Initialize datasource configuration");
|
|
4207
4022
|
try {
|
|
4208
|
-
await fs
|
|
4023
|
+
await fs.mkdir(configDirectory, { recursive: true });
|
|
4209
4024
|
if (!options.force) {
|
|
4210
4025
|
let exists = false;
|
|
4211
4026
|
try {
|
|
4212
|
-
await fs
|
|
4027
|
+
await fs.access(configPath);
|
|
4213
4028
|
exists = true;
|
|
4214
4029
|
} catch (error) {
|
|
4215
4030
|
if (error.code !== "ENOENT") throw error;
|
|
4216
4031
|
}
|
|
4217
4032
|
if (exists) throw new Error(`${configPath} already exists. Use --force to overwrite it.`);
|
|
4218
4033
|
}
|
|
4219
|
-
await fs
|
|
4034
|
+
await fs.writeFile(configPath, configTemplate, "utf8");
|
|
4220
4035
|
outro(`Created ${configPath}`);
|
|
4221
4036
|
} catch (error) {
|
|
4222
4037
|
cancel(error instanceof Error ? error.message : String(error));
|
|
@@ -4349,7 +4164,7 @@ function collectImpactedRoutes(params) {
|
|
|
4349
4164
|
return params.graphs.filter((graph) => graph.analysisFiles.some((analysisFile) => changedFiles.has(normalizeFilePath(analysisFile)))).sort((left, right) => `${left.method} ${left.path}`.localeCompare(`${right.method} ${right.path}`));
|
|
4350
4165
|
}
|
|
4351
4166
|
async function readChangedFiles(filePath) {
|
|
4352
|
-
return (await fs
|
|
4167
|
+
return (await fs.readFile(filePath, "utf8")).split(/\r?\n/).map((line) => normalizeFilePath(line.trim())).filter(Boolean);
|
|
4353
4168
|
}
|
|
4354
4169
|
async function readRouteGraphs(outputDirectory) {
|
|
4355
4170
|
const graphFiles = await globby("**/*.graph.yaml", {
|
|
@@ -4357,7 +4172,7 @@ async function readRouteGraphs(outputDirectory) {
|
|
|
4357
4172
|
absolute: true
|
|
4358
4173
|
});
|
|
4359
4174
|
const graphs = await Promise.all(graphFiles.map(async (graphFile) => {
|
|
4360
|
-
const document = parse(await fs
|
|
4175
|
+
const document = parse(await fs.readFile(graphFile, "utf8"));
|
|
4361
4176
|
if (!isGraphDocument(document)) throw new Error(`Invalid graph document: ${graphFile}`);
|
|
4362
4177
|
return {
|
|
4363
4178
|
method: document.route.method,
|
|
@@ -4475,8 +4290,8 @@ async function reportChangedHandler(cwd, options) {
|
|
|
4475
4290
|
routes: coverages
|
|
4476
4291
|
});
|
|
4477
4292
|
const reportPath = path.resolve(cwd, options.report);
|
|
4478
|
-
await fs
|
|
4479
|
-
await fs
|
|
4293
|
+
await fs.mkdir(path.dirname(reportPath), { recursive: true });
|
|
4294
|
+
await fs.writeFile(reportPath, `${report}\n`, "utf8");
|
|
4480
4295
|
note(report, "Datasource mapping report");
|
|
4481
4296
|
}
|
|
4482
4297
|
var reportChanged_default = (program) => void program.command("report:changed").requiredOption("--changed-files <file>", "Newline-delimited list of changed repository files").option("-o, --output <directory>", "Directory containing generated graph artifacts", ".tmp/datasource-catalogue").option("--report <file>", "Markdown report output path", ".tmp/datasource-catalogue/changed-routes-report.md").description("Report deterministic mapping coverage for routes impacted by changed files").action(async (options) => {
|
|
@@ -4496,9 +4311,11 @@ const program = new Command();
|
|
|
4496
4311
|
program.version("3.4.0-alpha.1");
|
|
4497
4312
|
program.name("atlas").description("Manage the API-to-backend mapping catalogue").option("--project-root <path>", "Root directory of the API project to analyze").option("-c, --config <path>", "Path to atlas.config.ts", path.resolve(process.cwd(), "atlas.config.ts")).hook("preAction", async (_thisCommand, actionCommand) => {
|
|
4498
4313
|
if (actionCommand.name() === "init") return;
|
|
4499
|
-
|
|
4314
|
+
const config = await loadAtlasConfig(program.opts().config, program.opts().projectRoot);
|
|
4315
|
+
setUserConfig(config);
|
|
4500
4316
|
});
|
|
4501
4317
|
init_default(program, program);
|
|
4318
|
+
backendSources_default(program);
|
|
4502
4319
|
generate_default(program);
|
|
4503
4320
|
generateGraph_default(program);
|
|
4504
4321
|
generateTest_default(program);
|