@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
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
// The CEL half of a problem rule: what an expression may read, and whether
|
|
2
|
+
// it reads it correctly.
|
|
3
|
+
//
|
|
4
|
+
// Plain JavaScript on purpose, like django-aggregates.mjs: the manifest is
|
|
5
|
+
// checked by scripts/manifest.mjs under Node, where nothing strips types, and
|
|
6
|
+
// the same expression runs in the browser over the merged catalog. One file
|
|
7
|
+
// says what `event.consumers` is so that the two cannot disagree. Types live
|
|
8
|
+
// in problem-rules-cel.d.mts.
|
|
9
|
+
//
|
|
10
|
+
// A rule is written over ONE subject and reads a flat view of it: strings,
|
|
11
|
+
// ints, bools and lists of strings. Everything a rule would have to join -
|
|
12
|
+
// who else writes the table, who publishes on the channel, whether a call's
|
|
13
|
+
// peer is in the estate - is a field on the row, computed once by
|
|
14
|
+
// problem-subjects.ts. The rule decides; the projection only describes
|
|
15
|
+
// (portolan.0017).
|
|
16
|
+
|
|
17
|
+
import { Environment } from "@marcbachmann/cel-js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* What each subject shows to an expression. The field list is the contract a
|
|
21
|
+
* rule is type-checked against: `event.nme` is refused when the manifest is
|
|
22
|
+
* read, not discovered as an empty page.
|
|
23
|
+
*/
|
|
24
|
+
export const SUBJECTS = {
|
|
25
|
+
service: {
|
|
26
|
+
description: "One service of the estate, with counts of what it declares.",
|
|
27
|
+
schema: {
|
|
28
|
+
id: "string",
|
|
29
|
+
slug: "string",
|
|
30
|
+
name: "string",
|
|
31
|
+
context: "string",
|
|
32
|
+
kind: "string",
|
|
33
|
+
repo: "string",
|
|
34
|
+
path: "string",
|
|
35
|
+
technologies: "list<string>",
|
|
36
|
+
owners: "list<string>",
|
|
37
|
+
provides: "int",
|
|
38
|
+
methods: "int",
|
|
39
|
+
calls: "int",
|
|
40
|
+
unresolvedCalls: "int",
|
|
41
|
+
aggregates: "int",
|
|
42
|
+
events: "int",
|
|
43
|
+
stores: "list<string>",
|
|
44
|
+
channels: "list<string>",
|
|
45
|
+
hosts: "list<string>",
|
|
46
|
+
dials: "list<string>",
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
call: {
|
|
50
|
+
description: "One call a service makes on an interface, and whether the estate answers it.",
|
|
51
|
+
schema: {
|
|
52
|
+
id: "string",
|
|
53
|
+
interface: "string",
|
|
54
|
+
method: "string",
|
|
55
|
+
peer: "string",
|
|
56
|
+
status: "string",
|
|
57
|
+
resolved: "bool",
|
|
58
|
+
peerKnown: "bool",
|
|
59
|
+
methodDeclared: "bool",
|
|
60
|
+
service: "string",
|
|
61
|
+
context: "string",
|
|
62
|
+
source: "string",
|
|
63
|
+
module: "string",
|
|
64
|
+
note: "string",
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
copy: {
|
|
68
|
+
description: "One vendored copy of an interface, held against what the provider publishes.",
|
|
69
|
+
schema: {
|
|
70
|
+
id: "string",
|
|
71
|
+
service: "string",
|
|
72
|
+
context: "string",
|
|
73
|
+
provider: "string",
|
|
74
|
+
methods: "int",
|
|
75
|
+
differences: "list<string>",
|
|
76
|
+
source: "string",
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
event: {
|
|
80
|
+
description: "One domain event, as its owning aggregate declares it.",
|
|
81
|
+
schema: {
|
|
82
|
+
id: "string",
|
|
83
|
+
slug: "string",
|
|
84
|
+
name: "string",
|
|
85
|
+
aggregate: "string",
|
|
86
|
+
service: "string",
|
|
87
|
+
context: "string",
|
|
88
|
+
versions: "int",
|
|
89
|
+
deprecated: "bool",
|
|
90
|
+
consumers: "list<string>",
|
|
91
|
+
unresolvedConsumers: "int",
|
|
92
|
+
wireName: "string",
|
|
93
|
+
channel: "string",
|
|
94
|
+
declaredChannels: "list<string>",
|
|
95
|
+
fields: "list<string>",
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
consumer: {
|
|
99
|
+
description: "One consumer an event names, from the publisher's side.",
|
|
100
|
+
schema: {
|
|
101
|
+
event: "string",
|
|
102
|
+
name: "string",
|
|
103
|
+
aggregate: "string",
|
|
104
|
+
owner: "string",
|
|
105
|
+
context: "string",
|
|
106
|
+
consumer: "string",
|
|
107
|
+
status: "string",
|
|
108
|
+
resolved: "bool",
|
|
109
|
+
note: "string",
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
channel: {
|
|
113
|
+
description: "One channel a service touches, declared or named by its events, and who else publishes there.",
|
|
114
|
+
schema: {
|
|
115
|
+
address: "string",
|
|
116
|
+
kind: "string",
|
|
117
|
+
title: "string",
|
|
118
|
+
service: "string",
|
|
119
|
+
context: "string",
|
|
120
|
+
declared: "bool",
|
|
121
|
+
publishes: "bool",
|
|
122
|
+
sends: "list<string>",
|
|
123
|
+
receives: "list<string>",
|
|
124
|
+
events: "list<string>",
|
|
125
|
+
otherPublishers: "list<string>",
|
|
126
|
+
otherClaims: "list<string>",
|
|
127
|
+
source: "string",
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
subscription: {
|
|
131
|
+
description: "One message a service's document says it receives, and who in the estate sends it.",
|
|
132
|
+
schema: {
|
|
133
|
+
service: "string",
|
|
134
|
+
context: "string",
|
|
135
|
+
channel: "string",
|
|
136
|
+
kind: "string",
|
|
137
|
+
name: "string",
|
|
138
|
+
encoding: "string",
|
|
139
|
+
published: "bool",
|
|
140
|
+
publishers: "list<string>",
|
|
141
|
+
mismatched: "list<string>",
|
|
142
|
+
mismatchedEncodings: "list<string>",
|
|
143
|
+
source: "string",
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
table: {
|
|
147
|
+
description: "One table or view of a store, with what the model and the code say of it.",
|
|
148
|
+
schema: {
|
|
149
|
+
id: "string",
|
|
150
|
+
name: "string",
|
|
151
|
+
kind: "string",
|
|
152
|
+
store: "string",
|
|
153
|
+
storeKind: "string",
|
|
154
|
+
owner: "string",
|
|
155
|
+
context: "string",
|
|
156
|
+
role: "string",
|
|
157
|
+
aggregate: "string",
|
|
158
|
+
aggregateOwner: "string",
|
|
159
|
+
aggregateFields: "int",
|
|
160
|
+
mappedColumns: "int",
|
|
161
|
+
claimedColumns: "int",
|
|
162
|
+
block: "string",
|
|
163
|
+
columns: "list<string>",
|
|
164
|
+
primaryKey: "list<string>",
|
|
165
|
+
foreignKeys: "list<string>",
|
|
166
|
+
foreignReads: "list<string>",
|
|
167
|
+
hasPayload: "bool",
|
|
168
|
+
indexes: "int",
|
|
169
|
+
reads: "int",
|
|
170
|
+
writes: "int",
|
|
171
|
+
deletes: "int",
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
column: {
|
|
175
|
+
description: "One column of a table or view, with where its value points and where it came from.",
|
|
176
|
+
schema: {
|
|
177
|
+
id: "string",
|
|
178
|
+
name: "string",
|
|
179
|
+
relation: "string",
|
|
180
|
+
kind: "string",
|
|
181
|
+
store: "string",
|
|
182
|
+
owner: "string",
|
|
183
|
+
context: "string",
|
|
184
|
+
type: "string",
|
|
185
|
+
nullable: "bool",
|
|
186
|
+
pk: "bool",
|
|
187
|
+
fk: "string",
|
|
188
|
+
fkOwner: "string",
|
|
189
|
+
from: "list<string>",
|
|
190
|
+
foreignFrom: "list<string>",
|
|
191
|
+
foreignOwners: "list<string>",
|
|
192
|
+
maps: "string",
|
|
193
|
+
fieldType: "string",
|
|
194
|
+
typeMatches: "bool",
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
deployment: {
|
|
198
|
+
description: "One Application the deployer manages, and whether the catalog can place it.",
|
|
199
|
+
schema: {
|
|
200
|
+
id: "string",
|
|
201
|
+
name: "string",
|
|
202
|
+
project: "string",
|
|
203
|
+
environment: "string",
|
|
204
|
+
cluster: "string",
|
|
205
|
+
namespace: "string",
|
|
206
|
+
repo: "string",
|
|
207
|
+
path: "string",
|
|
208
|
+
chart: "string",
|
|
209
|
+
from: "string",
|
|
210
|
+
targetRevision: "string",
|
|
211
|
+
revision: "string",
|
|
212
|
+
tool: "string",
|
|
213
|
+
service: "string",
|
|
214
|
+
labelled: "string",
|
|
215
|
+
claimed: "bool",
|
|
216
|
+
context: "string",
|
|
217
|
+
runsIn: "string",
|
|
218
|
+
images: "list<string>",
|
|
219
|
+
basis: "string",
|
|
220
|
+
drifted: "bool",
|
|
221
|
+
drift: "string",
|
|
222
|
+
url: "string",
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
flow: {
|
|
226
|
+
description: "One flow, with how much of it has been seen running.",
|
|
227
|
+
schema: {
|
|
228
|
+
id: "string",
|
|
229
|
+
slug: "string",
|
|
230
|
+
name: "string",
|
|
231
|
+
owner: "string",
|
|
232
|
+
trigger: "string",
|
|
233
|
+
triggerConfidence: "string",
|
|
234
|
+
participants: "list<string>",
|
|
235
|
+
contexts: "list<string>",
|
|
236
|
+
crossContext: "bool",
|
|
237
|
+
steps: "int",
|
|
238
|
+
verifiedSteps: "int",
|
|
239
|
+
declaredSteps: "int",
|
|
240
|
+
unresolvedSteps: "int",
|
|
241
|
+
seenSteps: "int",
|
|
242
|
+
events: "list<string>",
|
|
243
|
+
stores: "list<string>",
|
|
244
|
+
examples: "int",
|
|
245
|
+
source: "string",
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
aggregate: {
|
|
249
|
+
description: "One aggregate, with what its domain model declares.",
|
|
250
|
+
schema: {
|
|
251
|
+
id: "string",
|
|
252
|
+
slug: "string",
|
|
253
|
+
name: "string",
|
|
254
|
+
root: "string",
|
|
255
|
+
modelGroup: "bool",
|
|
256
|
+
service: "string",
|
|
257
|
+
context: "string",
|
|
258
|
+
entities: "int",
|
|
259
|
+
valueObjects: "int",
|
|
260
|
+
enums: "int",
|
|
261
|
+
commands: "int",
|
|
262
|
+
queries: "int",
|
|
263
|
+
exposedOperations: "int",
|
|
264
|
+
deprecatedOperations: "int",
|
|
265
|
+
events: "list<string>",
|
|
266
|
+
states: "list<string>",
|
|
267
|
+
transitions: "int",
|
|
268
|
+
tables: "list<string>",
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
/** What every rule sees beside its subject: the estate's names, for `in`. */
|
|
274
|
+
export const ESTATE_SCHEMA = {
|
|
275
|
+
services: "list<string>",
|
|
276
|
+
contexts: "list<string>",
|
|
277
|
+
stores: "list<string>",
|
|
278
|
+
channels: "list<string>",
|
|
279
|
+
externals: "list<string>",
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
export const SUBJECT_NAMES = Object.keys(SUBJECTS);
|
|
283
|
+
|
|
284
|
+
export const SEVERITIES = ["error", "warning"];
|
|
285
|
+
|
|
286
|
+
/** A rule id: dotted or dashed lower-case segments, the way a warning rule's is. */
|
|
287
|
+
export const RULE_ID = /^[a-z0-9]+(?:[-.][a-z0-9]+)*$/;
|
|
288
|
+
|
|
289
|
+
const LIMITS = {
|
|
290
|
+
maxAstNodes: 512,
|
|
291
|
+
maxDepth: 32,
|
|
292
|
+
maxListElements: 64,
|
|
293
|
+
maxMapEntries: 64,
|
|
294
|
+
maxCallArguments: 8,
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const environments = new Map();
|
|
298
|
+
|
|
299
|
+
/** One environment per subject, built on first use and kept. */
|
|
300
|
+
export function environmentFor(subject) {
|
|
301
|
+
const known = environments.get(subject);
|
|
302
|
+
if (known) return known;
|
|
303
|
+
const definition = SUBJECTS[subject];
|
|
304
|
+
if (!definition) throw new Error(`unknown subject "${subject}"; one of ${SUBJECT_NAMES.join(", ")}`);
|
|
305
|
+
const environment = new Environment({ unlistedVariablesAreDyn: false, limits: LIMITS })
|
|
306
|
+
.registerVariable({ name: subject, schema: definition.schema })
|
|
307
|
+
.registerVariable({ name: "estate", schema: ESTATE_SCHEMA });
|
|
308
|
+
environments.set(subject, environment);
|
|
309
|
+
return environment;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const compiled = new Map();
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Parses and type-checks one expression over one subject, and refuses one
|
|
316
|
+
* that does not come out as `type`. The error names the field: it is what the
|
|
317
|
+
* manifest check prints and what the page shows under the text box.
|
|
318
|
+
*/
|
|
319
|
+
export function compileExpression(subject, source, type) {
|
|
320
|
+
const key = `${subject}\0${type}\0${source}`;
|
|
321
|
+
const known = compiled.get(key);
|
|
322
|
+
if (known) return known;
|
|
323
|
+
let expression;
|
|
324
|
+
try {
|
|
325
|
+
expression = environmentFor(subject).parse(source);
|
|
326
|
+
} catch (cause) {
|
|
327
|
+
throw new Error(`invalid CEL: ${firstLine(cause)}`);
|
|
328
|
+
}
|
|
329
|
+
const checked = expression.check();
|
|
330
|
+
if (!checked.valid) throw new Error(`invalid CEL: ${firstLine(checked.error ?? "type check failed")}`);
|
|
331
|
+
if (checked.type !== type) throw new Error(`CEL expression must return ${type}, got ${checked.type}`);
|
|
332
|
+
compiled.set(key, expression);
|
|
333
|
+
return expression;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Everything wrong with one rule entry as written, as lines. Empty when the
|
|
338
|
+
* entry is good. `builtinIds` are the rules the package ships: an entry
|
|
339
|
+
* naming one may only switch it, re-grade it and say why; any other id must
|
|
340
|
+
* carry a whole rule.
|
|
341
|
+
*/
|
|
342
|
+
export function problemRuleProblems(entries, builtinIds, path = "portolan.json") {
|
|
343
|
+
const problems = [];
|
|
344
|
+
if (entries === undefined) return problems;
|
|
345
|
+
if (!Array.isArray(entries)) return [`${path} problemRules: must be an array`];
|
|
346
|
+
const builtin = new Set(builtinIds);
|
|
347
|
+
const seen = new Set();
|
|
348
|
+
entries.forEach((entry, index) => {
|
|
349
|
+
const at = `${path} problemRules/${index}`;
|
|
350
|
+
if (!entry || typeof entry !== "object") {
|
|
351
|
+
problems.push(`${at}: must be an object`);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if (typeof entry.id !== "string" || !RULE_ID.test(entry.id)) {
|
|
355
|
+
problems.push(`${at}/id: must match ${RULE_ID}`);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
if (seen.has(entry.id)) problems.push(`${at}/id: "${entry.id}" appears twice`);
|
|
359
|
+
seen.add(entry.id);
|
|
360
|
+
if (entry.severity !== undefined && !SEVERITIES.includes(entry.severity)) {
|
|
361
|
+
problems.push(`${at}/severity: must be one of ${SEVERITIES.join(", ")}`);
|
|
362
|
+
}
|
|
363
|
+
if (builtin.has(entry.id)) {
|
|
364
|
+
for (const key of ["over", "when", "message", "peer", "title", "note", "description", "action"]) {
|
|
365
|
+
if (entry[key] !== undefined) problems.push(`${at}/${key}: "${entry.id}" is a built-in rule; only enabled, severity and reason may be set`);
|
|
366
|
+
}
|
|
367
|
+
if (entry.enabled === false && !entry.reason) problems.push(`${at}/reason: a disabled rule needs a reason`);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
problems.push(...ruleExpressionProblems(entry, at));
|
|
371
|
+
});
|
|
372
|
+
return problems;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** The expressions of one whole rule, checked over its subject; lines for what is wrong. */
|
|
376
|
+
export function ruleExpressionProblems(rule, at = rule.id) {
|
|
377
|
+
const problems = [];
|
|
378
|
+
if (!SUBJECT_NAMES.includes(rule.over)) return [`${at}/over: must be one of ${SUBJECT_NAMES.join(", ")}`];
|
|
379
|
+
if (typeof rule.title !== "string" || !rule.title.trim()) problems.push(`${at}/title: required`);
|
|
380
|
+
for (const [key, type] of [
|
|
381
|
+
["when", "bool"],
|
|
382
|
+
["message", "string"],
|
|
383
|
+
]) {
|
|
384
|
+
if (typeof rule[key] !== "string" || !rule[key].trim()) {
|
|
385
|
+
problems.push(`${at}/${key}: required`);
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
try {
|
|
389
|
+
compileExpression(rule.over, rule[key], type);
|
|
390
|
+
} catch (cause) {
|
|
391
|
+
problems.push(`${at}/${key}: ${firstLine(cause)}`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
if (rule.peer !== undefined) {
|
|
395
|
+
try {
|
|
396
|
+
compileExpression(rule.over, rule.peer, "string");
|
|
397
|
+
} catch (cause) {
|
|
398
|
+
problems.push(`${at}/peer: ${firstLine(cause)}`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return problems;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function firstLine(cause) {
|
|
405
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
406
|
+
return message.split("\n", 1)[0];
|
|
407
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
// The rules behind the Problems page: every shipped rule compiles over its
|
|
2
|
+
// subject and runs clean on the frozen estate, the manifest's switches and
|
|
3
|
+
// re-grades take effect, and a rule of the estate's own sees what the schema
|
|
4
|
+
// says it sees. What each shipped rule finds is held by the tests beside the
|
|
5
|
+
// subjects they read - rules-data, rules-wire, rules-proto, rules-deploy and rules-edges.
|
|
6
|
+
|
|
7
|
+
import { describe, expect, it } from "vitest";
|
|
8
|
+
import { rawCatalog } from "../test-catalog";
|
|
9
|
+
import { buildIndex, validateCatalog } from "../catalog";
|
|
10
|
+
import type { Catalog } from "../catalog";
|
|
11
|
+
import { allProblems, evaluateProblems } from "./all-problems";
|
|
12
|
+
import {
|
|
13
|
+
BUILTIN_RULES,
|
|
14
|
+
builtinProblems,
|
|
15
|
+
estateOf,
|
|
16
|
+
evaluateRules,
|
|
17
|
+
resolveRules,
|
|
18
|
+
runRule,
|
|
19
|
+
subjectsOf,
|
|
20
|
+
SUBJECTS,
|
|
21
|
+
} from "./problem-rules";
|
|
22
|
+
import type { ProblemRule, ProblemRuleEntry, RuleSubject } from "./problem-rules";
|
|
23
|
+
import { problemRuleProblems, ruleExpressionProblems, SUBJECT_NAMES } from "./problem-rules-cel.mjs";
|
|
24
|
+
|
|
25
|
+
const catalog = validateCatalog(JSON.parse(JSON.stringify(rawCatalog)) as unknown as Catalog);
|
|
26
|
+
const index = buildIndex(catalog);
|
|
27
|
+
const builtinIds = BUILTIN_RULES.map((rule) => rule.id);
|
|
28
|
+
|
|
29
|
+
const custom = (entry: Partial<ProblemRuleEntry> & { id: string }): ProblemRule => ({
|
|
30
|
+
over: "event",
|
|
31
|
+
severity: "warning",
|
|
32
|
+
defaultSeverity: "warning",
|
|
33
|
+
title: entry.id,
|
|
34
|
+
note: entry.id,
|
|
35
|
+
description: "",
|
|
36
|
+
action: "",
|
|
37
|
+
when: "true",
|
|
38
|
+
message: "'m'",
|
|
39
|
+
builtin: false,
|
|
40
|
+
enabled: true,
|
|
41
|
+
...entry,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("rules/builtin.json", () => {
|
|
45
|
+
it("gives every rule a subject, a severity and words", () => {
|
|
46
|
+
for (const rule of BUILTIN_RULES) {
|
|
47
|
+
expect(SUBJECT_NAMES, rule.id).toContain(rule.over);
|
|
48
|
+
expect(["error", "warning"], rule.id).toContain(rule.severity);
|
|
49
|
+
for (const field of ["title", "note", "description", "action"] as const) {
|
|
50
|
+
expect(rule[field].trim().length, `${rule.id}.${field}`).toBeGreaterThan(0);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("is CEL that type-checks over its subject, rule by rule", () => {
|
|
56
|
+
for (const rule of BUILTIN_RULES) expect(ruleExpressionProblems(rule), rule.id).toEqual([]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("runs clean over the sample estate", () => {
|
|
60
|
+
const { failures, problems } = evaluateRules(catalog, index, resolveRules([]));
|
|
61
|
+
expect(failures).toEqual([]);
|
|
62
|
+
expect(problems.length).toBeGreaterThan(0);
|
|
63
|
+
for (const problem of problems) expect(builtinIds, problem.id).toContain(problem.rule);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("has no two rules with one id", () => {
|
|
67
|
+
expect(new Set(builtinIds).size).toBe(builtinIds.length);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("resolveRules", () => {
|
|
72
|
+
it("is every shipped rule, on, without a manifest entry", () => {
|
|
73
|
+
const rules = resolveRules([]);
|
|
74
|
+
expect(rules.map((rule) => rule.id)).toEqual(builtinIds);
|
|
75
|
+
expect(rules.every((rule) => rule.enabled && rule.builtin)).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("switches and re-grades a shipped rule, and keeps the passport's own severity", () => {
|
|
79
|
+
const rules = resolveRules([
|
|
80
|
+
{ id: "shared-store", enabled: false, reason: "one database by design" },
|
|
81
|
+
{ id: "cross-service-lineage", severity: "error" },
|
|
82
|
+
]);
|
|
83
|
+
const shared = rules.find((rule) => rule.id === "shared-store")!;
|
|
84
|
+
expect(shared.enabled).toBe(false);
|
|
85
|
+
expect(shared.reason).toBe("one database by design");
|
|
86
|
+
const lineage = rules.find((rule) => rule.id === "cross-service-lineage")!;
|
|
87
|
+
expect(lineage.severity).toBe("error");
|
|
88
|
+
expect(lineage.defaultSeverity).toBe("warning");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("appends the manifest's own rules after the shipped ones", () => {
|
|
92
|
+
const rules = resolveRules([{ id: "team.quiet-event", over: "event", when: "true", message: "'x'", title: "Quiet event" }]);
|
|
93
|
+
const last = rules[rules.length - 1]!;
|
|
94
|
+
expect(last.id).toBe("team.quiet-event");
|
|
95
|
+
expect(last.builtin).toBe(false);
|
|
96
|
+
expect(last.note).toBe("Quiet event");
|
|
97
|
+
expect(last.severity).toBe("warning");
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe("evaluateRules", () => {
|
|
102
|
+
const all = builtinProblems(catalog, index);
|
|
103
|
+
|
|
104
|
+
it("drops a switched-off rule's rows and still counts them", () => {
|
|
105
|
+
const fk = all.filter((problem) => problem.rule === "cross-service-fk");
|
|
106
|
+
expect(fk.length).toBeGreaterThan(0);
|
|
107
|
+
const { problems, matches } = evaluateRules(catalog, index, resolveRules([{ id: "cross-service-fk", enabled: false, reason: "by design" }]));
|
|
108
|
+
expect(problems.some((problem) => problem.rule === "cross-service-fk")).toBe(false);
|
|
109
|
+
expect(problems.length).toBe(all.length - fk.length);
|
|
110
|
+
expect(matches.get("cross-service-fk")).toBe(fk.length);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("re-grades a rule's rows", () => {
|
|
114
|
+
const { problems } = evaluateRules(catalog, index, resolveRules([{ id: "cross-service-fk", severity: "warning" }]));
|
|
115
|
+
const fk = problems.filter((problem) => problem.rule === "cross-service-fk");
|
|
116
|
+
expect(fk.length).toBeGreaterThan(0);
|
|
117
|
+
expect(fk.every((problem) => problem.severity === "warning")).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("sorts errors first after the rules have spoken", () => {
|
|
121
|
+
const { problems } = evaluateProblems(catalog, index, resolveRules([{ id: "cross-service-fk", severity: "warning" }]));
|
|
122
|
+
const firstWarning = problems.findIndex((problem) => problem.severity === "warning");
|
|
123
|
+
expect(problems.slice(firstWarning).every((problem) => problem.severity === "warning")).toBe(true);
|
|
124
|
+
expect(allProblems(catalog, index, resolveRules([])).length).toBe(all.length);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe("a rule of the estate's own", () => {
|
|
129
|
+
it("runs over every event and says which it matched", () => {
|
|
130
|
+
const rule = custom({
|
|
131
|
+
id: "team.quiet-event",
|
|
132
|
+
when: "size(event.consumers) == 0",
|
|
133
|
+
message: "'nothing consumes ' + event.name",
|
|
134
|
+
peer: "event.service",
|
|
135
|
+
});
|
|
136
|
+
const { problems, failure } = runRule(rule, catalog, index);
|
|
137
|
+
expect(failure).toBeUndefined();
|
|
138
|
+
const quiet = subjectsOf(catalog, index, "event").filter((subject) => (subject.row.consumers as string[]).length === 0);
|
|
139
|
+
expect(problems.length).toBe(quiet.length);
|
|
140
|
+
expect(problems.length).toBeGreaterThan(0);
|
|
141
|
+
const first = problems[0]!;
|
|
142
|
+
expect(first.rule).toBe("team.quiet-event");
|
|
143
|
+
expect(first.id).toBe(quiet[0]!.id);
|
|
144
|
+
expect(first.note).toBe(`nothing consumes ${quiet[0]!.row.name}`);
|
|
145
|
+
expect(first.peer).toBe(quiet[0]!.service);
|
|
146
|
+
expect(first.context).toBe(quiet[0]!.context);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("sees the estate beside its subject", () => {
|
|
150
|
+
const inside = runRule(custom({ id: "a", over: "call", when: "call.peer in estate.services", message: "'in'" }), catalog, index);
|
|
151
|
+
const outside = runRule(custom({ id: "b", over: "call", when: "!(call.peer in estate.services)", message: "'out'" }), catalog, index);
|
|
152
|
+
expect(inside.failure).toBeUndefined();
|
|
153
|
+
expect(outside.failure).toBeUndefined();
|
|
154
|
+
expect(inside.problems.length + outside.problems.length).toBe(subjectsOf(catalog, index, "call").length);
|
|
155
|
+
expect(estateOf(catalog).services).toContain("shop.cart");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("runs over flows and aggregates, and lands on the page each has", () => {
|
|
159
|
+
const flows = runRule(
|
|
160
|
+
custom({ id: "team.unproven-flow", over: "flow", when: "flow.verifiedSteps == 0 && flow.steps > 0", message: "flow.name + ' has never been seen running'" }),
|
|
161
|
+
catalog,
|
|
162
|
+
index,
|
|
163
|
+
);
|
|
164
|
+
expect(flows.failure).toBeUndefined();
|
|
165
|
+
const unproven = subjectsOf(catalog, index, "flow").filter((subject) => subject.row.verifiedSteps === 0n && (subject.row.steps as bigint) > 0n);
|
|
166
|
+
expect(flows.problems.length).toBe(unproven.length);
|
|
167
|
+
expect(flows.problems.length).toBeGreaterThan(0);
|
|
168
|
+
expect(flows.problems[0]!.context).toBe(unproven[0]!.row.owner);
|
|
169
|
+
|
|
170
|
+
const aggregates = runRule(
|
|
171
|
+
custom({ id: "team.silent-aggregate", over: "aggregate", when: "size(aggregate.events) == 0 && !aggregate.modelGroup", message: "aggregate.name + ' raises no event'", peer: "aggregate.service" }),
|
|
172
|
+
catalog,
|
|
173
|
+
index,
|
|
174
|
+
);
|
|
175
|
+
expect(aggregates.failure).toBeUndefined();
|
|
176
|
+
const silent = subjectsOf(catalog, index, "aggregate").filter((subject) => (subject.row.events as string[]).length === 0 && subject.row.modelGroup === false);
|
|
177
|
+
expect(aggregates.problems.length).toBe(silent.length);
|
|
178
|
+
for (const problem of aggregates.problems) expect(index.aggregateById.has(problem.id), problem.id).toBe(true);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("refuses a field the subject does not have, before any row", () => {
|
|
182
|
+
const { problems, failure } = runRule(custom({ id: "a", when: "event.nme == 'x'", message: "'m'" }), catalog, index);
|
|
183
|
+
expect(problems).toEqual([]);
|
|
184
|
+
expect(failure?.message).toMatch(/nme/);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("refuses a condition that is not a bool and a message that is not a string", () => {
|
|
188
|
+
expect(runRule(custom({ id: "a", when: "event.id", message: "'m'" }), catalog, index).failure?.message).toMatch(/bool/);
|
|
189
|
+
expect(runRule(custom({ id: "b", when: "true", message: "event.versions" }), catalog, index).failure?.message).toMatch(/string/);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("is counted but not shown while switched off", () => {
|
|
193
|
+
const rule = custom({ id: "team.off", when: "true", message: "'m'", enabled: false });
|
|
194
|
+
const { problems, matches, failures } = evaluateRules(catalog, index, [rule]);
|
|
195
|
+
expect(problems).toEqual([]);
|
|
196
|
+
expect(failures).toEqual([]);
|
|
197
|
+
expect(matches.get("team.off")).toBe(subjectsOf(catalog, index, "event").length);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe("subjects", () => {
|
|
202
|
+
const shapes: Record<string, (value: unknown) => boolean> = {
|
|
203
|
+
string: (value) => typeof value === "string",
|
|
204
|
+
int: (value) => typeof value === "bigint",
|
|
205
|
+
bool: (value) => typeof value === "boolean",
|
|
206
|
+
"list<string>": (value) => Array.isArray(value) && value.every((item) => typeof item === "string"),
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
// The frozen estate declares no AsyncAPI channel, no deployment and no
|
|
210
|
+
// vendored copy; one of each is added so every subject is proved on a
|
|
211
|
+
// row, not on an absence.
|
|
212
|
+
const furnished: Catalog = JSON.parse(JSON.stringify(rawCatalog)) as unknown as Catalog;
|
|
213
|
+
const first = furnished.contexts[0]!.services[0]!;
|
|
214
|
+
first.channels = [
|
|
215
|
+
{ address: "shop.cart.basket", kind: "event", messages: [{ name: "cart.BasketCreated", direction: "send" }, { name: "payments.PaymentAuthorized", direction: "receive" }], source: "asyncapi.yaml" },
|
|
216
|
+
];
|
|
217
|
+
first.copies = [{ id: "pricing.v1.Pricing", methods: [{ name: "GetQuote", request: "GetQuoteRequest", response: "Quote" }], source: "vendor/pricing.proto" }];
|
|
218
|
+
furnished.deployments = [
|
|
219
|
+
{ id: "argocd/cart", name: "cart", project: "shop", environment: "prod", cluster: "in-cluster", namespace: "shop", repo: first.repo, path: first.path, targetRevision: "main", revision: "abc", tool: "kustomize", url: "https://argocd/cart" },
|
|
220
|
+
];
|
|
221
|
+
const furnishedCatalog = validateCatalog(furnished);
|
|
222
|
+
const furnishedIndex = buildIndex(furnishedCatalog);
|
|
223
|
+
|
|
224
|
+
for (const over of SUBJECT_NAMES as RuleSubject[]) {
|
|
225
|
+
it(`${over}: every row has exactly the schema's fields, typed as it says`, () => {
|
|
226
|
+
const rows = subjectsOf(furnishedCatalog, furnishedIndex, over);
|
|
227
|
+
expect(rows.length, over).toBeGreaterThan(0);
|
|
228
|
+
const schema = SUBJECTS[over].schema;
|
|
229
|
+
for (const subject of rows) {
|
|
230
|
+
expect(Object.keys(subject.row).sort(), subject.id).toEqual(Object.keys(schema).sort());
|
|
231
|
+
for (const [field, type] of Object.entries(schema)) {
|
|
232
|
+
expect(shapes[type]!(subject.row[field]), `${over} ${subject.id}.${field} as ${type}`).toBe(true);
|
|
233
|
+
}
|
|
234
|
+
expect(subject.id.length).toBeGreaterThan(0);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
describe("problemRuleProblems", () => {
|
|
241
|
+
it("accepts a switch, a re-grade and a whole rule", () => {
|
|
242
|
+
expect(
|
|
243
|
+
problemRuleProblems(
|
|
244
|
+
[
|
|
245
|
+
{ id: "shared-store", enabled: false, reason: "by design" },
|
|
246
|
+
{ id: "cross-service-fk", severity: "warning" },
|
|
247
|
+
{ id: "team.quiet-event", over: "event", when: "size(event.consumers) == 0", message: "'quiet ' + event.id", title: "Quiet event" },
|
|
248
|
+
],
|
|
249
|
+
builtinIds,
|
|
250
|
+
),
|
|
251
|
+
).toEqual([]);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("names what is wrong, by entry and field", () => {
|
|
255
|
+
const problems = problemRuleProblems(
|
|
256
|
+
[
|
|
257
|
+
{ id: "shared-store", enabled: false },
|
|
258
|
+
{ id: "rpc", when: "true" },
|
|
259
|
+
{ id: "team.a", over: "event", when: "event.nme == 'x'", message: "'m'", title: "A" },
|
|
260
|
+
{ id: "team.b", over: "event", when: "true", message: "event.versions", title: "B" },
|
|
261
|
+
{ id: "team.c", over: "nowhere", when: "true", message: "'m'", title: "C" },
|
|
262
|
+
{ id: "team.d", over: "event", when: "true", message: "'m'" },
|
|
263
|
+
{ id: "Bad Id", over: "event", when: "true", message: "'m'", title: "D" },
|
|
264
|
+
{ id: "team.a", over: "event", when: "true", message: "'m'", title: "again" },
|
|
265
|
+
],
|
|
266
|
+
builtinIds,
|
|
267
|
+
"x.json",
|
|
268
|
+
);
|
|
269
|
+
expect(problems).toEqual(
|
|
270
|
+
expect.arrayContaining([
|
|
271
|
+
expect.stringMatching(/^x\.json problemRules\/0\/reason: /),
|
|
272
|
+
expect.stringMatching(/^x\.json problemRules\/1\/when: "rpc" is a built-in rule/),
|
|
273
|
+
expect.stringMatching(/^x\.json problemRules\/2\/when: .*nme/),
|
|
274
|
+
expect.stringMatching(/^x\.json problemRules\/3\/message: CEL expression must return string/),
|
|
275
|
+
expect.stringMatching(/^x\.json problemRules\/4\/over: /),
|
|
276
|
+
expect.stringMatching(/^x\.json problemRules\/5\/title: required/),
|
|
277
|
+
expect.stringMatching(/^x\.json problemRules\/6\/id: /),
|
|
278
|
+
expect.stringMatching(/^x\.json problemRules\/7\/id: "team\.a" appears twice/),
|
|
279
|
+
]),
|
|
280
|
+
);
|
|
281
|
+
expect(problems.length).toBe(8);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("is nothing to say about a manifest without the key", () => {
|
|
285
|
+
expect(problemRuleProblems(undefined, builtinIds)).toEqual([]);
|
|
286
|
+
});
|
|
287
|
+
});
|