@untangle-code/file-graph 0.0.0-stage → 0.1.0
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/LICENSE +661 -0
- package/NOTICE +90 -0
- package/README.md +284 -2
- package/THIRD_PARTY_LICENSES.md +29 -0
- package/dist/assignment.d.ts +15 -0
- package/dist/assignment.d.ts.map +1 -0
- package/dist/assignment.js +75 -0
- package/dist/assignment.js.map +1 -0
- package/dist/boundaries/endpoints.d.ts +11 -0
- package/dist/boundaries/endpoints.d.ts.map +1 -0
- package/dist/boundaries/endpoints.js +195 -0
- package/dist/boundaries/endpoints.js.map +1 -0
- package/dist/boundaries/index.d.ts +9 -0
- package/dist/boundaries/index.d.ts.map +1 -0
- package/dist/boundaries/index.js +93 -0
- package/dist/boundaries/index.js.map +1 -0
- package/dist/boundaries/requests.d.ts +12 -0
- package/dist/boundaries/requests.d.ts.map +1 -0
- package/dist/boundaries/requests.js +311 -0
- package/dist/boundaries/requests.js.map +1 -0
- package/dist/edges.d.ts +49 -0
- package/dist/edges.d.ts.map +1 -0
- package/dist/edges.js +223 -0
- package/dist/edges.js.map +1 -0
- package/dist/file-index.d.ts +37 -0
- package/dist/file-index.d.ts.map +1 -0
- package/dist/file-index.js +103 -0
- package/dist/file-index.js.map +1 -0
- package/dist/graph.d.ts +160 -0
- package/dist/graph.d.ts.map +1 -0
- package/dist/graph.js +448 -0
- package/dist/graph.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +48 -0
- package/dist/index.js.map +1 -0
- package/dist/metrics.d.ts +67 -0
- package/dist/metrics.d.ts.map +1 -0
- package/dist/metrics.js +272 -0
- package/dist/metrics.js.map +1 -0
- package/dist/package-catalog.d.ts +43 -0
- package/dist/package-catalog.d.ts.map +1 -0
- package/dist/package-catalog.js +136 -0
- package/dist/package-catalog.js.map +1 -0
- package/dist/paths.d.ts +56 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +129 -0
- package/dist/paths.js.map +1 -0
- package/dist/persistence.d.ts +83 -0
- package/dist/persistence.d.ts.map +1 -0
- package/dist/persistence.js +538 -0
- package/dist/persistence.js.map +1 -0
- package/dist/resolve/candidates.d.ts +52 -0
- package/dist/resolve/candidates.d.ts.map +1 -0
- package/dist/resolve/candidates.js +202 -0
- package/dist/resolve/candidates.js.map +1 -0
- package/dist/resolve/package-exports.d.ts +31 -0
- package/dist/resolve/package-exports.d.ts.map +1 -0
- package/dist/resolve/package-exports.js +229 -0
- package/dist/resolve/package-exports.js.map +1 -0
- package/dist/resolve/pipeline.d.ts +11 -0
- package/dist/resolve/pipeline.d.ts.map +1 -0
- package/dist/resolve/pipeline.js +109 -0
- package/dist/resolve/pipeline.js.map +1 -0
- package/dist/resolve/resolvers.d.ts +25 -0
- package/dist/resolve/resolvers.d.ts.map +1 -0
- package/dist/resolve/resolvers.js +367 -0
- package/dist/resolve/resolvers.js.map +1 -0
- package/dist/restore.d.ts +75 -0
- package/dist/restore.d.ts.map +1 -0
- package/dist/restore.js +136 -0
- package/dist/restore.js.map +1 -0
- package/dist/topology/compose.d.ts +27 -0
- package/dist/topology/compose.d.ts.map +1 -0
- package/dist/topology/compose.js +208 -0
- package/dist/topology/compose.js.map +1 -0
- package/dist/topology/jsonc.d.ts +38 -0
- package/dist/topology/jsonc.d.ts.map +1 -0
- package/dist/topology/jsonc.js +137 -0
- package/dist/topology/jsonc.js.map +1 -0
- package/dist/topology/load.d.ts +61 -0
- package/dist/topology/load.d.ts.map +1 -0
- package/dist/topology/load.js +436 -0
- package/dist/topology/load.js.map +1 -0
- package/dist/topology/manifest.d.ts +48 -0
- package/dist/topology/manifest.d.ts.map +1 -0
- package/dist/topology/manifest.js +109 -0
- package/dist/topology/manifest.js.map +1 -0
- package/dist/topology/project-config.d.ts +39 -0
- package/dist/topology/project-config.d.ts.map +1 -0
- package/dist/topology/project-config.js +150 -0
- package/dist/topology/project-config.js.map +1 -0
- package/dist/topology/workspace-definition.d.ts +26 -0
- package/dist/topology/workspace-definition.d.ts.map +1 -0
- package/dist/topology/workspace-definition.js +104 -0
- package/dist/topology/workspace-definition.js.map +1 -0
- package/dist/traverse.d.ts +49 -0
- package/dist/traverse.d.ts.map +1 -0
- package/dist/traverse.js +167 -0
- package/dist/traverse.js.map +1 -0
- package/dist/types.d.ts +332 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +63 -3
- package/src/assignment.ts +83 -0
- package/src/boundaries/endpoints.ts +273 -0
- package/src/boundaries/index.ts +118 -0
- package/src/boundaries/requests.ts +411 -0
- package/src/edges.ts +334 -0
- package/src/file-index.ts +127 -0
- package/src/graph.ts +591 -0
- package/src/index.ts +58 -0
- package/src/metrics.ts +350 -0
- package/src/package-catalog.ts +145 -0
- package/src/paths.ts +138 -0
- package/src/persistence.ts +746 -0
- package/src/resolve/candidates.ts +230 -0
- package/src/resolve/package-exports.ts +268 -0
- package/src/resolve/pipeline.ts +140 -0
- package/src/resolve/resolvers.ts +468 -0
- package/src/restore.ts +201 -0
- package/src/topology/compose.ts +268 -0
- package/src/topology/jsonc.ts +147 -0
- package/src/topology/load.ts +644 -0
- package/src/topology/manifest.ts +132 -0
- package/src/topology/project-config.ts +183 -0
- package/src/topology/workspace-definition.ts +115 -0
- package/src/traverse.ts +232 -0
- package/src/types.ts +372 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
HttpBoundaryFileFactsV1,
|
|
3
|
+
HttpBoundaryProvenanceV1,
|
|
4
|
+
HttpEndpointV1,
|
|
5
|
+
HttpRouterFactV1,
|
|
6
|
+
HttpServiceBindingV1,
|
|
7
|
+
} from "@untangle-code/domain";
|
|
8
|
+
import { UntangleError } from "@untangle-code/domain";
|
|
9
|
+
|
|
10
|
+
export function under(path: string, root: string): boolean {
|
|
11
|
+
const normalized = root.replace(/\/$/, "");
|
|
12
|
+
return normalized === "" || path === normalized || path.startsWith(`${normalized}/`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function provenance(
|
|
16
|
+
file: HttpBoundaryFileFactsV1,
|
|
17
|
+
range: HttpBoundaryProvenanceV1["range"],
|
|
18
|
+
kind: HttpBoundaryProvenanceV1["kind"],
|
|
19
|
+
name?: string,
|
|
20
|
+
): HttpBoundaryProvenanceV1 {
|
|
21
|
+
return {
|
|
22
|
+
path: file.path,
|
|
23
|
+
contentHash: file.contentHash,
|
|
24
|
+
range,
|
|
25
|
+
kind,
|
|
26
|
+
...(name === undefined ? {} : { name }),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface RouterRef {
|
|
31
|
+
file: HttpBoundaryFileFactsV1;
|
|
32
|
+
router: HttpRouterFactV1;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function key(ref: RouterRef): string {
|
|
36
|
+
return `${ref.file.path}:${ref.router.scope}:${ref.router.name}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function enclosingScopes(file: HttpBoundaryFileFactsV1, scope: string): string[] {
|
|
40
|
+
const result = [scope];
|
|
41
|
+
let current = scope;
|
|
42
|
+
for (let depth = 0; depth < 30 && current !== "module"; depth += 1) {
|
|
43
|
+
current = file.functions.find((fn) => fn.scope === current)?.parentScope ?? "module";
|
|
44
|
+
result.push(current);
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function localRouter(file: HttpBoundaryFileFactsV1, name: string, scope: string): RouterRef | null {
|
|
50
|
+
for (const candidateScope of enclosingScopes(file, scope)) {
|
|
51
|
+
const candidates = file.routers.filter(
|
|
52
|
+
(router) => router.name === name && router.scope === candidateScope,
|
|
53
|
+
);
|
|
54
|
+
const bindings = file.bindings.filter(
|
|
55
|
+
(binding) => binding.scope === candidateScope && binding.name === name,
|
|
56
|
+
);
|
|
57
|
+
const fn = file.functions.find((fn) => fn.scope === candidateScope);
|
|
58
|
+
if (bindings.length > 1 || fn?.parameters.some((param) => param.name === name)) return null;
|
|
59
|
+
if (bindings.length > 0 && candidates.length === 0) return null;
|
|
60
|
+
if (candidates.length === 1) return { file, router: candidates[0] as HttpRouterFactV1 };
|
|
61
|
+
if (candidates.length > 1) return null;
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function importedModulePath(from: string, module: string): string {
|
|
67
|
+
if (!module.startsWith(".")) return module.replaceAll(".", "/");
|
|
68
|
+
const dots = module.match(/^\.+/)?.[0].length ?? 0;
|
|
69
|
+
const dirs = from.split("/").slice(0, -dots);
|
|
70
|
+
return [...dirs, module.slice(dots).replaceAll(".", "/")].filter(Boolean).join("/");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function includedRouter(
|
|
74
|
+
files: readonly HttpBoundaryFileFactsV1[],
|
|
75
|
+
file: HttpBoundaryFileFactsV1,
|
|
76
|
+
name: string,
|
|
77
|
+
scope: string,
|
|
78
|
+
): RouterRef | null {
|
|
79
|
+
const local = localRouter(file, name, scope);
|
|
80
|
+
if (local !== null) return local;
|
|
81
|
+
const segments = name.split(".");
|
|
82
|
+
const base = segments[0];
|
|
83
|
+
if (
|
|
84
|
+
enclosingScopes(file, scope).some(
|
|
85
|
+
(candidateScope) =>
|
|
86
|
+
file.functions
|
|
87
|
+
.find((fn) => fn.scope === candidateScope)
|
|
88
|
+
?.parameters.some((parameter) => parameter.name === base) ||
|
|
89
|
+
file.bindings.some(
|
|
90
|
+
(binding) => binding.scope === candidateScope && binding.name === base,
|
|
91
|
+
) ||
|
|
92
|
+
file.functions.some((fn) => fn.parentScope === candidateScope && fn.name === base),
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
return null;
|
|
96
|
+
const imported = file.pythonImports.filter((item) => item.local === base);
|
|
97
|
+
if (imported.length !== 1) return null;
|
|
98
|
+
const item = imported[0];
|
|
99
|
+
if (item === undefined) return null;
|
|
100
|
+
const directRouter = segments.length === 1;
|
|
101
|
+
const module = directRouter ? item.module : `${item.module}.${item.imported ?? item.local}`;
|
|
102
|
+
const modulePath = importedModulePath(file.path, module);
|
|
103
|
+
const candidates = files.filter(
|
|
104
|
+
(candidate) =>
|
|
105
|
+
candidate.path === `${modulePath}.py` || candidate.path.endsWith(`/${modulePath}.py`),
|
|
106
|
+
);
|
|
107
|
+
if (candidates.length !== 1) return null;
|
|
108
|
+
const target = candidates[0];
|
|
109
|
+
return target === undefined
|
|
110
|
+
? null
|
|
111
|
+
: localRouter(
|
|
112
|
+
target,
|
|
113
|
+
directRouter ? (item.imported ?? "router") : segments.slice(1).join("."),
|
|
114
|
+
"module",
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function routePath(...parts: (string | null)[]): string | null {
|
|
119
|
+
if (parts.some((part) => part === null)) return null;
|
|
120
|
+
const result = parts.join("");
|
|
121
|
+
if (
|
|
122
|
+
!result.startsWith("/") ||
|
|
123
|
+
result.includes("//") ||
|
|
124
|
+
result.includes("?") ||
|
|
125
|
+
result.includes("#")
|
|
126
|
+
)
|
|
127
|
+
return null;
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function collectEndpoints(
|
|
132
|
+
files: readonly HttpBoundaryFileFactsV1[],
|
|
133
|
+
services: readonly HttpServiceBindingV1[],
|
|
134
|
+
signal?: AbortSignal,
|
|
135
|
+
): { endpoints: HttpEndpointV1[]; limitations: { path: string; reason: string }[] } {
|
|
136
|
+
const endpoints: HttpEndpointV1[] = [],
|
|
137
|
+
limitations: { path: string; reason: string }[] = [];
|
|
138
|
+
const mounted = new Set<string>();
|
|
139
|
+
let visits = 0;
|
|
140
|
+
const visit = (
|
|
141
|
+
ref: RouterRef,
|
|
142
|
+
prefix: string | null,
|
|
143
|
+
chain: HttpBoundaryProvenanceV1[],
|
|
144
|
+
seen: Set<string>,
|
|
145
|
+
serviceId: string | null,
|
|
146
|
+
): void => {
|
|
147
|
+
if (signal?.aborted)
|
|
148
|
+
throw new UntangleError("JOB_CANCELLED", "HTTP endpoint collection cancelled");
|
|
149
|
+
visits += 1;
|
|
150
|
+
if (visits > 10000 || endpoints.length > 20000)
|
|
151
|
+
throw new UntangleError("ANALYSIS_INCOMPLETE", "HTTP router expansion budget exceeded");
|
|
152
|
+
if (seen.size >= 100) {
|
|
153
|
+
limitations.push({ path: ref.file.path, reason: "Router include depth budget exceeded" });
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (seen.has(key(ref))) {
|
|
157
|
+
limitations.push({
|
|
158
|
+
path: ref.file.path,
|
|
159
|
+
reason: `Router include cycle at ${ref.router.name}`,
|
|
160
|
+
});
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
mounted.add(key(ref));
|
|
164
|
+
const nextSeen = new Set([...seen, key(ref)]);
|
|
165
|
+
const routerPrefix = routePath(prefix, ref.router.prefix);
|
|
166
|
+
const nextPrefix = prefix === "" && ref.router.prefix === "" ? "" : routerPrefix;
|
|
167
|
+
const provenanceChain = [
|
|
168
|
+
...chain,
|
|
169
|
+
provenance(ref.file, ref.router.range, "router", ref.router.name),
|
|
170
|
+
];
|
|
171
|
+
for (const route of ref.file.routes.filter(
|
|
172
|
+
(route) =>
|
|
173
|
+
route.router === ref.router.name &&
|
|
174
|
+
localRouter(ref.file, route.router, route.scope)?.router === ref.router,
|
|
175
|
+
)) {
|
|
176
|
+
const path = routePath(nextPrefix, route.path);
|
|
177
|
+
if (path === null)
|
|
178
|
+
limitations.push({
|
|
179
|
+
path: ref.file.path,
|
|
180
|
+
reason: `Dynamic or invalid route prefix/path for ${route.handler}`,
|
|
181
|
+
});
|
|
182
|
+
else {
|
|
183
|
+
if (/\{[^}]+:(?!str\})[^}]+\}/.test(path))
|
|
184
|
+
limitations.push({
|
|
185
|
+
path: ref.file.path,
|
|
186
|
+
reason: `Typed or catch-all route converters are not matched: ${path}`,
|
|
187
|
+
});
|
|
188
|
+
endpoints.push({
|
|
189
|
+
serviceId,
|
|
190
|
+
method: route.method,
|
|
191
|
+
path,
|
|
192
|
+
handler: route.handler,
|
|
193
|
+
provenance: [
|
|
194
|
+
...provenanceChain,
|
|
195
|
+
provenance(ref.file, route.range, "route", route.handler),
|
|
196
|
+
],
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
for (const include of ref.file.includes.filter(
|
|
201
|
+
(include) => localRouter(ref.file, include.owner, include.scope)?.router === ref.router,
|
|
202
|
+
)) {
|
|
203
|
+
const target = includedRouter(files, ref.file, include.router, include.scope);
|
|
204
|
+
if (target === null) {
|
|
205
|
+
limitations.push({
|
|
206
|
+
path: ref.file.path,
|
|
207
|
+
reason: `Unresolved router include ${include.router}`,
|
|
208
|
+
});
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (
|
|
212
|
+
serviceId !== null &&
|
|
213
|
+
!services.some(
|
|
214
|
+
(service) =>
|
|
215
|
+
service.id === serviceId &&
|
|
216
|
+
service.backendRoots.some((root) => under(target.file.path, root)),
|
|
217
|
+
)
|
|
218
|
+
) {
|
|
219
|
+
limitations.push({
|
|
220
|
+
path: ref.file.path,
|
|
221
|
+
reason: `Router include ${include.router} lies outside declared backend roots`,
|
|
222
|
+
});
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
const includePrefix =
|
|
226
|
+
nextPrefix === "" && include.prefix === "" ? "" : routePath(nextPrefix, include.prefix);
|
|
227
|
+
visit(
|
|
228
|
+
target,
|
|
229
|
+
includePrefix,
|
|
230
|
+
[...provenanceChain, provenance(ref.file, include.range, "include", include.router)],
|
|
231
|
+
nextSeen,
|
|
232
|
+
serviceId,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
for (const file of files) {
|
|
237
|
+
const unsafeMounts = files.filter((candidate) =>
|
|
238
|
+
candidate.limitations.some((reason) => reason.startsWith("ASGI app.mount")),
|
|
239
|
+
);
|
|
240
|
+
const unsafeService = unsafeMounts.some(
|
|
241
|
+
(unsafe) =>
|
|
242
|
+
services.length === 0 ||
|
|
243
|
+
services.some(
|
|
244
|
+
(service) =>
|
|
245
|
+
service.backendRoots.some((root) => under(file.path, root)) &&
|
|
246
|
+
service.backendRoots.some((root) => under(unsafe.path, root)),
|
|
247
|
+
),
|
|
248
|
+
);
|
|
249
|
+
if (unsafeService) {
|
|
250
|
+
limitations.push({
|
|
251
|
+
path: file.path,
|
|
252
|
+
reason: "App endpoint inventory suppressed because ASGI mount routing is unsupported",
|
|
253
|
+
});
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
for (const router of file.routers.filter((router) => router.kind === "app")) {
|
|
257
|
+
const matching = services.filter((service) =>
|
|
258
|
+
service.backendRoots.some((root) => under(file.path, root)),
|
|
259
|
+
);
|
|
260
|
+
for (const service of matching.length > 0 ? matching : [null])
|
|
261
|
+
visit({ file, router }, "", [], new Set(), service?.id ?? null);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
for (const file of files)
|
|
265
|
+
for (const router of file.routers) {
|
|
266
|
+
if (router.kind === "router" && !mounted.has(key({ file, router })))
|
|
267
|
+
limitations.push({
|
|
268
|
+
path: file.path,
|
|
269
|
+
reason: `Router ${router.name} is not proven mounted on a FastAPI app`,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
return { endpoints, limitations };
|
|
273
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
HttpBoundaryFileFactsV1,
|
|
3
|
+
HttpBoundaryReportV1,
|
|
4
|
+
HttpBoundaryRequestV1,
|
|
5
|
+
HttpEndpointV1,
|
|
6
|
+
HttpServiceBindingV1,
|
|
7
|
+
} from "@untangle-code/domain";
|
|
8
|
+
import { compareCodepoints, UntangleError } from "@untangle-code/domain";
|
|
9
|
+
|
|
10
|
+
import { collectEndpoints, under } from "./endpoints.js";
|
|
11
|
+
import { collectRequests } from "./requests.js";
|
|
12
|
+
|
|
13
|
+
export interface AnalyzeHttpBoundariesOptions {
|
|
14
|
+
files: readonly HttpBoundaryFileFactsV1[];
|
|
15
|
+
services: readonly HttpServiceBindingV1[];
|
|
16
|
+
signal?: AbortSignal;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function checkCancelled(signal?: AbortSignal): void {
|
|
20
|
+
if (signal?.aborted) throw new UntangleError("JOB_CANCELLED", "HTTP boundary analysis cancelled");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function matchesPath(request: string, route: string): boolean {
|
|
24
|
+
const requested = request.split("/"),
|
|
25
|
+
target = route.split("/");
|
|
26
|
+
if (requested.length !== target.length) return false;
|
|
27
|
+
return target.every((segment, index) => {
|
|
28
|
+
const value = requested[index] ?? "";
|
|
29
|
+
if (/^\{[A-Za-z_][A-Za-z0-9_]*(?::str)?\}$/.test(segment)) return value !== "";
|
|
30
|
+
return segment === value && value !== "{*}";
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function serviceApplies(
|
|
35
|
+
request: { origin: string | null },
|
|
36
|
+
service: HttpServiceBindingV1,
|
|
37
|
+
): boolean {
|
|
38
|
+
return request.origin === null
|
|
39
|
+
? service.sameOrigin === true
|
|
40
|
+
: (service.origins ?? []).some((origin) => {
|
|
41
|
+
try {
|
|
42
|
+
return new URL(origin).origin === request.origin && new URL(origin).pathname === "/";
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Resolve declared deployment scopes plus method/path evidence, never bare URL coincidences. */
|
|
50
|
+
export function analyzeHttpBoundaries(options: AnalyzeHttpBoundariesOptions): HttpBoundaryReportV1 {
|
|
51
|
+
checkCancelled(options.signal);
|
|
52
|
+
const files = [...options.files].sort((a, b) => compareCodepoints(a.path, b.path));
|
|
53
|
+
const services = [...options.services].sort((a, b) => compareCodepoints(a.id, b.id));
|
|
54
|
+
if (new Set(services.map((service) => service.id)).size !== services.length)
|
|
55
|
+
throw new TypeError("HTTP service identifiers must be unique");
|
|
56
|
+
if (new Set(files.map((file) => file.path)).size !== files.length)
|
|
57
|
+
throw new TypeError("HTTP source paths must be unique");
|
|
58
|
+
const { endpoints, limitations } = collectEndpoints(files, services, options.signal);
|
|
59
|
+
const requests: HttpBoundaryRequestV1[] = [];
|
|
60
|
+
for (const file of files) {
|
|
61
|
+
checkCancelled(options.signal);
|
|
62
|
+
limitations.push(...file.limitations.map((reason) => ({ path: file.path, reason })));
|
|
63
|
+
if (file.languageId === "python") continue;
|
|
64
|
+
for (const request of collectRequests(file)) {
|
|
65
|
+
checkCancelled(options.signal);
|
|
66
|
+
const applicable = services.filter(
|
|
67
|
+
(service) =>
|
|
68
|
+
service.frontendRoots.some((root) => under(file.path, root)) &&
|
|
69
|
+
serviceApplies(request, service),
|
|
70
|
+
);
|
|
71
|
+
const serviceIds = applicable.map((service) => service.id);
|
|
72
|
+
const targets: HttpEndpointV1[] =
|
|
73
|
+
request.reason === undefined && request.path !== null && request.method !== null
|
|
74
|
+
? endpoints.filter(
|
|
75
|
+
(endpoint) =>
|
|
76
|
+
endpoint.serviceId !== null &&
|
|
77
|
+
serviceIds.includes(endpoint.serviceId) &&
|
|
78
|
+
endpoint.method === request.method &&
|
|
79
|
+
matchesPath(request.path as string, endpoint.path),
|
|
80
|
+
)
|
|
81
|
+
: [];
|
|
82
|
+
const status =
|
|
83
|
+
targets.length === 1 ? "resolved" : targets.length > 1 ? "ambiguous" : "unresolved";
|
|
84
|
+
const reason =
|
|
85
|
+
request.reason ??
|
|
86
|
+
(applicable.length === 0
|
|
87
|
+
? "No declared service binding for the request origin and frontend root"
|
|
88
|
+
: targets.length === 0
|
|
89
|
+
? "No mounted route matches the HTTP method and normalized path"
|
|
90
|
+
: targets.length > 1
|
|
91
|
+
? "Multiple mounted routes match; no target was selected"
|
|
92
|
+
: undefined);
|
|
93
|
+
requests.push({
|
|
94
|
+
...request,
|
|
95
|
+
status,
|
|
96
|
+
...(reason === undefined ? {} : { reason }),
|
|
97
|
+
serviceIds,
|
|
98
|
+
targets,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
schemaVersion: 1,
|
|
104
|
+
kind: "http-boundaries",
|
|
105
|
+
services,
|
|
106
|
+
endpoints,
|
|
107
|
+
requests,
|
|
108
|
+
limitations,
|
|
109
|
+
summary: {
|
|
110
|
+
files: files.length,
|
|
111
|
+
endpoints: endpoints.length,
|
|
112
|
+
requests: requests.length,
|
|
113
|
+
resolved: requests.filter((request) => request.status === "resolved").length,
|
|
114
|
+
ambiguous: requests.filter((request) => request.status === "ambiguous").length,
|
|
115
|
+
unresolved: requests.filter((request) => request.status === "unresolved").length,
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|