@shortlink-org/portolan 0.2.3 → 0.3.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 +23 -0
- package/catalog/enum_test.go +46 -0
- package/catalog/evidence_test.go +35 -0
- package/catalog/model.go +1066 -0
- package/catalog/roundtrip_test.go +203 -0
- package/catalog/via_test.go +38 -0
- package/cli/init.test.mjs +6 -1
- package/cli/portolan.mjs +8 -0
- package/cli/portolan.test.mjs +49 -0
- package/go.mod +14 -0
- package/go.sum +20 -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 +22 -10
- package/plugin/describe.go +118 -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 +114 -46
- package/plugins/cmd/portolan-http-clients/main.go +19 -0
- package/plugins/extract-celery/extract.py +0 -2
- package/plugins/extract-celery/extract_test.py +1 -1
- package/plugins/extract-django/README.md +39 -17
- package/plugins/extract-django/domain.py +28 -17
- package/plugins/extract-django/extract.py +21 -7
- package/plugins/extract-django/extract_test.py +55 -2
- package/plugins/extract-django/lifecycle.py +2 -0
- package/plugins/extract-django/operations.py +1 -1
- package/plugins/extract-django/routing_test.py +109 -1
- 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-http-clients/describe.go +19 -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/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-python-kafka/README.md +6 -0
- package/plugins/extract-python-kafka/extract.py +2 -4
- package/plugins/extract-python-kafka/extract_test.py +18 -2
- 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/openapi/ids.go +261 -0
- package/plugins/openapi/ids_test.go +98 -0
- package/plugins/portolan-go.wasm +0 -0
- package/plugins/pyplugin/catalog.py +12 -1
- package/plugins/pyplugin/kafka.py +74 -3
- package/plugins/pyplugin/protocol.py +1 -5
- package/portolan.json +3 -2
- package/schema/portolan.schema.json +34 -0
- package/scripts/README.md +18 -12
- package/scripts/catalog-sources.mjs +6 -0
- package/scripts/delivery-presets.mjs +21 -11
- package/scripts/diff.mjs +5 -1
- package/scripts/django-aggregates.test.mjs +58 -0
- package/scripts/gen-likec4.mjs +79 -21
- package/scripts/gen-likec4.test.mjs +25 -2
- package/scripts/gen.mjs +118 -115
- 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-git.mjs +77 -21
- package/scripts/host-plugins/fetch-git.test.mjs +62 -8
- package/scripts/local-api.mjs +71 -4
- package/scripts/local-api.test.mjs +63 -4
- package/scripts/local-discovery.mjs +82 -9
- package/scripts/manifest.mjs +5 -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 +22 -2
- package/scripts/plugin-host.test.mjs +9 -0
- package/scripts/plugin-wasm-worker.mjs +4 -1
- package/scripts/provenance.mjs +72 -0
- package/scripts/provenance.test.mjs +149 -0
- package/scripts/run-builtin.mjs +39 -5
- package/scripts/schema.mjs +29 -0
- package/scripts/warning-policy.mjs +161 -0
- package/scripts/warning-policy.test.mjs +56 -0
- package/src/app/Breadcrumbs.test.ts +4 -0
- package/src/app/Breadcrumbs.tsx +1 -0
- package/src/app/Sidebar.tsx +3 -3
- 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-model.ts +70 -6
- package/src/catalog-stores.test.ts +17 -0
- package/src/catalog-validation.ts +52 -2
- package/src/catalog.test.ts +13 -2
- 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.messagepack.test.tsx +28 -0
- package/src/components/ChannelRows.test.tsx +54 -0
- package/src/components/ChannelRows.tsx +57 -10
- 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/LifecycleDiagram.tsx +28 -12
- package/src/components/MachineDocs.tsx +6 -5
- package/src/components/MethodRows.tsx +9 -2
- package/src/components/ProblemRow.tsx +4 -0
- package/src/components/RelationEvidence.test.tsx +14 -0
- package/src/components/RelationEvidence.tsx +53 -0
- package/src/components/WhatLinksHere.tsx +6 -4
- package/src/data.ts +25 -7
- package/src/enrich.test.ts +336 -6
- package/src/enrich.ts +206 -3
- package/src/flow/StepDetail.tsx +104 -54
- package/src/flow/answers.test.ts +18 -1
- package/src/flow/answers.ts +37 -8
- package/src/flow/evidence.test.ts +16 -0
- package/src/flow/evidence.ts +34 -0
- package/src/index.css +44 -0
- package/src/landing/DraggableReveal.tsx +3 -2
- package/src/landing/EvidencePipeline.tsx +105 -0
- package/src/landing/LandingPage.tsx +2 -59
- package/src/lib/backlinks.test.ts +16 -1
- package/src/lib/backlinks.ts +20 -0
- package/src/lib/catalog-diff.test.ts +18 -0
- package/src/lib/catalog-diff.ts +20 -2
- package/src/lib/derive.ts +1 -0
- 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/kafka-ui.test.ts +87 -0
- package/src/lib/kafka-ui.ts +105 -0
- package/src/lib/local-api.ts +20 -2
- package/src/lib/setup-info.test.ts +17 -0
- package/src/lib/setup-info.ts +58 -0
- package/src/lib/warnings.test.ts +54 -0
- package/src/lib/warnings.ts +260 -0
- package/src/lib/wire-problems.test.ts +21 -0
- package/src/lib/wire-problems.ts +62 -1
- package/src/likec4/FlowView.tsx +2 -6
- package/src/likec4/flow-edges.test.ts +64 -1
- package/src/likec4/flow-edges.ts +43 -7
- package/src/likec4/view-index.ts +8 -2
- package/src/map/ContextMapGraph.tsx +76 -32
- package/src/merge.test.ts +23 -0
- package/src/merge.ts +33 -10
- package/src/pages/AggregatePage.tsx +8 -7
- package/src/pages/CatalogFailure.tsx +2 -2
- package/src/pages/ContextPage.tsx +6 -5
- package/src/pages/ServicePage.tsx +4 -3
- package/src/pages/Settings.tsx +200 -44
- package/src/pages/settings/DjangoAggregateChoices.tsx +79 -0
- package/src/pages/settings/IntegrationsSettings.tsx +117 -0
- package/src/routes.test.ts +2 -0
- package/src/routes.ts +2 -1
- package/src/selection/DetailPanel.tsx +61 -1
- 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
|
@@ -16,7 +16,14 @@
|
|
|
16
16
|
// status among everything the line carries: a relationship is known no better
|
|
17
17
|
// than its least-known hop.
|
|
18
18
|
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
useCallback,
|
|
21
|
+
useEffect,
|
|
22
|
+
useLayoutEffect,
|
|
23
|
+
useMemo,
|
|
24
|
+
useRef,
|
|
25
|
+
useState,
|
|
26
|
+
} from "react";
|
|
20
27
|
import { useNavigate } from "react-router";
|
|
21
28
|
import {
|
|
22
29
|
Background,
|
|
@@ -79,6 +86,36 @@ export interface ContextMapGraphProps {
|
|
|
79
86
|
inactiveNodeOpacity?: number;
|
|
80
87
|
}
|
|
81
88
|
|
|
89
|
+
// React Flow reads its parent box during mount. The page shell and embedded
|
|
90
|
+
// hero can both briefly be 0×0 while their layout settles, so keep the flow
|
|
91
|
+
// out of that box until a real size has been observed.
|
|
92
|
+
function useMeasuredCanvas() {
|
|
93
|
+
const canvasRef = useRef<HTMLDivElement>(null);
|
|
94
|
+
const [hasSize, setHasSize] = useState(false);
|
|
95
|
+
|
|
96
|
+
useLayoutEffect(() => {
|
|
97
|
+
const canvas = canvasRef.current;
|
|
98
|
+
if (!canvas) return;
|
|
99
|
+
|
|
100
|
+
const measure = (): void => {
|
|
101
|
+
const next = canvas.clientWidth > 0 && canvas.clientHeight > 0;
|
|
102
|
+
setHasSize((current) => (current === next ? current : next));
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
measure();
|
|
106
|
+
if (typeof ResizeObserver === "undefined") {
|
|
107
|
+
window.addEventListener("resize", measure);
|
|
108
|
+
return () => window.removeEventListener("resize", measure);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const observer = new ResizeObserver(measure);
|
|
112
|
+
observer.observe(canvas);
|
|
113
|
+
return () => observer.disconnect();
|
|
114
|
+
}, []);
|
|
115
|
+
|
|
116
|
+
return { canvasRef, hasSize };
|
|
117
|
+
}
|
|
118
|
+
|
|
82
119
|
export function ContextMapGraph({
|
|
83
120
|
catalog,
|
|
84
121
|
relations,
|
|
@@ -97,6 +134,7 @@ export function ContextMapGraph({
|
|
|
97
134
|
edges: [],
|
|
98
135
|
ready: false,
|
|
99
136
|
});
|
|
137
|
+
const { canvasRef, hasSize } = useMeasuredCanvas();
|
|
100
138
|
|
|
101
139
|
// One entry per pair the catalog has anything to say about. `back` is what
|
|
102
140
|
// turns an arrow into a double-headed one; elk only ever sees a -> b, so the
|
|
@@ -257,37 +295,43 @@ export function ContextMapGraph({
|
|
|
257
295
|
);
|
|
258
296
|
|
|
259
297
|
return (
|
|
260
|
-
<div
|
|
261
|
-
{
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
298
|
+
<div
|
|
299
|
+
ref={canvasRef}
|
|
300
|
+
className="canvas-motion relative h-full w-full"
|
|
301
|
+
>
|
|
302
|
+
{drawn.ready && hasSize ? (
|
|
303
|
+
<ReactFlow
|
|
304
|
+
nodes={shownNodes}
|
|
305
|
+
edges={shownEdges}
|
|
306
|
+
nodeTypes={contextNodeTypes}
|
|
307
|
+
edgeTypes={mapEdgeTypes}
|
|
308
|
+
onNodeClick={onNodeClick}
|
|
309
|
+
onNodeDoubleClick={onNodeDoubleClick}
|
|
310
|
+
onPaneClick={() => clear("diagram")}
|
|
311
|
+
nodesDraggable={false}
|
|
312
|
+
nodesConnectable={false}
|
|
313
|
+
elementsSelectable
|
|
314
|
+
proOptions={{ hideAttribution: true }}
|
|
315
|
+
fitView
|
|
316
|
+
fitViewOptions={{ padding: compactLayout ? 0.06 : 0.16 }}
|
|
317
|
+
zoomOnScroll={zoomOnScroll}
|
|
318
|
+
preventScrolling={zoomOnScroll}
|
|
319
|
+
minZoom={0.2}
|
|
320
|
+
maxZoom={1.6}
|
|
321
|
+
key={fitKey}
|
|
322
|
+
>
|
|
323
|
+
<Background gap={20} size={2} />
|
|
324
|
+
{/* The map has no modes to switch, so its only control is the way
|
|
325
|
+
out: the drawing as a file, for the page that explains it. */}
|
|
326
|
+
{showExport ? (
|
|
327
|
+
<Panel position="top-right">
|
|
328
|
+
<ExportSeg name="context-map" />
|
|
329
|
+
</Panel>
|
|
330
|
+
) : null}
|
|
331
|
+
</ReactFlow>
|
|
332
|
+
) : (
|
|
333
|
+
<DiagramSkeleton />
|
|
334
|
+
)}
|
|
291
335
|
</div>
|
|
292
336
|
);
|
|
293
337
|
}
|
package/src/merge.test.ts
CHANGED
|
@@ -757,6 +757,29 @@ describe("mergeCatalogs: schema modules", () => {
|
|
|
757
757
|
// came from.
|
|
758
758
|
expect(channels?.[0]?.source).toBe("bus/asyncapi.yaml");
|
|
759
759
|
});
|
|
760
|
+
|
|
761
|
+
it("keeps a message encoding and reports sources that disagree about it", () => {
|
|
762
|
+
const first = context("shop", ["shop.cart"]);
|
|
763
|
+
first.services[0]!.channels = [{
|
|
764
|
+
address: "shop.cart.basket",
|
|
765
|
+
messages: [{ name: "cart.BasketCreated", direction: "send", encoding: "msgpack", contentType: "application/msgpack" }],
|
|
766
|
+
}];
|
|
767
|
+
const second = context("shop", ["shop.cart"]);
|
|
768
|
+
second.services[0]!.channels = [{
|
|
769
|
+
address: "shop.cart.basket",
|
|
770
|
+
messages: [{ name: "cart.BasketCreated", direction: "send", encoding: "json", contentType: "application/json" }],
|
|
771
|
+
}];
|
|
772
|
+
|
|
773
|
+
const merged = mergeCatalogs([
|
|
774
|
+
source("a-msgpack.json", { contexts: [first] }),
|
|
775
|
+
source("b-json.json", { contexts: [second] }),
|
|
776
|
+
]);
|
|
777
|
+
const message = merged.catalog.contexts[0]!.services[0]!.channels![0]!.messages[0]!;
|
|
778
|
+
expect(message.encoding).toBe("msgpack");
|
|
779
|
+
expect(message.contentType).toBe("application/msgpack");
|
|
780
|
+
expect(merged.conflicts.map((conflict) => conflict.message).join(" ")).toMatch(/encoding json.*msgpack/);
|
|
781
|
+
expect(merged.conflicts.map((conflict) => conflict.message).join(" ")).toMatch(/contentType application\/json.*application\/msgpack/);
|
|
782
|
+
});
|
|
760
783
|
});
|
|
761
784
|
|
|
762
785
|
describe("a second source that has seen the flow run", () => {
|
package/src/merge.ts
CHANGED
|
@@ -42,13 +42,14 @@ import type {
|
|
|
42
42
|
* A catalog as a FILE may carry it, which is not quite a catalog as a reader
|
|
43
43
|
* gets one: the two stamps are optional here and required downstream.
|
|
44
44
|
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
45
|
+
* A fragment carries no stamp of its own (portolan.0010). When it last
|
|
46
|
+
* changed, and in which commit, is what the history of the checkout says,
|
|
47
|
+
* and whoever reads the file asks the history and hands the answer over
|
|
48
|
+
* beside it. A file may still say something - a fragment written before
|
|
49
|
+
* portolan.0010 does - and that is taken only when nobody read the history.
|
|
50
|
+
* A provenance typed in by hand is a claim no process keeps true, so a source
|
|
51
|
+
* that says nothing and was read outside any history is stamped by nobody,
|
|
52
|
+
* and saying nothing is treated as what it is - an absence of evidence, which
|
|
52
53
|
* drops out of the merge rather than voting in it.
|
|
53
54
|
*/
|
|
54
55
|
export type SourceCatalog = Omit<Catalog, "generatedAt" | "commit"> &
|
|
@@ -59,6 +60,12 @@ export interface CatalogSource {
|
|
|
59
60
|
/** Where it was read from, as a reader would type it. */
|
|
60
61
|
path: string;
|
|
61
62
|
catalog: SourceCatalog;
|
|
63
|
+
/**
|
|
64
|
+
* When the file last changed and in which commit, as the reader found out
|
|
65
|
+
* from the history of the checkout (portolan.0010). Left out when there
|
|
66
|
+
* was no history to ask, and the file then speaks for itself.
|
|
67
|
+
*/
|
|
68
|
+
stamp?: Pick<SourceStamp, "generatedAt" | "commit">;
|
|
62
69
|
}
|
|
63
70
|
|
|
64
71
|
/**
|
|
@@ -120,8 +127,8 @@ export function mergeCatalogs(sources: CatalogSource[]): MergeResult {
|
|
|
120
127
|
|
|
121
128
|
const stamps: SourceStamp[] = ordered.map((source) => ({
|
|
122
129
|
path: source.path,
|
|
123
|
-
generatedAt: source.catalog.generatedAt ?? "",
|
|
124
|
-
commit: source.catalog.commit ?? "",
|
|
130
|
+
generatedAt: source.stamp?.generatedAt ?? source.catalog.generatedAt ?? "",
|
|
131
|
+
commit: source.stamp?.commit ?? source.catalog.commit ?? "",
|
|
125
132
|
}));
|
|
126
133
|
|
|
127
134
|
for (const { path, catalog } of ordered) {
|
|
@@ -442,6 +449,7 @@ function foldMaps(catalog: Catalog, stores: Store[]): Store[] {
|
|
|
442
449
|
if (!aggregate) return table;
|
|
443
450
|
return {
|
|
444
451
|
...table,
|
|
452
|
+
...(table.accesses ? { accesses: table.accesses.map((access) => ({ ...access })) } : {}),
|
|
445
453
|
columns: table.columns.map((column) => {
|
|
446
454
|
if (!column.maps) return column;
|
|
447
455
|
const maps = fold(aggregate, column.maps);
|
|
@@ -608,6 +616,21 @@ function mergeService(
|
|
|
608
616
|
mine.messages,
|
|
609
617
|
theirs.messages,
|
|
610
618
|
(m) => `${m.direction} ${m.name}`,
|
|
619
|
+
(held, offered) => {
|
|
620
|
+
for (const field of ["encoding", "contentType"] as const) {
|
|
621
|
+
if (!held[field] && offered[field]) {
|
|
622
|
+
held[field] = offered[field];
|
|
623
|
+
} else if (held[field] && offered[field] && held[field] !== offered[field]) {
|
|
624
|
+
conflicts.push({
|
|
625
|
+
path,
|
|
626
|
+
where: incoming.id,
|
|
627
|
+
message: `message "${held.name}" on channel "${mine.address}" of service "${incoming.id}" has ${field} ${offered[field]} here and ${held[field]} in ${owner}; the first one is used`,
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
return undefined;
|
|
633
|
+
},
|
|
611
634
|
);
|
|
612
635
|
|
|
613
636
|
// Mutated in place: the channel that was here keeps its prose and its
|
|
@@ -620,7 +643,7 @@ function mergeService(
|
|
|
620
643
|
}
|
|
621
644
|
|
|
622
645
|
function copyChannel(channel: Channel): Channel {
|
|
623
|
-
return { ...channel, messages:
|
|
646
|
+
return { ...channel, messages: channel.messages.map((message) => ({ ...message })) };
|
|
624
647
|
}
|
|
625
648
|
|
|
626
649
|
/**
|
|
@@ -77,14 +77,14 @@ function BuildingBlocks({
|
|
|
77
77
|
</div>
|
|
78
78
|
<div className="flex flex-wrap items-center gap-x-4 gap-y-2 px-4 py-3">
|
|
79
79
|
<span className="mono flex items-center gap-1.5 text-muted">
|
|
80
|
-
root
|
|
80
|
+
{aggregate.kind === "model-group" ? "aggregate boundary" : "root"}
|
|
81
81
|
<KindIcon kind="entity" />
|
|
82
82
|
{rootTo ? (
|
|
83
83
|
<Link to={rootTo} className="text-ink hover:underline">
|
|
84
84
|
{aggregate.root}
|
|
85
85
|
</Link>
|
|
86
86
|
) : (
|
|
87
|
-
<span className="text-ink">{aggregate.root}</span>
|
|
87
|
+
<span className="text-ink">{aggregate.kind === "model-group" ? "not specified" : aggregate.root}</span>
|
|
88
88
|
)}
|
|
89
89
|
</span>
|
|
90
90
|
<span aria-hidden className="h-4 w-px bg-line-strong" />
|
|
@@ -385,7 +385,7 @@ export function AggregatePage() {
|
|
|
385
385
|
<PageHeader
|
|
386
386
|
kind={
|
|
387
387
|
<>
|
|
388
|
-
aggregate ·{" "}
|
|
388
|
+
{aggregate.kind === "model-group" ? "model group" : "aggregate"} ·{" "}
|
|
389
389
|
<Link
|
|
390
390
|
to={paths.service(context.id, service.slug)}
|
|
391
391
|
className="rounded-control hover:text-ink hover:underline"
|
|
@@ -410,6 +410,7 @@ export function AggregatePage() {
|
|
|
410
410
|
/>
|
|
411
411
|
|
|
412
412
|
<Markdown mermaid>{aggregate.readme}</Markdown>
|
|
413
|
+
{aggregate.kind === "model-group" ? <p className="mb-section text-muted">Models discovered in this application. Their relationships are extracted from the source; aggregate boundaries have not been specified.</p> : null}
|
|
413
414
|
|
|
414
415
|
<div
|
|
415
416
|
className="mt-section max-w-prose"
|
|
@@ -583,14 +584,14 @@ export function AggregatePage() {
|
|
|
583
584
|
anchor={AGGREGATE_ANCHOR.commands}
|
|
584
585
|
right={
|
|
585
586
|
<span>
|
|
586
|
-
they change the aggregate — one row lock each
|
|
587
|
+
{aggregate.kind === "model-group" ? "write operations discovered in this application" : "they change the aggregate — one row lock each"}
|
|
587
588
|
</span>
|
|
588
589
|
}
|
|
589
590
|
>
|
|
590
591
|
Commands
|
|
591
592
|
</SectionTitle>
|
|
592
593
|
{commands.length === 0 ? (
|
|
593
|
-
<Empty>nothing changes this aggregate from outside</Empty>
|
|
594
|
+
<Empty>{aggregate.kind === "model-group" ? "no application commands discovered" : "nothing changes this aggregate from outside"}</Empty>
|
|
594
595
|
) : null}
|
|
595
596
|
<OperationList kind="command" operations={commands} service={service} />
|
|
596
597
|
</div>
|
|
@@ -606,7 +607,7 @@ export function AggregatePage() {
|
|
|
606
607
|
Queries
|
|
607
608
|
</SectionTitle>
|
|
608
609
|
{queries.length === 0 ? (
|
|
609
|
-
<Empty>nothing reads this aggregate by name</Empty>
|
|
610
|
+
<Empty>{aggregate.kind === "model-group" ? "no application queries discovered" : "nothing reads this aggregate by name"}</Empty>
|
|
610
611
|
) : null}
|
|
611
612
|
<OperationList kind="query" operations={queries} service={service} />
|
|
612
613
|
</div>
|
|
@@ -750,7 +751,7 @@ export function AggregatePage() {
|
|
|
750
751
|
/>
|
|
751
752
|
</div>
|
|
752
753
|
|
|
753
|
-
<Toc items={toc} label="Sections of this aggregate" title="Outline" />
|
|
754
|
+
<Toc items={toc} label={aggregate.kind === "model-group" ? "Sections of this model group" : "Sections of this aggregate"} title="Outline" />
|
|
754
755
|
</div>
|
|
755
756
|
</div>
|
|
756
757
|
);
|
|
@@ -26,8 +26,8 @@ const HINTS: { test: RegExp; hint: string }[] = [
|
|
|
26
26
|
hint: "A step names a lane the flow never declared. Add the service to the flow's `participants`, or point the step at one that is already there.",
|
|
27
27
|
},
|
|
28
28
|
{
|
|
29
|
-
test: /resolves to neither an Event nor
|
|
30
|
-
hint: "Either the ref is stale — the event was renamed or removed — or the step really does point at something outside the catalog, in which case its status belongs as `unresolved`.",
|
|
29
|
+
test: /resolves to neither an Event, an RpcCall nor a method/,
|
|
30
|
+
hint: "Either the ref is stale — the event or RPC method was renamed or removed — or the step really does point at something outside the catalog, in which case its status belongs as `unresolved`.",
|
|
31
31
|
},
|
|
32
32
|
{
|
|
33
33
|
test: /must have id/,
|
|
@@ -37,6 +37,7 @@ export function ContextPage() {
|
|
|
37
37
|
);
|
|
38
38
|
const neutral = groupKind(context) !== "bounded-context";
|
|
39
39
|
const showDomain = !neutral || aggregates.length > 0;
|
|
40
|
+
const hasModelGroups = aggregates.some(({ aggregate }) => aggregate.kind === "model-group");
|
|
40
41
|
const toc: TocItem[] = [
|
|
41
42
|
{ id: CONTEXT_ANCHOR.services, label: neutral ? "Components" : "Services" },
|
|
42
43
|
...(showDomain
|
|
@@ -86,7 +87,7 @@ export function ContextPage() {
|
|
|
86
87
|
{showDomain ? (
|
|
87
88
|
<>
|
|
88
89
|
<a href={`#${CONTEXT_ANCHOR.aggregates}`} className="rounded-control hover:text-ink">
|
|
89
|
-
<span className="tnum">{stats.aggregates}</span>{" "}{plural(stats.aggregates, "aggregate")}
|
|
90
|
+
<span className="tnum">{stats.aggregates}</span>{" "}{plural(stats.aggregates, hasModelGroups ? "group" : "aggregate")}
|
|
90
91
|
</a>
|
|
91
92
|
<a href={`#${CONTEXT_ANCHOR.events}`} className="rounded-control hover:text-ink">
|
|
92
93
|
<span className="tnum">{stats.events}</span>{" "}{plural(stats.events, "event")}
|
|
@@ -173,7 +174,7 @@ export function ContextPage() {
|
|
|
173
174
|
<div className="mono mt-4 flex flex-wrap gap-x-4 text-muted">
|
|
174
175
|
{service.aggregates.length > 0 || componentKind(service) === "service" ? (
|
|
175
176
|
<Link to={`${paths.service(context.id, service.slug)}#svc-aggregates`} className="rounded-control hover:text-ink">
|
|
176
|
-
<span className="tnum">{service.aggregates.length}</span>{" "}{plural(service.aggregates.length, "aggregate")}
|
|
177
|
+
<span className="tnum">{service.aggregates.length}</span>{" "}{plural(service.aggregates.length, service.aggregates.some((aggregate) => aggregate.kind === "model-group") ? "group" : "aggregate")}
|
|
177
178
|
</Link>
|
|
178
179
|
) : null}
|
|
179
180
|
<Link
|
|
@@ -234,11 +235,11 @@ export function ContextPage() {
|
|
|
234
235
|
anchor={CONTEXT_ANCHOR.aggregates}
|
|
235
236
|
right={
|
|
236
237
|
<span>
|
|
237
|
-
every aggregate this domain owns, whichever service holds it
|
|
238
|
+
{hasModelGroups ? "models grouped by their source application or aggregate" : "every aggregate this domain owns, whichever service holds it"}
|
|
238
239
|
</span>
|
|
239
240
|
}
|
|
240
241
|
>
|
|
241
|
-
Aggregates
|
|
242
|
+
{hasModelGroups ? "Aggregates and model groups" : "Aggregates"}
|
|
242
243
|
</SectionTitle>
|
|
243
244
|
{aggregates.length === 0 ? (
|
|
244
245
|
<Empty>this domain owns nothing yet — only services</Empty>
|
|
@@ -264,7 +265,7 @@ export function ContextPage() {
|
|
|
264
265
|
{aggregate.name}
|
|
265
266
|
</Link>
|
|
266
267
|
<span className="meta">{service.slug}</span>
|
|
267
|
-
<span className="meta">root {aggregate.root}</span>
|
|
268
|
+
<span className="meta">{aggregate.kind === "model-group" ? "model group · boundary not specified" : `root ${aggregate.root}`}</span>
|
|
268
269
|
<RowActions
|
|
269
270
|
copy={aggregate.id}
|
|
270
271
|
reveal={aggregate.id}
|
|
@@ -130,6 +130,7 @@ export function ServicePage() {
|
|
|
130
130
|
.find((source) => source !== undefined && hasAsyncSpec(source));
|
|
131
131
|
const showDomain =
|
|
132
132
|
componentKind(service) === "service" || service.aggregates.length > 0;
|
|
133
|
+
const hasModelGroups = service.aggregates.some((aggregate) => aggregate.kind === "model-group");
|
|
133
134
|
const integrations = integrationsFor(service, catalog);
|
|
134
135
|
|
|
135
136
|
const counts: Record<Tab, number | null> = {
|
|
@@ -219,7 +220,7 @@ export function ServicePage() {
|
|
|
219
220
|
className="rounded-control hover:text-ink"
|
|
220
221
|
>
|
|
221
222
|
<span className="tnum">{service.aggregates.length}</span>{" "}
|
|
222
|
-
{plural(service.aggregates.length, "aggregate")}
|
|
223
|
+
{plural(service.aggregates.length, hasModelGroups ? "group" : "aggregate")}
|
|
223
224
|
</a>
|
|
224
225
|
) : null}
|
|
225
226
|
{showDomain ? (
|
|
@@ -312,7 +313,7 @@ export function ServicePage() {
|
|
|
312
313
|
className="mt-section max-w-table"
|
|
313
314
|
>
|
|
314
315
|
<SectionTitle anchor={SERVICE_ANCHOR.aggregates}>
|
|
315
|
-
Aggregates
|
|
316
|
+
{hasModelGroups ? "Aggregates and model groups" : "Aggregates"}
|
|
316
317
|
</SectionTitle>
|
|
317
318
|
{/* icon, slug, name, event count, actions - one column each,
|
|
318
319
|
so the counts stack up instead of drifting with the name. */}
|
|
@@ -336,7 +337,7 @@ export function ServicePage() {
|
|
|
336
337
|
>
|
|
337
338
|
{aggregate.slug}
|
|
338
339
|
</Link>
|
|
339
|
-
<span className="meta">{aggregate.name}</span>
|
|
340
|
+
<span className="meta">{aggregate.name}{aggregate.kind === "model-group" ? " · model group" : ""}</span>
|
|
340
341
|
<Link
|
|
341
342
|
to={`${to}#bb-events`}
|
|
342
343
|
className="mono rounded-control hover:underline"
|