@shortlink-org/portolan 0.3.0 → 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 +151 -6
- package/catalog/model.go +147 -0
- package/catalog/roundtrip_test.go +7 -0
- package/cli/portolan.mjs +6 -1
- package/go.mod +7 -0
- package/go.sum +20 -6
- package/package.json +10 -9
- package/plugin/describe.go +38 -0
- package/plugins/README.md +296 -5
- package/plugins/extract-argocd/options.schema.json +44 -0
- 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 +13 -2
- package/plugins/extract-django/choices.py +47 -0
- package/plugins/extract-django/domain.py +2 -1
- package/plugins/extract-django/extract_test.py +13 -0
- package/plugins/extract-django/lifecycle.py +2 -28
- package/plugins/extract-django/main.py +1 -0
- package/plugins/extract-django/rules.py +154 -0
- package/plugins/extract-django/rules_test.py +158 -0
- package/plugins/extract-go-sqs/options.schema.json +21 -0
- package/plugins/extract-http-clients/describe.go +5 -4
- package/plugins/extract-java/build/org/portolan/extract/Main.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/main.py +1 -0
- package/plugins/extract-terraform/options.schema.json +26 -0
- package/plugins/extract-ts/main.ts +1 -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/verify-otel/options.schema.json +12 -0
- package/portolan.json +130 -3
- package/rules/builtin.json +218 -0
- package/schema/portolan.schema.json +880 -13
- package/scripts/README.md +3 -1
- package/scripts/delivery-presets.mjs +36 -3
- package/scripts/gen-likec4.mjs +149 -16
- package/scripts/gen-likec4.test.mjs +96 -0
- package/scripts/gen.mjs +30 -3
- package/scripts/gitops-example.test.mjs +108 -0
- 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 +1 -0
- 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 +216 -8
- package/scripts/local-api.test.mjs +88 -2
- package/scripts/local-discovery.mjs +12 -0
- package/scripts/manifest.mjs +10 -1
- package/scripts/plugin-host.mjs +23 -1
- package/scripts/plugins-fresh.mjs +97 -0
- package/scripts/plugins-fresh.test.mjs +64 -0
- package/scripts/run-builtin.mjs +26 -5
- package/scripts/schema.mjs +138 -19
- package/scripts/trace-trials.mjs +176 -0
- package/scripts/trace-trials.test.mjs +142 -0
- package/scripts/warning-policy.mjs +12 -6
- package/scripts/warning-policy.test.mjs +40 -3
- 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/SidebarFooter.tsx +20 -4
- package/src/catalog-index.ts +25 -0
- package/src/catalog-model.ts +246 -1
- package/src/catalog-validation.ts +104 -2
- package/src/catalog.test.ts +40 -1
- 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.tsx +1 -1
- package/src/components/PluginIcon.tsx +77 -0
- package/src/components/ProblemRow.tsx +173 -153
- package/src/components/RuleMarks.tsx +22 -0
- package/src/components/ShapeRows.tsx +24 -20
- package/src/data.ts +21 -0
- package/src/enrich.test.ts +127 -3
- package/src/enrich.ts +104 -6
- 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 +53 -0
- package/src/flow/TraceTrial.tsx +419 -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 +24 -0
- package/src/landing/LandingPage.tsx +15 -11
- package/src/landing/ProductTour.tsx +6 -6
- package/src/lib/all-problems.ts +27 -17
- 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/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 +96 -2
- 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/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 +9 -0
- package/src/lib/warnings.ts +10 -10
- package/src/likec4/ids.test.ts +6 -2
- package/src/likec4/ids.ts +43 -0
- package/src/main.tsx +23 -0
- package/src/merge-deployments.test.ts +127 -0
- package/src/merge.test.ts +82 -0
- package/src/merge.ts +173 -9
- package/src/pages/AggregatePage.tsx +57 -7
- package/src/pages/ContextMap.tsx +45 -3
- package/src/pages/ContextPage.tsx +2 -0
- 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 +58 -2
- package/src/pages/Settings.tsx +28 -2
- package/src/pages/settings/AboutSettings.tsx +8 -1
- 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/lib/data-problems.ts +0 -314
- package/src/lib/proto-problems.ts +0 -237
- package/src/lib/wire-problems.ts +0 -342
|
@@ -0,0 +1,737 @@
|
|
|
1
|
+
// What a rule sees: one row per subject, flat and typed.
|
|
2
|
+
//
|
|
3
|
+
// A rule is a predicate over one row, and a row cannot see the row next to
|
|
4
|
+
// it. So everything a rule would have to look up - who else writes this
|
|
5
|
+
// table, who publishes on this channel, whether the peer of a call is in the
|
|
6
|
+
// estate, what the provider's copy of an interface says - is looked up here,
|
|
7
|
+
// once, and written onto the row as a field. The joins are code; the
|
|
8
|
+
// judgement is CEL. That split is the whole design (portolan.0017): a
|
|
9
|
+
// projection says what is, and never whether it is wrong.
|
|
10
|
+
//
|
|
11
|
+
// Every field is a string, an int, a bool or a list of strings, so that the
|
|
12
|
+
// same row reads the same under Node and in the browser, and so the schema
|
|
13
|
+
// in problem-rules-cel.mjs can say exactly what an expression may name.
|
|
14
|
+
|
|
15
|
+
import type {
|
|
16
|
+
Aggregate,
|
|
17
|
+
Catalog,
|
|
18
|
+
CatalogIndex,
|
|
19
|
+
Channel,
|
|
20
|
+
ChannelMessage,
|
|
21
|
+
Column,
|
|
22
|
+
Deployment,
|
|
23
|
+
Event,
|
|
24
|
+
Flow,
|
|
25
|
+
RpcEnum,
|
|
26
|
+
RpcMessage,
|
|
27
|
+
RpcMethod,
|
|
28
|
+
RpcService,
|
|
29
|
+
Service,
|
|
30
|
+
Store,
|
|
31
|
+
Table,
|
|
32
|
+
View,
|
|
33
|
+
} from "../catalog";
|
|
34
|
+
import {
|
|
35
|
+
aggregateBlocks,
|
|
36
|
+
allDeployments,
|
|
37
|
+
blockFields,
|
|
38
|
+
deploys,
|
|
39
|
+
enumsOf,
|
|
40
|
+
mapsFieldPath,
|
|
41
|
+
relationOfColumnId,
|
|
42
|
+
storeViews,
|
|
43
|
+
viewReads,
|
|
44
|
+
walkSteps,
|
|
45
|
+
} from "../catalog";
|
|
46
|
+
import { payloadColumn, typesDisagree } from "./data-model";
|
|
47
|
+
import { driftLines } from "./deployment-drift";
|
|
48
|
+
import type { RuleSubject } from "./problem-rules-cel.mjs";
|
|
49
|
+
|
|
50
|
+
/** One row a rule is asked about, with what a problem needs to know of it. */
|
|
51
|
+
export interface Subject {
|
|
52
|
+
/** What the row is shown as, and where its near end links. */
|
|
53
|
+
id: string;
|
|
54
|
+
context: string;
|
|
55
|
+
service: string;
|
|
56
|
+
source: string | undefined;
|
|
57
|
+
/** What the expression sees, under the subject's name. */
|
|
58
|
+
row: Record<string, unknown>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const int = (n: number) => BigInt(n);
|
|
62
|
+
const unique = (values: string[]) => [...new Set(values)];
|
|
63
|
+
|
|
64
|
+
function contextOf(index: CatalogIndex, service: Service | string | undefined): string {
|
|
65
|
+
const id = typeof service === "string" ? service : service?.id;
|
|
66
|
+
return id ? index.serviceContext.get(id)?.id ?? "" : "";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Every service with the context it sits in, in catalog order. */
|
|
70
|
+
function servicesOf(catalog: Catalog): { context: string; service: Service }[] {
|
|
71
|
+
return catalog.contexts.flatMap((context) => context.services.map((service) => ({ context: context.id, service })));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Every row of one subject kind in the catalog, in catalog order. */
|
|
75
|
+
export function subjectsOf(catalog: Catalog, index: CatalogIndex, over: RuleSubject): Subject[] {
|
|
76
|
+
switch (over) {
|
|
77
|
+
case "service":
|
|
78
|
+
return serviceSubjects(catalog);
|
|
79
|
+
case "event":
|
|
80
|
+
return eventSubjects(catalog);
|
|
81
|
+
case "consumer":
|
|
82
|
+
return consumerSubjects(catalog);
|
|
83
|
+
case "channel":
|
|
84
|
+
return channelSubjects(catalog);
|
|
85
|
+
case "subscription":
|
|
86
|
+
return subscriptionSubjects(catalog, index);
|
|
87
|
+
case "table":
|
|
88
|
+
return tableSubjects(catalog, index);
|
|
89
|
+
case "column":
|
|
90
|
+
return columnSubjects(catalog, index);
|
|
91
|
+
case "deployment":
|
|
92
|
+
return deploymentSubjects(catalog);
|
|
93
|
+
case "flow":
|
|
94
|
+
return catalog.flows.map((flow) => flowSubject(catalog, flow));
|
|
95
|
+
case "aggregate":
|
|
96
|
+
return servicesOf(catalog).flatMap(({ service }) => service.aggregates.map((aggregate) => aggregateSubject(catalog, index, aggregate)));
|
|
97
|
+
case "call":
|
|
98
|
+
return callSubjects(catalog, index);
|
|
99
|
+
case "copy":
|
|
100
|
+
return copySubjects(catalog, index);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The names the estate answers to, for `x in estate.services`. */
|
|
105
|
+
export function estateOf(catalog: Catalog): Record<string, string[]> {
|
|
106
|
+
const services = catalog.contexts.flatMap((context) => context.services);
|
|
107
|
+
return {
|
|
108
|
+
services: services.map((service) => service.id),
|
|
109
|
+
contexts: catalog.contexts.map((context) => context.id),
|
|
110
|
+
stores: (catalog.stores ?? []).map((store) => store.id),
|
|
111
|
+
channels: unique(services.flatMap((service) => (service.channels ?? []).map((channel) => channel.address))),
|
|
112
|
+
externals: (catalog.externals ?? []).map((external) => external.id),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// Services and calls.
|
|
118
|
+
|
|
119
|
+
function serviceSubjects(catalog: Catalog): Subject[] {
|
|
120
|
+
return servicesOf(catalog).map(({ context, service }) => ({
|
|
121
|
+
id: service.id,
|
|
122
|
+
context,
|
|
123
|
+
service: service.id,
|
|
124
|
+
source: undefined,
|
|
125
|
+
row: {
|
|
126
|
+
id: service.id,
|
|
127
|
+
slug: service.slug,
|
|
128
|
+
name: service.name,
|
|
129
|
+
context,
|
|
130
|
+
kind: service.kind ?? "service",
|
|
131
|
+
repo: service.repo,
|
|
132
|
+
path: service.path,
|
|
133
|
+
technologies: service.technologies ?? [],
|
|
134
|
+
owners: service.owners ?? [],
|
|
135
|
+
provides: int(service.provides.length),
|
|
136
|
+
methods: int(service.provides.reduce((n, provided) => n + provided.methods.length, 0)),
|
|
137
|
+
calls: int(service.consumes.length),
|
|
138
|
+
unresolvedCalls: int(service.consumes.filter((call) => call.status === "unresolved").length),
|
|
139
|
+
aggregates: int(service.aggregates.length),
|
|
140
|
+
events: int(service.aggregates.reduce((n, aggregate) => n + aggregate.events.length, 0)),
|
|
141
|
+
stores: service.stores ?? [],
|
|
142
|
+
channels: (service.channels ?? []).map((channel) => channel.address),
|
|
143
|
+
hosts: service.hosts ?? [],
|
|
144
|
+
dials: service.dials ?? [],
|
|
145
|
+
},
|
|
146
|
+
}));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function callSubjects(catalog: Catalog, index: CatalogIndex): Subject[] {
|
|
150
|
+
return servicesOf(catalog).flatMap(({ context, service }) =>
|
|
151
|
+
service.consumes.map((call) => {
|
|
152
|
+
const slash = call.id.lastIndexOf("/");
|
|
153
|
+
return {
|
|
154
|
+
id: call.id,
|
|
155
|
+
context,
|
|
156
|
+
service: service.id,
|
|
157
|
+
source: call.source,
|
|
158
|
+
row: {
|
|
159
|
+
id: call.id,
|
|
160
|
+
interface: slash >= 0 ? call.id.slice(0, slash) : call.id,
|
|
161
|
+
method: slash >= 0 ? call.id.slice(slash + 1) : "",
|
|
162
|
+
peer: call.peer,
|
|
163
|
+
status: call.status,
|
|
164
|
+
resolved: call.status !== "unresolved",
|
|
165
|
+
// The peer is a service of the estate, as opposed to an external
|
|
166
|
+
// or a name nobody answers to; and the peer answers on the method.
|
|
167
|
+
peerKnown: index.serviceById.has(call.peer),
|
|
168
|
+
methodDeclared: index.rpcProviderByMethod.has(call.id),
|
|
169
|
+
service: service.id,
|
|
170
|
+
context,
|
|
171
|
+
source: call.source,
|
|
172
|
+
module: call.module ?? "",
|
|
173
|
+
note: call.note ?? "",
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}),
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
// A vendored copy of an interface, held against what the provider publishes.
|
|
182
|
+
|
|
183
|
+
function copySubjects(catalog: Catalog, index: CatalogIndex): Subject[] {
|
|
184
|
+
return servicesOf(catalog).flatMap(({ context, service }) =>
|
|
185
|
+
(service.copies ?? []).map((copy) => {
|
|
186
|
+
const provider = providerOf(service, copy, index);
|
|
187
|
+
return {
|
|
188
|
+
id: copy.id,
|
|
189
|
+
context,
|
|
190
|
+
service: service.id,
|
|
191
|
+
source: copy.source,
|
|
192
|
+
row: {
|
|
193
|
+
id: copy.id,
|
|
194
|
+
service: service.id,
|
|
195
|
+
context,
|
|
196
|
+
provider: provider?.service.id ?? "",
|
|
197
|
+
methods: int(copy.methods.length),
|
|
198
|
+
differences: provider ? compareInterfaces(copy, provider.interface) : [],
|
|
199
|
+
source: copy.source ?? "",
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}),
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** The provider a copy is compared against: the peer of a resolved call on one of its methods, when that peer publishes the interface. */
|
|
207
|
+
function providerOf(consumer: Service, copy: RpcService, index: CatalogIndex): { service: Service; interface: RpcService } | undefined {
|
|
208
|
+
const calls = new Map(consumer.consumes.filter((call) => call.id.startsWith(`${copy.id}/`)).map((call) => [call.id, call]));
|
|
209
|
+
const resolved = copy.methods
|
|
210
|
+
.map((method) => calls.get(`${copy.id}/${method.name}`))
|
|
211
|
+
.find((call) => call?.status !== "unresolved" && index.serviceById.has(call?.peer ?? ""));
|
|
212
|
+
if (!resolved) return undefined;
|
|
213
|
+
const service = index.serviceById.get(resolved.peer);
|
|
214
|
+
const provided = service?.provides.find((candidate) => candidate.id === copy.id);
|
|
215
|
+
return service && provided ? { service, interface: provided } : undefined;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Every way a narrowed copy disagrees with the provider's interface. A method
|
|
220
|
+
* the provider lacks is not listed: that is the `proto-missing` row, and a
|
|
221
|
+
* stronger one. A field the copy omits is not a difference either - a
|
|
222
|
+
* narrowed copy may leave out what it does not read - but every field it
|
|
223
|
+
* carries must have the provider's type and number, and an enum must match
|
|
224
|
+
* whole, because names and numbers are both wire claims.
|
|
225
|
+
*/
|
|
226
|
+
export function compareInterfaces(copy: RpcService, provider: RpcService): string[] {
|
|
227
|
+
const out: string[] = [];
|
|
228
|
+
const methods = new Map(provider.methods.map((method) => [method.name, method]));
|
|
229
|
+
for (const method of copy.methods) {
|
|
230
|
+
const actual = methods.get(method.name);
|
|
231
|
+
if (actual) compareMethod(method, actual, out);
|
|
232
|
+
}
|
|
233
|
+
const messages = new Map((provider.messages ?? []).map((message) => [message.name, message]));
|
|
234
|
+
for (const message of copy.messages ?? []) compareMessage(message, messages.get(message.name), out);
|
|
235
|
+
const enums = new Map((provider.enums ?? []).map((item) => [item.name, item]));
|
|
236
|
+
for (const item of copy.enums ?? []) compareEnum(item, enums.get(item.name), out);
|
|
237
|
+
return out;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function compareMethod(copy: RpcMethod, provider: RpcMethod, out: string[]): void {
|
|
241
|
+
for (const side of ["request", "response"] as const) {
|
|
242
|
+
if (copy[side] !== provider[side]) {
|
|
243
|
+
out.push(`${copy.name} ${side} is ${copy[side] || "unnamed"}, provider has ${provider[side] || "unnamed"}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (copy.streaming !== provider.streaming) {
|
|
247
|
+
out.push(`${copy.name} streaming is ${copy.streaming ?? "unary"}, provider has ${provider.streaming ?? "unary"}`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function compareMessage(copy: RpcMessage, provider: RpcMessage | undefined, out: string[]): void {
|
|
252
|
+
if (!provider) {
|
|
253
|
+
out.push(`message ${copy.name} is absent from the provider`);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const fields = new Map(provider.fields.map((field) => [field.name, field]));
|
|
257
|
+
for (const field of copy.fields) {
|
|
258
|
+
const actual = fields.get(field.name);
|
|
259
|
+
if (!actual) {
|
|
260
|
+
out.push(`${copy.name}.${field.name} is absent from the provider`);
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if (field.type !== actual.type) out.push(`${copy.name}.${field.name} is ${field.type}, provider has ${actual.type}`);
|
|
264
|
+
if (field.number !== undefined && actual.number !== undefined && field.number !== actual.number) {
|
|
265
|
+
out.push(`${copy.name}.${field.name} is field ${field.number}, provider has ${actual.number}`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function compareEnum(copy: RpcEnum, provider: RpcEnum | undefined, out: string[]): void {
|
|
271
|
+
if (!provider) {
|
|
272
|
+
out.push(`enum ${copy.name} is absent from the provider`);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const copyByName = new Map(copy.values.map((value) => [value.name, value]));
|
|
276
|
+
const providerByName = new Map(provider.values.map((value) => [value.name, value]));
|
|
277
|
+
for (const value of copy.values) {
|
|
278
|
+
const actual = providerByName.get(value.name);
|
|
279
|
+
if (!actual) out.push(`${copy.name}.${value.name} is absent from the provider`);
|
|
280
|
+
else if (value.number !== actual.number) out.push(`${copy.name}.${value.name} is ${value.number}, provider has ${actual.number}`);
|
|
281
|
+
}
|
|
282
|
+
for (const value of provider.values) {
|
|
283
|
+
if (!copyByName.has(value.name)) out.push(`${copy.name}.${value.name} is missing from the copy`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
// Events, their consumers, and the bus.
|
|
289
|
+
|
|
290
|
+
/** The channels a service's document declares it sends domain events on. */
|
|
291
|
+
function declaredSends(service: Service): string[] {
|
|
292
|
+
return (service.channels ?? [])
|
|
293
|
+
.filter((channel) => (!channel.kind || channel.kind === "event") && channel.messages.some((message) => message.direction === "send"))
|
|
294
|
+
.map((channel) => channel.address);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function eventSubjects(catalog: Catalog): Subject[] {
|
|
298
|
+
return servicesOf(catalog).flatMap(({ context, service }) => {
|
|
299
|
+
const declaredChannels = declaredSends(service);
|
|
300
|
+
return service.aggregates.flatMap((aggregate) =>
|
|
301
|
+
aggregate.events.map((event): Subject => {
|
|
302
|
+
const latest = event.versions[event.versions.length - 1];
|
|
303
|
+
return {
|
|
304
|
+
id: event.id,
|
|
305
|
+
context,
|
|
306
|
+
service: service.id,
|
|
307
|
+
source: event.versions[0]?.source,
|
|
308
|
+
row: {
|
|
309
|
+
id: event.id,
|
|
310
|
+
slug: event.slug,
|
|
311
|
+
name: event.name,
|
|
312
|
+
aggregate: aggregate.id,
|
|
313
|
+
service: service.id,
|
|
314
|
+
context,
|
|
315
|
+
versions: int(event.versions.length),
|
|
316
|
+
deprecated: latest?.deprecated === true,
|
|
317
|
+
consumers: event.consumers.map((consumer) => consumer.service),
|
|
318
|
+
unresolvedConsumers: int(event.consumers.filter((consumer) => consumer.status === "unresolved").length),
|
|
319
|
+
wireName: event.wire?.name ?? "",
|
|
320
|
+
channel: event.wire?.channel ?? "",
|
|
321
|
+
declaredChannels,
|
|
322
|
+
fields: latest?.fields.map((field) => field.name) ?? [],
|
|
323
|
+
},
|
|
324
|
+
};
|
|
325
|
+
}),
|
|
326
|
+
);
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** One row per consumer an event names: the edge, from the publisher's side. */
|
|
331
|
+
function consumerSubjects(catalog: Catalog): Subject[] {
|
|
332
|
+
return servicesOf(catalog).flatMap(({ context, service }) =>
|
|
333
|
+
service.aggregates.flatMap((aggregate) =>
|
|
334
|
+
aggregate.events.flatMap((event) =>
|
|
335
|
+
event.consumers.map(
|
|
336
|
+
(consumer): Subject => ({
|
|
337
|
+
id: event.id,
|
|
338
|
+
context,
|
|
339
|
+
service: service.id,
|
|
340
|
+
source: undefined,
|
|
341
|
+
row: {
|
|
342
|
+
event: event.id,
|
|
343
|
+
name: event.name,
|
|
344
|
+
aggregate: aggregate.id,
|
|
345
|
+
owner: service.id,
|
|
346
|
+
context,
|
|
347
|
+
consumer: consumer.service,
|
|
348
|
+
status: consumer.status,
|
|
349
|
+
resolved: consumer.status !== "unresolved",
|
|
350
|
+
note: consumer.note ?? "",
|
|
351
|
+
},
|
|
352
|
+
}),
|
|
353
|
+
),
|
|
354
|
+
),
|
|
355
|
+
),
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
interface Publishing {
|
|
360
|
+
service: Service;
|
|
361
|
+
context: string;
|
|
362
|
+
events: Event[];
|
|
363
|
+
declared: Channel | undefined;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** What a publisher is putting on a channel, as the catalog knows it. */
|
|
367
|
+
function claimOf(publishing: Publishing): string {
|
|
368
|
+
if (publishing.events.length > 0) return publishing.events.map((event) => event.name).join(", ");
|
|
369
|
+
const sends = publishing.declared?.messages.filter((message) => message.direction === "send").map((message) => message.name) ?? [];
|
|
370
|
+
return sends.length > 0 ? sends.join(", ") : "nothing the catalog can name";
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* One row per service per channel it touches: the channels its document
|
|
375
|
+
* declares and the ones its events name on the wire, as one list. A row
|
|
376
|
+
* knows who else publishes on the address, because that is the one fact a
|
|
377
|
+
* subscriber cannot see and a rule about sharing needs.
|
|
378
|
+
*/
|
|
379
|
+
function channelSubjects(catalog: Catalog): Subject[] {
|
|
380
|
+
const rows: { address: string; publishing: Publishing }[] = [];
|
|
381
|
+
const byAddress = new Map<string, Publishing[]>();
|
|
382
|
+
for (const { context, service } of servicesOf(catalog)) {
|
|
383
|
+
const mine = new Map<string, Publishing>();
|
|
384
|
+
const at = (address: string): Publishing => {
|
|
385
|
+
let entry = mine.get(address);
|
|
386
|
+
if (!entry) {
|
|
387
|
+
entry = { service, context, events: [], declared: undefined };
|
|
388
|
+
mine.set(address, entry);
|
|
389
|
+
rows.push({ address, publishing: entry });
|
|
390
|
+
const all = byAddress.get(address) ?? [];
|
|
391
|
+
all.push(entry);
|
|
392
|
+
byAddress.set(address, all);
|
|
393
|
+
}
|
|
394
|
+
return entry;
|
|
395
|
+
};
|
|
396
|
+
for (const aggregate of service.aggregates) {
|
|
397
|
+
for (const event of aggregate.events) if (event.wire?.channel) at(event.wire.channel).events.push(event);
|
|
398
|
+
}
|
|
399
|
+
for (const channel of service.channels ?? []) at(channel.address).declared = channel;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const publishes = (entry: Publishing): boolean => {
|
|
403
|
+
if (entry.events.length > 0) return true;
|
|
404
|
+
const channel = entry.declared;
|
|
405
|
+
return Boolean(channel && (!channel.kind || channel.kind === "event") && channel.messages.some((message) => message.direction === "send"));
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
return rows.map(({ address, publishing }): Subject => {
|
|
409
|
+
const channel = publishing.declared;
|
|
410
|
+
const others = (byAddress.get(address) ?? []).filter((other) => other !== publishing && publishes(other));
|
|
411
|
+
const first = publishing.events[0];
|
|
412
|
+
const sends = channel?.messages.filter((message) => message.direction === "send").map((message) => message.name) ?? [];
|
|
413
|
+
return {
|
|
414
|
+
id: first?.id ?? publishing.service.id,
|
|
415
|
+
context: publishing.context,
|
|
416
|
+
service: publishing.service.id,
|
|
417
|
+
source: first?.versions[0]?.source ?? channel?.source,
|
|
418
|
+
row: {
|
|
419
|
+
address,
|
|
420
|
+
kind: channel?.kind ?? "event",
|
|
421
|
+
title: channel?.title ?? "",
|
|
422
|
+
service: publishing.service.id,
|
|
423
|
+
context: publishing.context,
|
|
424
|
+
declared: channel !== undefined,
|
|
425
|
+
publishes: publishes(publishing),
|
|
426
|
+
sends,
|
|
427
|
+
receives: channel?.messages.filter((message) => message.direction === "receive").map((message) => message.name) ?? [],
|
|
428
|
+
events: publishing.events.map((event) => event.id),
|
|
429
|
+
otherPublishers: others.map((other) => other.service.id),
|
|
430
|
+
otherClaims: others.map((other) => `${other.service.id}: ${claimOf(other)}`),
|
|
431
|
+
source: channel?.source ?? "",
|
|
432
|
+
},
|
|
433
|
+
};
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function wireFormat(message: ChannelMessage): string {
|
|
438
|
+
if (message.encoding) return message.encoding.trim().toLowerCase();
|
|
439
|
+
const contentType = message.contentType?.split(";", 1)[0]?.trim().toLowerCase() ?? "";
|
|
440
|
+
if (contentType.includes("msgpack") || contentType.includes("messagepack")) return "msgpack";
|
|
441
|
+
return contentType;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* One row per message a service's document says it receives, with who in
|
|
446
|
+
* the estate puts that name on that channel - the one edge in the catalog
|
|
447
|
+
* that runs from the subscriber outwards.
|
|
448
|
+
*/
|
|
449
|
+
function subscriptionSubjects(catalog: Catalog, index: CatalogIndex): Subject[] {
|
|
450
|
+
interface Sender {
|
|
451
|
+
service: Service;
|
|
452
|
+
message: ChannelMessage;
|
|
453
|
+
}
|
|
454
|
+
const senders = new Map<string, Sender[]>();
|
|
455
|
+
const published = new Set(index.eventByWireName.keys());
|
|
456
|
+
for (const { service } of servicesOf(catalog)) {
|
|
457
|
+
for (const channel of service.channels ?? []) {
|
|
458
|
+
for (const message of channel.messages) {
|
|
459
|
+
if (message.direction !== "send") continue;
|
|
460
|
+
published.add(message.name);
|
|
461
|
+
const key = `${channel.address}${message.name}`;
|
|
462
|
+
const list = senders.get(key) ?? [];
|
|
463
|
+
list.push({ service, message });
|
|
464
|
+
senders.set(key, list);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return servicesOf(catalog).flatMap(({ context, service }) =>
|
|
469
|
+
(service.channels ?? []).flatMap((channel) =>
|
|
470
|
+
channel.messages
|
|
471
|
+
.filter((message) => message.direction === "receive")
|
|
472
|
+
.map((message): Subject => {
|
|
473
|
+
const expected = wireFormat(message);
|
|
474
|
+
const sending = senders.get(`${channel.address}${message.name}`) ?? [];
|
|
475
|
+
const mismatched = expected ? sending.filter((sender) => wireFormat(sender.message) && wireFormat(sender.message) !== expected) : [];
|
|
476
|
+
return {
|
|
477
|
+
id: service.id,
|
|
478
|
+
context,
|
|
479
|
+
service: service.id,
|
|
480
|
+
source: channel.source,
|
|
481
|
+
row: {
|
|
482
|
+
service: service.id,
|
|
483
|
+
context,
|
|
484
|
+
channel: channel.address,
|
|
485
|
+
kind: channel.kind ?? "event",
|
|
486
|
+
name: message.name,
|
|
487
|
+
encoding: expected,
|
|
488
|
+
published: published.has(message.name),
|
|
489
|
+
publishers: sending.map((sender) => sender.service.id),
|
|
490
|
+
mismatched: mismatched.map((sender) => sender.service.id),
|
|
491
|
+
mismatchedEncodings: mismatched.map((sender) => wireFormat(sender.message)),
|
|
492
|
+
source: channel.source ?? "",
|
|
493
|
+
},
|
|
494
|
+
};
|
|
495
|
+
}),
|
|
496
|
+
),
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// ---------------------------------------------------------------------------
|
|
501
|
+
// Where the aggregates live.
|
|
502
|
+
|
|
503
|
+
/** The fields an aggregate's blocks declare, first declaration of a name winning. */
|
|
504
|
+
function declaredFields(catalog: Catalog, aggregate: Aggregate | undefined): Map<string, string> {
|
|
505
|
+
const fields = new Map<string, string>();
|
|
506
|
+
if (!aggregate) return fields;
|
|
507
|
+
for (const { block } of aggregateBlocks(aggregate)) {
|
|
508
|
+
for (const field of blockFields(catalog, block)) if (!fields.has(field.name)) fields.set(field.name, field.type);
|
|
509
|
+
}
|
|
510
|
+
return fields;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function relationSubject(catalog: Catalog, index: CatalogIndex, store: Store, relation: Table | View, kind: "table" | "view"): Subject {
|
|
514
|
+
const owner = index.serviceById.get(store.owner);
|
|
515
|
+
const context = contextOf(index, owner);
|
|
516
|
+
const aggregateId = relation.persists?.aggregate ?? "";
|
|
517
|
+
const aggregate = aggregateId ? index.aggregateById.get(aggregateId) : undefined;
|
|
518
|
+
const fields = declaredFields(catalog, aggregate);
|
|
519
|
+
const table = kind === "table" ? (relation as Table) : undefined;
|
|
520
|
+
const accesses = table?.accesses ?? [];
|
|
521
|
+
const claimed = relation.columns.filter((column) => column.maps);
|
|
522
|
+
const mapped = claimed.filter((column) => fields.has(mapsFieldPath(column.maps!).split(".")[0] ?? ""));
|
|
523
|
+
const view = kind === "view" ? (relation as View) : undefined;
|
|
524
|
+
const foreignReads = view
|
|
525
|
+
? viewReads(view).filter((readId) => {
|
|
526
|
+
const target = index.tableById.get(readId) ?? index.viewById.get(readId);
|
|
527
|
+
return target !== undefined && target.store.owner !== store.owner;
|
|
528
|
+
})
|
|
529
|
+
: [];
|
|
530
|
+
return {
|
|
531
|
+
id: relation.id,
|
|
532
|
+
context,
|
|
533
|
+
service: store.owner,
|
|
534
|
+
source: view?.source ?? store.source,
|
|
535
|
+
row: {
|
|
536
|
+
id: relation.id,
|
|
537
|
+
name: relation.name,
|
|
538
|
+
kind,
|
|
539
|
+
store: store.id,
|
|
540
|
+
storeKind: store.kind,
|
|
541
|
+
owner: store.owner,
|
|
542
|
+
context,
|
|
543
|
+
role: table?.role ?? "",
|
|
544
|
+
aggregate: aggregateId,
|
|
545
|
+
aggregateOwner: aggregateId ? index.aggregateOwner.get(aggregateId)?.id ?? "" : "",
|
|
546
|
+
aggregateFields: int(fields.size),
|
|
547
|
+
mappedColumns: int(mapped.length),
|
|
548
|
+
claimedColumns: int(claimed.length),
|
|
549
|
+
block: relation.persists?.block ?? "",
|
|
550
|
+
columns: relation.columns.map((column) => column.name),
|
|
551
|
+
primaryKey: relation.columns.filter((column) => column.pk).map((column) => column.name),
|
|
552
|
+
foreignKeys: relation.columns.flatMap((column) => (column.fk ? [column.fk.table] : [])),
|
|
553
|
+
foreignReads,
|
|
554
|
+
hasPayload: table ? payloadColumn(table) !== null : false,
|
|
555
|
+
indexes: int(table?.indexes?.length ?? 0),
|
|
556
|
+
reads: int(accesses.filter((access) => access.operation === "read").length),
|
|
557
|
+
writes: int(accesses.filter((access) => access.operation === "write").length),
|
|
558
|
+
deletes: int(accesses.filter((access) => access.operation === "delete").length),
|
|
559
|
+
},
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function tableSubjects(catalog: Catalog, index: CatalogIndex): Subject[] {
|
|
564
|
+
return (catalog.stores ?? []).flatMap((store) => [
|
|
565
|
+
...store.tables.map((table) => relationSubject(catalog, index, store, table, "table")),
|
|
566
|
+
...storeViews(store).map((view) => relationSubject(catalog, index, store, view, "view")),
|
|
567
|
+
]);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function columnSubject(catalog: Catalog, index: CatalogIndex, store: Store, relation: Table | View, kind: "table" | "view", column: Column): Subject {
|
|
571
|
+
const owner = index.serviceById.get(store.owner);
|
|
572
|
+
const context = contextOf(index, owner);
|
|
573
|
+
const fkTarget = column.fk ? index.tableById.get(column.fk.table) : undefined;
|
|
574
|
+
const from = column.from ?? [];
|
|
575
|
+
const foreignFrom = unique(
|
|
576
|
+
from.filter((ref) => {
|
|
577
|
+
const target = index.tableById.get(relationOfColumnId(ref)) ?? index.viewById.get(relationOfColumnId(ref));
|
|
578
|
+
return target !== undefined && target.store.owner !== store.owner;
|
|
579
|
+
}),
|
|
580
|
+
);
|
|
581
|
+
const aggregateId = relation.persists?.aggregate;
|
|
582
|
+
const fields = declaredFields(catalog, aggregateId ? index.aggregateById.get(aggregateId) : undefined);
|
|
583
|
+
const fieldType = column.maps ? fields.get(mapsFieldPath(column.maps).split(".")[0] ?? "") ?? "" : "";
|
|
584
|
+
return {
|
|
585
|
+
id: `${relation.id}.${column.name}`,
|
|
586
|
+
context,
|
|
587
|
+
service: store.owner,
|
|
588
|
+
source: store.source,
|
|
589
|
+
row: {
|
|
590
|
+
id: `${relation.id}.${column.name}`,
|
|
591
|
+
name: column.name,
|
|
592
|
+
relation: relation.id,
|
|
593
|
+
kind,
|
|
594
|
+
store: store.id,
|
|
595
|
+
owner: store.owner,
|
|
596
|
+
context,
|
|
597
|
+
type: column.type,
|
|
598
|
+
nullable: column.nullable,
|
|
599
|
+
pk: column.pk === true,
|
|
600
|
+
fk: column.fk?.table ?? "",
|
|
601
|
+
fkOwner: fkTarget?.store.owner ?? "",
|
|
602
|
+
from,
|
|
603
|
+
foreignFrom,
|
|
604
|
+
foreignOwners: unique(
|
|
605
|
+
foreignFrom.map((ref) => (index.tableById.get(relationOfColumnId(ref)) ?? index.viewById.get(relationOfColumnId(ref)))?.store.owner ?? ""),
|
|
606
|
+
),
|
|
607
|
+
maps: column.maps ?? "",
|
|
608
|
+
fieldType,
|
|
609
|
+
typeMatches: !fieldType || !typesDisagree(column.type, fieldType),
|
|
610
|
+
},
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function columnSubjects(catalog: Catalog, index: CatalogIndex): Subject[] {
|
|
615
|
+
return (catalog.stores ?? []).flatMap((store) => [
|
|
616
|
+
...store.tables.flatMap((table) => table.columns.map((column) => columnSubject(catalog, index, store, table, "table", column))),
|
|
617
|
+
...storeViews(store).flatMap((view) => view.columns.map((column) => columnSubject(catalog, index, store, view, "view", column))),
|
|
618
|
+
]);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// ---------------------------------------------------------------------------
|
|
622
|
+
// Where the services run.
|
|
623
|
+
|
|
624
|
+
/** Where the Application deploys from, as a row's far end: the thing nobody claims. */
|
|
625
|
+
function deployedFrom(deployment: Deployment): string {
|
|
626
|
+
if (deployment.chart && !deployment.path) return `chart ${deployment.chart}`;
|
|
627
|
+
return [deployment.repo, deployment.path].filter(Boolean).join("/") || "?";
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function deploymentSubjects(catalog: Catalog): Subject[] {
|
|
631
|
+
const services = servicesOf(catalog);
|
|
632
|
+
return allDeployments(catalog).map((deployment): Subject => {
|
|
633
|
+
const owner = services.find(({ service }) => deploys(deployment, service));
|
|
634
|
+
return {
|
|
635
|
+
id: deployment.id,
|
|
636
|
+
context: owner?.context ?? "",
|
|
637
|
+
service: owner?.service.id ?? "",
|
|
638
|
+
source: undefined,
|
|
639
|
+
row: {
|
|
640
|
+
id: deployment.id,
|
|
641
|
+
name: deployment.name,
|
|
642
|
+
project: deployment.project,
|
|
643
|
+
environment: deployment.environment,
|
|
644
|
+
cluster: deployment.cluster,
|
|
645
|
+
namespace: deployment.namespace,
|
|
646
|
+
repo: deployment.repo,
|
|
647
|
+
path: deployment.path,
|
|
648
|
+
chart: deployment.chart ?? "",
|
|
649
|
+
from: deployedFrom(deployment),
|
|
650
|
+
targetRevision: deployment.targetRevision,
|
|
651
|
+
revision: deployment.revision,
|
|
652
|
+
tool: deployment.tool,
|
|
653
|
+
service: owner?.service.id ?? "",
|
|
654
|
+
labelled: deployment.service ?? "",
|
|
655
|
+
claimed: owner !== undefined,
|
|
656
|
+
context: owner?.context ?? "",
|
|
657
|
+
runsIn: deployment.environment || deployment.cluster || "an unnamed environment",
|
|
658
|
+
images: deployment.images ?? [],
|
|
659
|
+
basis: deployment.basis ?? "api",
|
|
660
|
+
drifted: driftLines(deployment).length > 0,
|
|
661
|
+
drift: driftLines(deployment).join("; "),
|
|
662
|
+
url: deployment.url,
|
|
663
|
+
},
|
|
664
|
+
};
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// ---------------------------------------------------------------------------
|
|
669
|
+
// Flows and aggregates.
|
|
670
|
+
|
|
671
|
+
function flowSubject(catalog: Catalog, flow: Flow): Subject {
|
|
672
|
+
const steps = walkSteps(flow.steps);
|
|
673
|
+
const participants = flow.participants.filter((participant) => participant.kind === "service").map((participant) => participant.id);
|
|
674
|
+
const contexts = unique(flow.participants.flatMap((participant) => (participant.context ? [participant.context] : [])));
|
|
675
|
+
// The near end of a flow's row is the service that owns its first lane,
|
|
676
|
+
// which is where the flow's page says it starts; the owner is a context.
|
|
677
|
+
const first = participants[0] ?? "";
|
|
678
|
+
return {
|
|
679
|
+
id: flow.id,
|
|
680
|
+
context: flow.owner,
|
|
681
|
+
service: catalog.contexts.some((context) => context.services.some((service) => service.id === first)) ? first : "",
|
|
682
|
+
source: flow.source,
|
|
683
|
+
row: {
|
|
684
|
+
id: flow.id,
|
|
685
|
+
slug: flow.slug,
|
|
686
|
+
name: flow.name,
|
|
687
|
+
owner: flow.owner,
|
|
688
|
+
trigger: flow.trigger?.kind ?? "",
|
|
689
|
+
triggerConfidence: flow.trigger?.confidence ?? "",
|
|
690
|
+
participants,
|
|
691
|
+
contexts,
|
|
692
|
+
crossContext: contexts.length > 1,
|
|
693
|
+
steps: int(steps.length),
|
|
694
|
+
verifiedSteps: int(steps.filter((step) => step.status === "verified").length),
|
|
695
|
+
declaredSteps: int(steps.filter((step) => step.status === "declared").length),
|
|
696
|
+
unresolvedSteps: int(steps.filter((step) => step.status === "unresolved").length),
|
|
697
|
+
seenSteps: int(steps.filter((step) => step.seen !== undefined).length),
|
|
698
|
+
events: unique(steps.filter((step) => step.kind === "event" && step.ref).map((step) => step.ref!)),
|
|
699
|
+
stores: unique(steps.flatMap((step) => (step.storeAccess ? [step.storeAccess.store] : []))),
|
|
700
|
+
examples: int(flow.examples?.length ?? 0),
|
|
701
|
+
source: flow.source ?? "",
|
|
702
|
+
},
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function aggregateSubject(catalog: Catalog, index: CatalogIndex, aggregate: Aggregate): Subject {
|
|
707
|
+
const owner = index.aggregateOwner.get(aggregate.id);
|
|
708
|
+
const tables = (catalog.stores ?? []).flatMap((store) =>
|
|
709
|
+
store.tables.filter((table) => table.persists?.aggregate === aggregate.id).map((table) => table.id),
|
|
710
|
+
);
|
|
711
|
+
return {
|
|
712
|
+
id: aggregate.id,
|
|
713
|
+
context: contextOf(index, owner),
|
|
714
|
+
service: owner?.id ?? "",
|
|
715
|
+
source: undefined,
|
|
716
|
+
row: {
|
|
717
|
+
id: aggregate.id,
|
|
718
|
+
slug: aggregate.slug,
|
|
719
|
+
name: aggregate.name,
|
|
720
|
+
root: aggregate.root,
|
|
721
|
+
modelGroup: aggregate.kind === "model-group",
|
|
722
|
+
service: owner?.id ?? "",
|
|
723
|
+
context: contextOf(index, owner),
|
|
724
|
+
entities: int(aggregate.entities.length),
|
|
725
|
+
valueObjects: int(aggregate.valueObjects.length),
|
|
726
|
+
enums: int(enumsOf(aggregate).length),
|
|
727
|
+
commands: int(aggregate.operations.filter((operation) => operation.kind === "command").length),
|
|
728
|
+
queries: int(aggregate.operations.filter((operation) => operation.kind === "query").length),
|
|
729
|
+
exposedOperations: int(aggregate.operations.filter((operation) => (operation.exposedBy?.length ?? 0) > 0).length),
|
|
730
|
+
deprecatedOperations: int(aggregate.operations.filter((operation) => operation.deprecated).length),
|
|
731
|
+
events: aggregate.events.map((event) => event.id),
|
|
732
|
+
states: aggregate.lifecycle?.states ?? [],
|
|
733
|
+
transitions: int(aggregate.lifecycle?.transitions.length ?? 0),
|
|
734
|
+
tables,
|
|
735
|
+
},
|
|
736
|
+
};
|
|
737
|
+
}
|