@shortlink-org/portolan 0.2.4 → 0.4.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/README.md +174 -6
- package/catalog/enum_test.go +46 -0
- package/catalog/evidence_test.go +35 -0
- package/catalog/model.go +1213 -0
- package/catalog/roundtrip_test.go +210 -0
- package/catalog/via_test.go +38 -0
- package/cli/init.test.mjs +6 -1
- package/cli/portolan.mjs +14 -1
- package/cli/portolan.test.mjs +49 -0
- package/go.mod +21 -0
- package/go.sum +34 -0
- package/internal/gocall/README.md +19 -0
- package/internal/gocall/analyze.go +189 -0
- package/internal/gocall/analyze_test.go +107 -0
- package/internal/gohttp/analyze.go +2562 -0
- package/internal/gohttp/destination.go +373 -0
- package/internal/gohttp/endpoints.go +1067 -0
- package/internal/gohttp/roots.go +320 -0
- package/internal/gohttp/typed.go +96 -0
- package/internal/goscan/constants.go +85 -0
- package/internal/goscan/goscan_test.go +227 -0
- package/internal/goscan/index.go +629 -0
- package/internal/goscan/index_test.go +66 -0
- package/internal/goscan/names.go +52 -0
- package/internal/goscan/parse_test.go +11 -0
- package/internal/goscan/source.go +37 -0
- package/internal/goscan/tree.go +284 -0
- package/internal/goscan/types.go +99 -0
- package/internal/wsdl/ids.go +127 -0
- package/internal/wsdl/ids_test.go +21 -0
- package/internal/wsdl/model.go +70 -0
- package/internal/wsdl/parse.go +949 -0
- package/internal/wsdl/parse_test.go +170 -0
- package/package.json +28 -15
- package/plugin/describe.go +156 -0
- package/plugin/describe_test.go +114 -0
- package/plugin/protocol.go +141 -0
- package/plugin/schematest/schematest.go +126 -0
- package/plugins/README.md +404 -50
- package/plugins/cmd/portolan-http-clients/main.go +19 -0
- package/plugins/extract-argocd/options.schema.json +44 -0
- package/plugins/extract-celery/extract.py +0 -2
- package/plugins/extract-celery/extract_test.py +1 -1
- package/plugins/extract-celery/main.py +1 -0
- package/plugins/extract-csharp-ddd/README.md +213 -0
- package/plugins/extract-csharp-ddd/options.schema.json +55 -0
- package/plugins/extract-django/README.md +52 -19
- package/plugins/extract-django/choices.py +47 -0
- package/plugins/extract-django/domain.py +30 -18
- package/plugins/extract-django/extract.py +21 -7
- package/plugins/extract-django/extract_test.py +68 -2
- package/plugins/extract-django/lifecycle.py +4 -28
- package/plugins/extract-django/main.py +1 -0
- package/plugins/extract-django/operations.py +1 -1
- package/plugins/extract-django/routing_test.py +109 -1
- package/plugins/extract-django/rules.py +154 -0
- package/plugins/extract-django/rules_test.py +158 -0
- package/plugins/extract-django/store.py +1 -1
- package/plugins/extract-django/transport.py +101 -55
- package/plugins/extract-django/verbs.py +241 -0
- package/plugins/extract-go/README.md +47 -0
- package/plugins/extract-go-sqs/options.schema.json +21 -0
- package/plugins/extract-http-clients/describe.go +20 -0
- package/plugins/extract-http-clients/describe_test.go +11 -0
- package/plugins/extract-http-clients/extract.go +740 -0
- package/plugins/extract-http-clients/extract_test.go +1561 -0
- package/plugins/extract-http-clients/main.go +41 -0
- package/plugins/extract-java/build/org/portolan/extract/Extract.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Main.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Protocol$Builder.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Protocol$Input.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Protocol$Options.class +0 -0
- package/plugins/extract-k8s/options.schema.json +32 -0
- package/plugins/extract-laravel/Cargo.lock +962 -0
- package/plugins/extract-laravel/Cargo.toml +20 -0
- package/plugins/extract-laravel/README.md +200 -0
- package/plugins/extract-laravel/options.schema.json +68 -0
- package/plugins/extract-laravel/rustfmt.toml +1 -0
- package/plugins/extract-php-ddd/Cargo.lock +972 -0
- package/plugins/extract-php-ddd/Cargo.toml +22 -0
- package/plugins/extract-php-ddd/README.md +141 -0
- package/plugins/extract-php-ddd/options.schema.json +50 -0
- package/plugins/extract-php-ddd/rustfmt.toml +1 -0
- package/plugins/extract-python-kafka/extract.py +0 -2
- package/plugins/extract-python-kafka/extract_test.py +1 -1
- package/plugins/extract-python-kafka/main.py +1 -0
- package/plugins/extract-terraform/options.schema.json +26 -0
- package/plugins/extract-ts/extract.test.ts +2 -2
- package/plugins/extract-ts/extract.ts +4 -5
- package/plugins/extract-ts/graphql.test.ts +1 -1
- package/plugins/extract-ts/main.ts +1 -0
- package/plugins/openapi/ids.go +261 -0
- package/plugins/openapi/ids_test.go +98 -0
- package/plugins/phpscan/Cargo.lock +953 -0
- package/plugins/phpscan/Cargo.toml +19 -0
- package/plugins/phpscan/rustfmt.toml +1 -0
- package/plugins/portolan-go.wasm +0 -0
- package/plugins/pyplugin/catalog.py +24 -1
- package/plugins/pyplugin/protocol.py +1 -5
- package/plugins/verify-otel/options.schema.json +12 -0
- package/portolan.json +132 -4
- package/rules/builtin.json +218 -0
- package/schema/portolan.schema.json +905 -4
- package/scripts/README.md +21 -13
- package/scripts/catalog-sources.mjs +6 -0
- package/scripts/delivery-presets.mjs +57 -14
- package/scripts/diff.mjs +5 -1
- package/scripts/django-aggregates.test.mjs +58 -0
- package/scripts/gen-likec4.mjs +150 -17
- package/scripts/gen-likec4.test.mjs +96 -0
- package/scripts/gen.mjs +148 -118
- package/scripts/gitops-example.test.mjs +108 -0
- package/scripts/go-discovery.test.mjs +30 -0
- package/scripts/history.mjs +186 -3
- package/scripts/history.test.mjs +1 -1
- package/scripts/host-plugins/fetch-argocd.mjs +338 -0
- package/scripts/host-plugins/fetch-argocd.options.json +49 -0
- package/scripts/host-plugins/fetch-argocd.test.mjs +274 -0
- package/scripts/host-plugins/fetch-bsr.mjs +1 -0
- package/scripts/host-plugins/fetch-csr.mjs +1 -0
- package/scripts/host-plugins/fetch-git.mjs +78 -21
- package/scripts/host-plugins/fetch-git.test.mjs +62 -8
- package/scripts/host-plugins/fetch-k8s.mjs +263 -0
- package/scripts/host-plugins/fetch-k8s.options.json +50 -0
- package/scripts/host-plugins/fetch-k8s.test.mjs +259 -0
- package/scripts/host-plugins/k8s-topology.mjs +183 -0
- package/scripts/json-format.mjs +192 -0
- package/scripts/json-format.test.mjs +97 -0
- package/scripts/local-api.mjs +287 -12
- package/scripts/local-api.test.mjs +150 -5
- package/scripts/local-discovery.mjs +94 -9
- package/scripts/manifest.mjs +14 -3
- package/scripts/manifest.test.mjs +24 -0
- package/scripts/output-diff.mjs +94 -0
- package/scripts/output-diff.test.mjs +36 -0
- package/scripts/package-smoke.mjs +62 -4
- package/scripts/plugin-host.mjs +45 -3
- package/scripts/plugin-host.test.mjs +9 -0
- package/scripts/plugin-wasm-worker.mjs +4 -1
- package/scripts/plugins-fresh.mjs +97 -0
- package/scripts/plugins-fresh.test.mjs +64 -0
- package/scripts/provenance.mjs +72 -0
- package/scripts/provenance.test.mjs +149 -0
- package/scripts/run-builtin.mjs +62 -7
- package/scripts/schema.mjs +167 -19
- package/scripts/trace-trials.mjs +176 -0
- package/scripts/trace-trials.test.mjs +142 -0
- package/scripts/warning-policy.mjs +167 -0
- package/scripts/warning-policy.test.mjs +93 -0
- package/src/app/Breadcrumbs.test.ts +3 -0
- package/src/app/Breadcrumbs.tsx +3 -0
- package/src/app/CatalogApp.tsx +2 -0
- package/src/app/Sidebar.tsx +3 -3
- package/src/app/SidebarFooter.tsx +20 -4
- package/src/catalog-docs.test.ts +64 -0
- package/src/catalog-docs.ts +35 -0
- package/src/catalog-error.test.ts +15 -0
- package/src/catalog-index.ts +25 -0
- package/src/catalog-model.ts +293 -5
- package/src/catalog-validation.ts +113 -2
- package/src/catalog.test.ts +40 -1
- package/src/chat/Starter.tsx +5 -11
- package/src/chat/tools.test.ts +27 -0
- package/src/chat/tools.ts +5 -9
- package/src/components/CatalogStamp.tsx +10 -8
- package/src/components/ChannelRows.test.tsx +76 -1
- package/src/components/ChannelRows.tsx +223 -60
- package/src/components/DeploymentRows.tsx +131 -0
- package/src/components/DocsLinks.test.tsx +27 -0
- package/src/components/DocsLinks.tsx +56 -0
- package/src/components/FieldTree.tsx +5 -2
- package/src/components/HTTPDestinationEvidence.test.tsx +23 -0
- package/src/components/HTTPDestinationEvidence.tsx +31 -0
- package/src/components/Integrations.tsx +1 -1
- package/src/components/MachineDocs.tsx +6 -5
- package/src/components/MethodRows.tsx +9 -2
- package/src/components/PluginIcon.tsx +77 -0
- package/src/components/ProblemRow.tsx +173 -153
- package/src/components/RelationEvidence.test.tsx +14 -0
- package/src/components/RelationEvidence.tsx +53 -0
- package/src/components/RuleMarks.tsx +22 -0
- package/src/components/ShapeRows.tsx +24 -20
- package/src/data.ts +46 -7
- package/src/enrich.test.ts +459 -4
- package/src/enrich.ts +308 -7
- package/src/er/ErCanvas.tsx +218 -12
- package/src/er/GroupNode.tsx +57 -0
- package/src/er/StoreHeader.tsx +1 -0
- package/src/er/layout.test.ts +85 -2
- package/src/er/layout.ts +140 -5
- package/src/er/spec.test.ts +17 -0
- package/src/er/spec.ts +23 -10
- package/src/flow/Recordings.test.tsx +52 -0
- package/src/flow/Recordings.tsx +236 -0
- package/src/flow/StepDetail.tsx +59 -0
- package/src/flow/TraceTrial.tsx +419 -0
- package/src/flow/evidence.test.ts +16 -0
- package/src/flow/evidence.ts +34 -0
- package/src/flow/examples.test.ts +33 -0
- package/src/flow/examples.ts +37 -0
- package/src/flow/outline.test.ts +28 -0
- package/src/flow/outline.ts +7 -2
- package/src/flow/trace-trial-resume.test.ts +42 -0
- package/src/flow/trace-trial-resume.ts +74 -0
- package/src/graph/elk.ts +78 -0
- package/src/index.css +68 -0
- package/src/landing/DraggableReveal.tsx +3 -2
- package/src/landing/EvidencePipeline.tsx +105 -0
- package/src/landing/LandingPage.tsx +17 -70
- package/src/landing/ProductTour.tsx +6 -6
- package/src/lib/all-problems.ts +27 -17
- package/src/lib/catalog-diff.ts +1 -1
- package/src/lib/centrality.test.ts +251 -0
- package/src/lib/centrality.ts +232 -0
- package/src/lib/confluence.test.ts +37 -0
- package/src/lib/confluence.ts +41 -0
- package/src/lib/context-color.ts +1 -1
- package/src/lib/deployment-drift.ts +22 -0
- package/src/lib/derive.ts +43 -69
- package/src/lib/django-aggregates.d.mts +9 -0
- package/src/lib/django-aggregates.mjs +36 -0
- package/src/lib/django-aggregates.test.ts +29 -0
- package/src/lib/django-aggregates.ts +5 -0
- package/src/lib/environments.test.ts +83 -0
- package/src/lib/environments.ts +57 -0
- package/src/lib/integration-url.test.ts +30 -0
- package/src/lib/integration-url.ts +63 -0
- package/src/lib/kafka-ui.ts +3 -45
- package/src/lib/local-api.ts +116 -4
- package/src/lib/notion.ts +13 -0
- package/src/lib/plugin-index.json +2765 -0
- package/src/lib/plugins.test.ts +68 -0
- package/src/lib/plugins.ts +264 -0
- package/src/lib/problem-flows.test.ts +61 -0
- package/src/lib/problem-flows.ts +78 -0
- package/src/lib/problem-rules-cel.d.mts +43 -0
- package/src/lib/problem-rules-cel.mjs +407 -0
- package/src/lib/problem-rules.test.ts +287 -0
- package/src/lib/problem-rules.ts +271 -0
- package/src/lib/problem-subjects.ts +737 -0
- package/src/lib/rule-entries.ts +39 -0
- package/src/lib/{data-problems.test.ts → rules-data.test.ts} +18 -16
- package/src/lib/rules-deploy.test.ts +133 -0
- package/src/lib/{problems.test.ts → rules-edges.test.ts} +8 -3
- package/src/lib/{proto-problems.test.ts → rules-proto.test.ts} +9 -25
- package/src/lib/{wire-problems.test.ts → rules-wire.test.ts} +13 -11
- package/src/lib/rules.test.ts +51 -0
- package/src/lib/rules.ts +86 -0
- package/src/lib/setup-info.test.ts +17 -0
- package/src/lib/setup-info.ts +58 -0
- package/src/lib/shape.test.ts +32 -0
- package/src/lib/shape.ts +30 -6
- package/src/lib/tech.ts +16 -0
- package/src/lib/trace-project.test.ts +34 -0
- package/src/lib/trace-project.ts +50 -0
- package/src/lib/use-problems.ts +23 -0
- package/src/lib/warnings.test.ts +63 -0
- package/src/lib/warnings.ts +260 -0
- package/src/likec4/ids.test.ts +6 -2
- package/src/likec4/ids.ts +43 -0
- package/src/main.tsx +23 -0
- package/src/map/ContextMapGraph.tsx +76 -32
- package/src/merge-deployments.test.ts +127 -0
- package/src/merge.test.ts +82 -0
- package/src/merge.ts +189 -18
- package/src/pages/AggregatePage.tsx +65 -14
- package/src/pages/ContextMap.tsx +45 -3
- package/src/pages/ContextPage.tsx +8 -5
- package/src/pages/EventPage.tsx +15 -5
- package/src/pages/FlowDetail.tsx +23 -2
- package/src/pages/GraphPage.tsx +40 -3
- package/src/pages/Overview.tsx +152 -12
- package/src/pages/PluginIndex.tsx +190 -0
- package/src/pages/Problems.tsx +396 -128
- package/src/pages/ServicePage.tsx +62 -5
- package/src/pages/Settings.tsx +217 -43
- package/src/pages/settings/AboutSettings.tsx +8 -1
- package/src/pages/settings/DjangoAggregateChoices.tsx +79 -0
- package/src/pages/settings/IntegrationsSettings.tsx +63 -17
- package/src/pages/settings/RecordingSettings.tsx +138 -0
- package/src/pages/settings/RulesSettings.tsx +825 -0
- package/src/routes.test.ts +9 -0
- package/src/routes.ts +22 -1
- package/src/selection/DetailPanel.tsx +15 -0
- package/src/virtual-provenance.d.ts +11 -0
- package/vite.config.ts +5 -0
- package/scripts/vendor-lock.mjs +0 -58
- package/scripts/vendor-lock.test.mjs +0 -69
- package/src/lib/data-problems.ts +0 -314
- package/src/lib/proto-problems.ts +0 -237
- package/src/lib/wire-problems.ts +0 -342
package/src/enrich.ts
CHANGED
|
@@ -24,6 +24,7 @@ import type {
|
|
|
24
24
|
Flow,
|
|
25
25
|
FlowNode,
|
|
26
26
|
RpcCall,
|
|
27
|
+
HTTPDestination,
|
|
27
28
|
Service,
|
|
28
29
|
Status,
|
|
29
30
|
Step,
|
|
@@ -60,7 +61,7 @@ export function enrichCatalog(input: Catalog): Enriched {
|
|
|
60
61
|
// rather than the name.
|
|
61
62
|
const catalog = resolveForeignKeys(
|
|
62
63
|
resolveStoreAccesses(
|
|
63
|
-
composeExecutionContinuations(resolveWireNames(input)),
|
|
64
|
+
composeExecutionContinuations(resolveWireNames(resolveHTTPCalls(input))),
|
|
64
65
|
),
|
|
65
66
|
);
|
|
66
67
|
|
|
@@ -228,6 +229,266 @@ export function enrichCatalog(input: Catalog): Enriched {
|
|
|
228
229
|
return { catalog: { ...catalog, contexts }, derived };
|
|
229
230
|
}
|
|
230
231
|
|
|
232
|
+
interface HTTPProvider {
|
|
233
|
+
service: string;
|
|
234
|
+
context: string;
|
|
235
|
+
ref: string;
|
|
236
|
+
method: string;
|
|
237
|
+
path: string;
|
|
238
|
+
/** Set when the manifests, not the route alone, chose this provider. */
|
|
239
|
+
basis?: "kubernetes-host";
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Resolves the protocol-neutral calls emitted by the HTTP client extractor
|
|
244
|
+
* against HTTP routes contributed by server-side extractors.
|
|
245
|
+
*
|
|
246
|
+
* No source fragment can do this on its own: a client repository knows the
|
|
247
|
+
* verb and path but not which of the estate's services answers it, while an
|
|
248
|
+
* OpenAPI or framework fragment knows only what its own service provides.
|
|
249
|
+
* The merged catalog is the first place both facts exist.
|
|
250
|
+
*
|
|
251
|
+
* Proven full paths are authoritative and never fall back to a suffix.
|
|
252
|
+
* Legacy calls can still use a unique suffix because mounted
|
|
253
|
+
* applications commonly see only their local route (`/get-admin-settings`)
|
|
254
|
+
* while the server extractor records the mount too
|
|
255
|
+
* (`/settings/get-admin-settings`). Ambiguous matches and possible self-calls
|
|
256
|
+
* deliberately remain unresolved.
|
|
257
|
+
*/
|
|
258
|
+
function resolveHTTPCalls(input: Catalog): Catalog {
|
|
259
|
+
const providers: HTTPProvider[] = [];
|
|
260
|
+
// What the manifests said, where a tree of them was read: the names each
|
|
261
|
+
// service answers on, and the names each caller is configured to dial.
|
|
262
|
+
const hostsByService = new Map<string, Set<string>>();
|
|
263
|
+
const dialsByService = new Map<string, string[]>();
|
|
264
|
+
for (const context of input.contexts) {
|
|
265
|
+
for (const service of context.services) {
|
|
266
|
+
if (service.hosts?.length) {
|
|
267
|
+
hostsByService.set(service.id, new Set(service.hosts));
|
|
268
|
+
}
|
|
269
|
+
if (service.dials?.length) dialsByService.set(service.id, service.dials);
|
|
270
|
+
for (const provided of service.provides) {
|
|
271
|
+
for (const method of provided.methods) {
|
|
272
|
+
// A route with an empty method is mounted but its verb is unknown
|
|
273
|
+
// (extract-django's `Planet.fetch`); the path alone never confirms
|
|
274
|
+
// a link, so it is kept out of the candidates deliberately.
|
|
275
|
+
if (!method.http || !method.http.method) continue;
|
|
276
|
+
providers.push({
|
|
277
|
+
service: service.id,
|
|
278
|
+
context: context.id,
|
|
279
|
+
ref: `${provided.id}/${method.name}`,
|
|
280
|
+
method: method.http.method.toUpperCase(),
|
|
281
|
+
path: method.http.path,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (providers.length === 0) return input;
|
|
288
|
+
|
|
289
|
+
const resolvedByCaller = new Map<string, Map<string, HTTPProvider>>();
|
|
290
|
+
const resolve = (caller: string, call: RpcCall): HTTPProvider | undefined => {
|
|
291
|
+
if (call.status !== "unresolved") return undefined;
|
|
292
|
+
const raw = rawHTTPRoute(call.id);
|
|
293
|
+
const route = call.destination?.fullPath
|
|
294
|
+
? { method: call.destination.method, path: call.destination.fullPath }
|
|
295
|
+
: raw;
|
|
296
|
+
if (!route) return undefined;
|
|
297
|
+
|
|
298
|
+
const candidates = providers.filter(
|
|
299
|
+
(provider) =>
|
|
300
|
+
provider.service !== caller &&
|
|
301
|
+
provider.method === route.method &&
|
|
302
|
+
(call.destination?.fullPath
|
|
303
|
+
? sameHTTPShape(provider.path, route.path)
|
|
304
|
+
: sameHTTPPath(provider.path, route.path)),
|
|
305
|
+
);
|
|
306
|
+
const exact = uniqueHTTPProviders(
|
|
307
|
+
candidates.filter((provider) => sameHTTPShape(provider.path, route.path)),
|
|
308
|
+
);
|
|
309
|
+
const matches = exact.length > 0 ? exact : uniqueHTTPProviders(candidates);
|
|
310
|
+
if (matches.length === 1) return matches[0];
|
|
311
|
+
if (matches.length < 2) return undefined;
|
|
312
|
+
// Ambiguous by route alone. The manifests may say which of them the
|
|
313
|
+
// caller reaches: the host the call names, or failing that the hosts the
|
|
314
|
+
// caller's workload is configured to dial. They only ever decide between
|
|
315
|
+
// providers of the route; a host never conjures a provider that does not
|
|
316
|
+
// have it.
|
|
317
|
+
const host = callHost(call);
|
|
318
|
+
const dials = dialsByService.get(caller) ?? [];
|
|
319
|
+
const reachable = matches.filter((provider) => {
|
|
320
|
+
const hosts = hostsByService.get(provider.service);
|
|
321
|
+
if (!hosts) return false;
|
|
322
|
+
return host ? hosts.has(host) : dials.some((dial) => hosts.has(dial));
|
|
323
|
+
});
|
|
324
|
+
return reachable.length === 1
|
|
325
|
+
? { ...reachable[0]!, basis: "kubernetes-host" }
|
|
326
|
+
: undefined;
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const evidence = (call: Pick<RpcCall, "id" | "source" | "destination">, provider: HTTPProvider): HTTPDestination => {
|
|
330
|
+
const raw = rawHTTPRoute(call.id);
|
|
331
|
+
const destination = call.destination ?? {
|
|
332
|
+
callSite: call.source, endpointExpression: raw?.path ?? call.id,
|
|
333
|
+
method: raw?.method ?? provider.method, localPath: raw?.path,
|
|
334
|
+
};
|
|
335
|
+
return { ...destination, resolution: {
|
|
336
|
+
basis: provider.basis ?? (destination.fullPath ? "full-path" : sameHTTPShape(provider.path, raw?.path ?? "") ? "exact-route" : "unique-suffix"),
|
|
337
|
+
provider: provider.service, route: provider.path,
|
|
338
|
+
} };
|
|
339
|
+
};
|
|
340
|
+
let changed = false;
|
|
341
|
+
const contexts = input.contexts.map((context) => ({
|
|
342
|
+
...context,
|
|
343
|
+
services: context.services.map((service) => {
|
|
344
|
+
const resolved = new Map<string, HTTPProvider>();
|
|
345
|
+
const mapped = service.consumes.map((call) => {
|
|
346
|
+
const provider = resolve(service.id, call);
|
|
347
|
+
if (!provider) return call;
|
|
348
|
+
changed = true;
|
|
349
|
+
resolved.set(call.id, provider);
|
|
350
|
+
return {
|
|
351
|
+
...call,
|
|
352
|
+
id: provider.ref,
|
|
353
|
+
destination: evidence(call, provider),
|
|
354
|
+
peer: provider.service,
|
|
355
|
+
status: "declared" as const,
|
|
356
|
+
};
|
|
357
|
+
});
|
|
358
|
+
const consumes = [
|
|
359
|
+
...new Map(mapped.map((call) => [call.id, call])).values(),
|
|
360
|
+
];
|
|
361
|
+
if (resolved.size > 0) resolvedByCaller.set(service.id, resolved);
|
|
362
|
+
return resolved.size > 0 ? { ...service, consumes } : service;
|
|
363
|
+
}),
|
|
364
|
+
}));
|
|
365
|
+
|
|
366
|
+
if (!changed) return input;
|
|
367
|
+
|
|
368
|
+
const flows = input.flows.map((flow) => {
|
|
369
|
+
let flowChanged = false;
|
|
370
|
+
const targets = new Map<string, HTTPProvider>();
|
|
371
|
+
const steps = mapSteps(flow.steps, (step) => {
|
|
372
|
+
if (step.kind !== "rpc" || !step.ref) return step;
|
|
373
|
+
const provider = resolvedByCaller.get(step.from)?.get(step.ref);
|
|
374
|
+
if (!provider) return step;
|
|
375
|
+
flowChanged = true;
|
|
376
|
+
targets.set(provider.service, provider);
|
|
377
|
+
return {
|
|
378
|
+
...step,
|
|
379
|
+
ref: provider.ref,
|
|
380
|
+
destination: evidence({ id: step.ref, source: step.line ?? "", destination: step.destination }, provider),
|
|
381
|
+
to: provider.service,
|
|
382
|
+
status: "declared",
|
|
383
|
+
};
|
|
384
|
+
});
|
|
385
|
+
if (!flowChanged) return flow;
|
|
386
|
+
|
|
387
|
+
const participants = [...flow.participants];
|
|
388
|
+
const seen = new Set(participants.map((participant) => participant.id));
|
|
389
|
+
for (const target of targets.values()) {
|
|
390
|
+
if (seen.has(target.service)) continue;
|
|
391
|
+
seen.add(target.service);
|
|
392
|
+
participants.push({
|
|
393
|
+
id: target.service,
|
|
394
|
+
kind: "service",
|
|
395
|
+
context: target.context,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
return { ...flow, participants, steps };
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
return { ...input, contexts, flows };
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function rawHTTPRoute(
|
|
405
|
+
id: string,
|
|
406
|
+
): { method: string; path: string } | undefined {
|
|
407
|
+
// The extractor qualifies a raw call with its destination after " @ "
|
|
408
|
+
// (`http-client/POST /foo @ payments.internal`) so that the same route to two
|
|
409
|
+
// hosts stays two calls; the route itself is what resolves against providers.
|
|
410
|
+
const match = /^http-client\/([A-Z]+)\s+(\/\S*)(?: @ .+)?$/.exec(id);
|
|
411
|
+
return match ? { method: match[1]!, path: match[2]! } : undefined;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* The host a call names, when it names one: the service-discovery alias the
|
|
416
|
+
* extractor recorded, the hostname of a literal base URL, or the destination
|
|
417
|
+
* after " @ " in the id when that is a host rather than an expression. A port
|
|
418
|
+
* is dropped, because a Service's name has none.
|
|
419
|
+
*/
|
|
420
|
+
function callHost(call: RpcCall): string | undefined {
|
|
421
|
+
const at = /^http-client\/[A-Z]+\s+\/\S*\s@\s(.+)$/.exec(call.id)?.[1];
|
|
422
|
+
for (const value of [
|
|
423
|
+
call.destination?.serviceDiscoveryAlias,
|
|
424
|
+
call.destination?.baseURL?.value,
|
|
425
|
+
at,
|
|
426
|
+
]) {
|
|
427
|
+
const host = hostOf(value);
|
|
428
|
+
if (host) return host;
|
|
429
|
+
}
|
|
430
|
+
return undefined;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function hostOf(value: string | undefined): string | undefined {
|
|
434
|
+
if (!value) return undefined;
|
|
435
|
+
const trimmed = value.trim();
|
|
436
|
+
if (trimmed.includes("://")) {
|
|
437
|
+
try {
|
|
438
|
+
return new URL(trimmed).hostname || undefined;
|
|
439
|
+
} catch {
|
|
440
|
+
return undefined;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
// Lower case by the cluster's own rule, so that `Config.SupplierURL` - an
|
|
444
|
+
// expression the extractor could not evaluate - is not taken for a host.
|
|
445
|
+
return /^([a-z0-9]([a-z0-9.-]*[a-z0-9])?)(:[0-9]+)?$/.exec(trimmed)?.[1];
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function uniqueHTTPProviders(providers: HTTPProvider[]): HTTPProvider[] {
|
|
449
|
+
const unique = new Map<string, HTTPProvider>();
|
|
450
|
+
for (const provider of providers) {
|
|
451
|
+
unique.set(`${provider.service}\u0000${provider.ref}`, provider);
|
|
452
|
+
}
|
|
453
|
+
return [...unique.values()];
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function sameHTTPPath(provided: string, called: string): boolean {
|
|
457
|
+
const provider = httpSegments(provided);
|
|
458
|
+
const call = httpSegments(called);
|
|
459
|
+
if (call.length === 0) return provider.length === 0;
|
|
460
|
+
if (call.length > provider.length) return false;
|
|
461
|
+
const offset = provider.length - call.length;
|
|
462
|
+
for (let index = 0; index < call.length; index += 1) {
|
|
463
|
+
if (!sameHTTPSegment(provider[offset + index]!, call[index]!)) return false;
|
|
464
|
+
}
|
|
465
|
+
return true;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function sameHTTPShape(left: string, right: string): boolean {
|
|
469
|
+
const a = httpSegments(left);
|
|
470
|
+
const b = httpSegments(right);
|
|
471
|
+
return (
|
|
472
|
+
a.length === b.length &&
|
|
473
|
+
a.every((segment, index) => sameHTTPSegment(segment, b[index]!))
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function httpSegments(path: string): string[] {
|
|
478
|
+
const withoutQuery = path.split("?", 1)[0] ?? path;
|
|
479
|
+
return withoutQuery.split("/").filter(Boolean);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function sameHTTPSegment(left: string, right: string): boolean {
|
|
483
|
+
return left === right || (isHTTPParameter(left) && isHTTPParameter(right));
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function isHTTPParameter(segment: string): boolean {
|
|
487
|
+
return (
|
|
488
|
+
segment === "%s" || /^\{[^{}]+\}$/.test(segment) || /^:[^:]+$/.test(segment)
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
|
|
231
492
|
/**
|
|
232
493
|
* Joins the use-case-side repository call emitted by extract-go to the
|
|
233
494
|
* concrete Redis client call emitted independently by extract-redis. Method
|
|
@@ -273,8 +534,10 @@ function resolveStoreAccesses(input: Catalog): Catalog {
|
|
|
273
534
|
) {
|
|
274
535
|
return node;
|
|
275
536
|
}
|
|
276
|
-
const method =
|
|
277
|
-
|
|
537
|
+
const method =
|
|
538
|
+
storeAccess.method.split(".").pop() ?? storeAccess.method;
|
|
539
|
+
const matches =
|
|
540
|
+
byMethod.get(`${storeAccess.store}\u0000${method}`) ?? [];
|
|
278
541
|
if (matches.length !== 1) return node;
|
|
279
542
|
changed = true;
|
|
280
543
|
return {
|
|
@@ -866,21 +1129,59 @@ function resolveWireNames(catalog: Catalog): Catalog {
|
|
|
866
1129
|
* keeps the raw name, because no extractor sees two schemas. The merge sees
|
|
867
1130
|
* all of them, and a name exactly one table in the estate answers to is that
|
|
868
1131
|
* table - which is what turns a dangling key into the crossing it is, on the
|
|
869
|
-
* page and on Problems.
|
|
1132
|
+
* page and on Problems.
|
|
1133
|
+
*
|
|
1134
|
+
* Two tables of that name are told apart by who the referencing service
|
|
1135
|
+
* talks to: its own store first, then the stores of the services it shares a
|
|
1136
|
+
* context with or calls, or that call it. A shop's delivery service keying
|
|
1137
|
+
* on `orders` means the order service it fetches orders from, not the
|
|
1138
|
+
* `orders` of an unrelated shop that happens to be in the same estate. A
|
|
1139
|
+
* name that is still ambiguous after that resolves to neither.
|
|
870
1140
|
*/
|
|
871
1141
|
function resolveForeignKeys(catalog: Catalog): Catalog {
|
|
872
1142
|
const stores = catalog.stores ?? [];
|
|
873
1143
|
if (stores.length === 0) return catalog;
|
|
874
1144
|
|
|
875
1145
|
const known = new Set<string>();
|
|
876
|
-
|
|
1146
|
+
// name → every table id answering to it, in store order.
|
|
1147
|
+
const byName = new Map<string, string[]>();
|
|
1148
|
+
const ownerOfTable = new Map<string, string>();
|
|
877
1149
|
for (const store of stores) {
|
|
878
1150
|
for (const table of store.tables) {
|
|
879
1151
|
known.add(table.id);
|
|
880
|
-
byName.set(table.name, byName.
|
|
1152
|
+
byName.set(table.name, [...(byName.get(table.name) ?? []), table.id]);
|
|
1153
|
+
ownerOfTable.set(table.id, store.owner);
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// Who a service is close to: the services of its context, the ones it
|
|
1158
|
+
// calls, and the ones that call it.
|
|
1159
|
+
const peers = new Map<string, Set<string>>();
|
|
1160
|
+
const near = (a: string, b: string) => {
|
|
1161
|
+
if (!peers.has(a)) peers.set(a, new Set());
|
|
1162
|
+
peers.get(a)!.add(b);
|
|
1163
|
+
};
|
|
1164
|
+
for (const ctx of catalog.contexts) {
|
|
1165
|
+
for (const svc of ctx.services) {
|
|
1166
|
+
for (const other of ctx.services) near(svc.id, other.id);
|
|
1167
|
+
for (const call of svc.consumes) {
|
|
1168
|
+
near(svc.id, call.peer);
|
|
1169
|
+
near(call.peer, svc.id);
|
|
1170
|
+
}
|
|
881
1171
|
}
|
|
882
1172
|
}
|
|
883
1173
|
|
|
1174
|
+
const resolveName = (name: string, from: string, owner: string): string | null => {
|
|
1175
|
+
const candidates = byName.get(name) ?? [];
|
|
1176
|
+
if (candidates.length === 0) return null;
|
|
1177
|
+
if (candidates.length === 1) return candidates[0]!;
|
|
1178
|
+
const own = candidates.filter((id) => id.startsWith(`${from}.`));
|
|
1179
|
+
if (own.length === 1) return own[0]!;
|
|
1180
|
+
const close = peers.get(owner) ?? new Set<string>();
|
|
1181
|
+
const nearby = candidates.filter((id) => close.has(ownerOfTable.get(id) ?? ""));
|
|
1182
|
+
return nearby.length === 1 ? nearby[0]! : null;
|
|
1183
|
+
};
|
|
1184
|
+
|
|
884
1185
|
let any = false;
|
|
885
1186
|
const resolved = stores.map((store) => {
|
|
886
1187
|
let touched = false;
|
|
@@ -888,7 +1189,7 @@ function resolveForeignKeys(catalog: Catalog): Catalog {
|
|
|
888
1189
|
let changed = false;
|
|
889
1190
|
const columns = table.columns.map((column) => {
|
|
890
1191
|
if (!column.fk || known.has(column.fk.table)) return column;
|
|
891
|
-
const found =
|
|
1192
|
+
const found = resolveName(column.fk.table, store.id, store.owner);
|
|
892
1193
|
if (!found) return column;
|
|
893
1194
|
changed = true;
|
|
894
1195
|
|