@eventcatalog/core 5.2.0 → 5.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/dist/analytics/analytics.cjs +1 -1
- package/dist/analytics/analytics.js +2 -2
- package/dist/analytics/count-resources.cjs +10 -0
- package/dist/analytics/count-resources.js +1 -1
- package/dist/analytics/log-build.cjs +11 -1
- package/dist/analytics/log-build.js +4 -4
- package/dist/{chunk-DJCOJ77J.js → chunk-7RJN4FNG.js} +1 -1
- package/dist/{chunk-RNYFUWJI.js → chunk-EPDVXHC7.js} +4 -4
- package/dist/{chunk-XI5HQWM2.js → chunk-O2K4VPDO.js} +1 -1
- package/dist/{chunk-N2LLD5TR.js → chunk-OT2U5OES.js} +1 -1
- package/dist/{chunk-QJ7KKN2D.js → chunk-OX6TSVHY.js} +1 -1
- package/dist/{chunk-66OL4B7U.js → chunk-PAS36QHK.js} +1 -1
- package/dist/{chunk-NBYKF4Z4.js → chunk-TERJXCLK.js} +1 -1
- package/dist/{chunk-ZAZHAVKB.js → chunk-YRZK7HKX.js} +10 -0
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/docs/development/ask-your-architecture/03-mcp-server/getting-started.md +1 -1
- package/dist/docs/development/developer-tools/api-catalog.md +0 -2
- package/dist/docs/development/export/01-introduction.md +1 -1
- package/dist/docs/development/export/04-reference.md +1 -1
- package/dist/docs/development/guides/fitness-functions/01-what-are-fitness-functions.md +116 -0
- package/dist/docs/development/guides/fitness-functions/02-create-fitness-function.md +222 -0
- package/dist/docs/development/guides/fitness-functions/03-global-and-resource-functions.md +198 -0
- package/dist/docs/development/guides/fitness-functions/04-run-in-ci.md +223 -0
- package/dist/docs/development/guides/fitness-functions/05-api-reference.md +271 -0
- package/dist/docs/development/guides/fitness-functions/06-examples.md +354 -0
- package/dist/docs/development/guides/fitness-functions/_category_.json +11 -0
- package/dist/docs/development/guides/owners/05-teams-reference.md +0 -12
- package/dist/docs/development/guides/owners/06-users-reference.md +0 -12
- package/dist/docs/development/upgrading/v5.md +1 -1
- package/dist/eventcatalog.cjs +26 -2
- package/dist/eventcatalog.js +28 -14
- package/dist/federation/federate.js +3 -3
- package/dist/federation/source-provider.js +2 -2
- package/dist/generate.cjs +1 -1
- package/dist/generate.js +3 -3
- package/dist/runtime-dependencies/@eventcatalog__fitness.mjs +3 -0
- package/dist/runtime-dependencies/@modelcontextprotocol__client.mjs +3 -0
- package/dist/runtime-dependencies/@modelcontextprotocol__server.mjs +3 -0
- package/dist/runtime-dependencies/manifest.json +3 -4
- package/dist/search-indexer.cjs +1 -1
- package/dist/search-indexer.js +1 -1
- package/dist/utils/cli-logger.cjs +1 -1
- package/dist/utils/cli-logger.js +2 -2
- package/eventcatalog/src/content.config.ts +0 -3
- package/eventcatalog/src/features/collections/resource-docs.ts +0 -1
- package/eventcatalog/src/features/mcp/apps/generated/canvas.html +73 -74
- package/eventcatalog/src/features/mcp/apps/generated/viewer.html +71 -73
- package/eventcatalog/src/features/mcp/apps/shared/app-view.tsx +1 -1
- package/eventcatalog/src/features/mcp/custom-tool-input.ts +38 -0
- package/eventcatalog/src/features/mcp/mcp-apps.ts +6 -4
- package/eventcatalog/src/features/mcp/mcp-scope.ts +2 -3
- package/eventcatalog/src/features/mcp/mcp-server.ts +24 -17
- package/eventcatalog/src/features/studio/server/canvas-mcp.ts +2 -4
- package/eventcatalog/src/features/tools/catalog-tools.ts +6 -8
- package/eventcatalog/src/pages/api-catalog/specifications/[collection]/[id]/[version]/[specification].ts +2 -4
- package/eventcatalog/src/utils/collections/adrs.ts +0 -1
- package/eventcatalog/src/utils/collections/agents.ts +0 -1
- package/eventcatalog/src/utils/collections/channels.ts +0 -1
- package/eventcatalog/src/utils/collections/commands.ts +0 -1
- package/eventcatalog/src/utils/collections/containers.ts +0 -1
- package/eventcatalog/src/utils/collections/domains.ts +0 -2
- package/eventcatalog/src/utils/collections/entities.ts +0 -1
- package/eventcatalog/src/utils/collections/events.ts +0 -1
- package/eventcatalog/src/utils/collections/flows.ts +0 -2
- package/eventcatalog/src/utils/collections/owners.ts +1 -4
- package/eventcatalog/src/utils/collections/queries.ts +0 -1
- package/eventcatalog/src/utils/collections/resource-docs.ts +1 -5
- package/eventcatalog/src/utils/collections/resource-owners.ts +1 -2
- package/eventcatalog/src/utils/collections/services.ts +0 -1
- package/eventcatalog/src/utils/collections/systems.ts +0 -1
- package/eventcatalog/src/utils/collections/teams.ts +3 -6
- package/eventcatalog/src/utils/collections/users.ts +4 -8
- package/eventcatalog/src/utils/homepage-dashboard.ts +2 -3
- package/eventcatalog/src/utils/node-graphs/flow-lanes.ts +2 -6
- package/package.json +9 -6
- package/dist/runtime-dependencies/@modelcontextprotocol__sdk.mjs +0 -3
- package/dist/runtime-dependencies/@modelcontextprotocol__sdk__server__mcp.js.mjs +0 -3
- package/dist/runtime-dependencies/@modelcontextprotocol__sdk__server__webStandardStreamableHttp.js.mjs +0 -3
- package/dist/runtime-dependencies/@modelcontextprotocol__sdk__types.js.mjs +0 -3
- package/eventcatalog/src/features/studio/server/tool-icons.ts +0 -30
- package/dist/{chunk-A2RZR3U4.js → chunk-6K7XZAYI.js} +3 -3
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
---
|
|
2
|
+
sidebar_position: 6
|
|
3
|
+
sidebar_label: Examples
|
|
4
|
+
title: Fitness function examples
|
|
5
|
+
description: Ready-to-use fitness functions for team ownership, shared databases, coupling, circular commands, deprecated messages and business-critical services.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
These fitness functions check rules teams commonly agree on. Copy one into a `fitness/` folder in your catalog, change the names and limits to match your architecture, and run `npx eventcatalog fitness check`.
|
|
9
|
+
|
|
10
|
+
Each one only works with the relationships and metadata your catalog records. A rule about data stores needs `writesTo` on your services, a rule about deprecation needs `deprecated` on your messages, and so on. If a rule passes when you expect it to fail, check that your catalog documents what it reads.
|
|
11
|
+
|
|
12
|
+
| Example | Checks | Severity |
|
|
13
|
+
|---|---|---|
|
|
14
|
+
| [Every service is owned by a team](#every-service-is-owned-by-a-team) | Services list a team, not only people, in `owners` | `error` |
|
|
15
|
+
| [Each data store has one writer](#each-data-store-has-one-writer) | No data store is written to by more than one service | `warning` |
|
|
16
|
+
| [Services stay loosely coupled](#services-stay-loosely-coupled) | No service receives more than a set number of messages | `warning` |
|
|
17
|
+
| [No circular commands](#no-circular-commands) | Services don't send commands to each other in a loop | `error` |
|
|
18
|
+
| [No deprecated messages](#no-deprecated-messages) | No service sends or receives a deprecated message | `warning` |
|
|
19
|
+
| [Business-critical services are documented](#business-critical-services-are-documented) | Services with a "Business Critical" badge have a repository, owners and a specification | `error` |
|
|
20
|
+
| [Domain boundaries](#domain-boundaries) | A domain's services only use its own data stores | `error` |
|
|
21
|
+
|
|
22
|
+
## Every service is owned by a team
|
|
23
|
+
|
|
24
|
+
`owners` can list [teams and users](/docs/development/guides/owners/what-are-teams-and-users). A service owned only by a person loses its owner when that person moves on. This rule checks each owner with `catalog.getTeam`, and fails services that don't have at least one team.
|
|
25
|
+
|
|
26
|
+
```ts title="fitness/team-ownership.fitness.ts"
|
|
27
|
+
import { defineFitnessFunction } from '@eventcatalog/fitness';
|
|
28
|
+
|
|
29
|
+
export default defineFitnessFunction({
|
|
30
|
+
id: 'services-owned-by-teams',
|
|
31
|
+
name: 'Every service is owned by a team',
|
|
32
|
+
description: 'People move on. A team keeps owning the service when they do.',
|
|
33
|
+
|
|
34
|
+
async run({ catalog }) {
|
|
35
|
+
const services = await catalog.getServices({ latestOnly: true });
|
|
36
|
+
const violations = [];
|
|
37
|
+
|
|
38
|
+
for (const service of services) {
|
|
39
|
+
// owners can list teams and users. Keep the ones that are teams.
|
|
40
|
+
const teams = [];
|
|
41
|
+
for (const owner of service.owners ?? []) {
|
|
42
|
+
if (await catalog.getTeam(owner)) teams.push(owner);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (teams.length === 0) {
|
|
46
|
+
violations.push({
|
|
47
|
+
message: `${service.name} is not owned by a team`,
|
|
48
|
+
resources: [{ type: 'service', id: service.id, version: service.version }],
|
|
49
|
+
suggestion: 'Add the owning team to the owners field',
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return violations;
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
✗ Every service is owned by a team 1 violation
|
|
61
|
+
• Payment Worker is not owned by a team
|
|
62
|
+
Resources: payment-worker (service)
|
|
63
|
+
Suggestion: Add the owning team to the owners field
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Each data store has one writer
|
|
67
|
+
|
|
68
|
+
When several services write to the same [data store](/docs/development/guides/resources/data/introduction), none of them can change its schema without coordinating with the others. This rule groups services by the data stores in their [`writesTo`](/docs/development/guides/resources/data/add-read-write-relationships) and warns about any data store with more than one writer.
|
|
69
|
+
|
|
70
|
+
```ts title="fitness/single-writer.fitness.ts"
|
|
71
|
+
import { defineFitnessFunction } from '@eventcatalog/fitness';
|
|
72
|
+
|
|
73
|
+
export default defineFitnessFunction({
|
|
74
|
+
id: 'single-writer-per-data-store',
|
|
75
|
+
name: 'Each data store has one writer',
|
|
76
|
+
description: 'A data store written by several services is a shared database: none of them can change its schema safely.',
|
|
77
|
+
severity: 'warning',
|
|
78
|
+
|
|
79
|
+
async run({ catalog }) {
|
|
80
|
+
const services = await catalog.getServices({ latestOnly: true });
|
|
81
|
+
|
|
82
|
+
// Group the services by the data stores they write to
|
|
83
|
+
const writers = new Map<string, typeof services>();
|
|
84
|
+
for (const service of services) {
|
|
85
|
+
for (const store of service.writesTo ?? []) {
|
|
86
|
+
writers.set(store.id, [...(writers.get(store.id) ?? []), service]);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return [...writers]
|
|
91
|
+
.filter(([, services]) => services.length > 1)
|
|
92
|
+
.map(([storeId, services]) => ({
|
|
93
|
+
message: `${storeId} is written to by ${services.length} services`,
|
|
94
|
+
// The data store first, then every service that writes to it
|
|
95
|
+
resources: [
|
|
96
|
+
{ type: 'container', id: storeId },
|
|
97
|
+
...services.map((service) => ({ type: 'service', id: service.id, version: service.version })),
|
|
98
|
+
],
|
|
99
|
+
suggestion: 'Give the data store one owning service, and have the others send it commands or events',
|
|
100
|
+
}));
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
! Each data store has one writer 5 warnings
|
|
107
|
+
• review-database is written to by 2 services
|
|
108
|
+
Resources: review-database (container), review-moderation-worker (service), review-api (service)
|
|
109
|
+
Suggestion: Give the data store one owning service, and have the others send it commands or events
|
|
110
|
+
• payment-database is written to by 2 services
|
|
111
|
+
Resources: payment-database (container), payment-worker (service), payment-api (service)
|
|
112
|
+
Suggestion: Give the data store one owning service, and have the others send it commands or events
|
|
113
|
+
...
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
A worker and an API that share a database can be a deliberate choice. Start this rule as a `warning`, and change it to `error` once the shared data stores you've decided to keep are documented.
|
|
117
|
+
|
|
118
|
+
## Services stay loosely coupled
|
|
119
|
+
|
|
120
|
+
A service that receives many messages usually has more than one reason to change. This rule warns when a service receives more messages than a limit you choose.
|
|
121
|
+
|
|
122
|
+
```ts title="fitness/max-dependencies.fitness.ts"
|
|
123
|
+
import { defineFitnessFunction } from '@eventcatalog/fitness';
|
|
124
|
+
|
|
125
|
+
const MAX_MESSAGES_RECEIVED = 8;
|
|
126
|
+
|
|
127
|
+
export default defineFitnessFunction({
|
|
128
|
+
id: 'max-messages-received',
|
|
129
|
+
name: `Services receive at most ${MAX_MESSAGES_RECEIVED} messages`,
|
|
130
|
+
severity: 'warning',
|
|
131
|
+
|
|
132
|
+
async run({ catalog }) {
|
|
133
|
+
const services = await catalog.getServices({ latestOnly: true });
|
|
134
|
+
|
|
135
|
+
return services
|
|
136
|
+
.filter((service) => (service.receives ?? []).length > MAX_MESSAGES_RECEIVED)
|
|
137
|
+
.map((service) => ({
|
|
138
|
+
message: `${service.name} receives ${service.receives!.length} messages`,
|
|
139
|
+
resources: [{ type: 'service', id: service.id, version: service.version }],
|
|
140
|
+
details: 'A service that reacts to many messages usually has more than one reason to change.',
|
|
141
|
+
suggestion: 'Look for a group of messages that could move to a service of its own',
|
|
142
|
+
}));
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
To set a different limit for one service, put a copy of the rule in that service's `fitness/` folder and check `resource` instead of every service. See [resource fitness functions](./03-global-and-resource-functions.md#resource-fitness-functions) for an example.
|
|
148
|
+
|
|
149
|
+
## No circular commands
|
|
150
|
+
|
|
151
|
+
Events often go back and forth between services, and that's fine: each service reacts to what happened. Commands are different. If service A tells B what to do, and B tells A what to do, the two can't be changed or deployed independently.
|
|
152
|
+
|
|
153
|
+
This rule builds a graph of which services send commands to which, from what each service `sends` and `receives`, and reports every loop it finds.
|
|
154
|
+
|
|
155
|
+
```ts title="fitness/no-circular-commands.fitness.ts"
|
|
156
|
+
import { defineFitnessFunction } from '@eventcatalog/fitness';
|
|
157
|
+
|
|
158
|
+
export default defineFitnessFunction({
|
|
159
|
+
id: 'no-circular-commands',
|
|
160
|
+
name: 'Services do not send commands in a circle',
|
|
161
|
+
description: 'If A commands B and B commands A, neither can be deployed or understood without the other.',
|
|
162
|
+
|
|
163
|
+
async run({ catalog }) {
|
|
164
|
+
const services = await catalog.getServices({ latestOnly: true });
|
|
165
|
+
const commands = new Set((await catalog.getCommands({ latestOnly: true })).map((command) => command.id));
|
|
166
|
+
|
|
167
|
+
// Who sends each command, and who receives it
|
|
168
|
+
const senders = new Map<string, string[]>();
|
|
169
|
+
for (const service of services) {
|
|
170
|
+
for (const message of service.sends ?? []) {
|
|
171
|
+
if (commands.has(message.id)) senders.set(message.id, [...(senders.get(message.id) ?? []), service.id]);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// An edge from each sender to the service that receives its command
|
|
176
|
+
const edges = new Map<string, Set<string>>();
|
|
177
|
+
for (const service of services) {
|
|
178
|
+
for (const message of service.receives ?? []) {
|
|
179
|
+
for (const sender of senders.get(message.id) ?? []) {
|
|
180
|
+
if (sender === service.id) continue;
|
|
181
|
+
edges.set(sender, (edges.get(sender) ?? new Set()).add(service.id));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Depth-first search. Reaching a service that is already on the path means there's a cycle.
|
|
187
|
+
const cycles = new Map<string, string[]>();
|
|
188
|
+
const visit = (id: string, path: string[]) => {
|
|
189
|
+
if (path.includes(id)) {
|
|
190
|
+
const cycle = path.slice(path.indexOf(id));
|
|
191
|
+
// Report each cycle once, however it was reached
|
|
192
|
+
cycles.set([...cycle].sort().join(','), cycle);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
for (const next of edges.get(id) ?? []) visit(next, [...path, id]);
|
|
196
|
+
};
|
|
197
|
+
for (const id of edges.keys()) visit(id, []);
|
|
198
|
+
|
|
199
|
+
return [...cycles.values()].map((cycle) => ({
|
|
200
|
+
message: `Commands go round in a circle: ${[...cycle, cycle[0]].join(' → ')}`,
|
|
201
|
+
resources: cycle.map((id) => ({ type: 'service', id })),
|
|
202
|
+
suggestion: 'Replace one of the commands with an event, so one service reacts instead of being told',
|
|
203
|
+
}));
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
```
|
|
209
|
+
✗ Services do not send commands in a circle 1 violation
|
|
210
|
+
• Commands go round in a circle: order-service → checkout-orchestrator → order-service
|
|
211
|
+
Resources: order-service (service), checkout-orchestrator (service)
|
|
212
|
+
Suggestion: Replace one of the commands with an event, so one service reacts instead of being told
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
To check for loops through events and queries too, remove the `commands.has(...)` check. Expect more results: in an event-driven architecture, many of those loops are intended.
|
|
216
|
+
|
|
217
|
+
## No deprecated messages
|
|
218
|
+
|
|
219
|
+
When you [deprecate a message](/docs/development/guides/resources/messages/versioning-and-lifecycle/deprecate-messages), the services still using it need to move off it. This rule finds every service that still sends or receives a deprecated event, command or query, and includes the deprecation message from the frontmatter in the result.
|
|
220
|
+
|
|
221
|
+
```ts title="fitness/no-deprecated-messages.fitness.ts"
|
|
222
|
+
import { defineFitnessFunction } from '@eventcatalog/fitness';
|
|
223
|
+
|
|
224
|
+
export default defineFitnessFunction({
|
|
225
|
+
id: 'no-deprecated-messages',
|
|
226
|
+
name: 'Services do not use deprecated messages',
|
|
227
|
+
severity: 'warning',
|
|
228
|
+
|
|
229
|
+
async run({ catalog }) {
|
|
230
|
+
// Every deprecated event, command and query, by id
|
|
231
|
+
const messages = [
|
|
232
|
+
...(await catalog.getEvents({ latestOnly: true })).map((message) => ({ ...message, type: 'event' })),
|
|
233
|
+
...(await catalog.getCommands({ latestOnly: true })).map((message) => ({ ...message, type: 'command' })),
|
|
234
|
+
...(await catalog.getQueries({ latestOnly: true })).map((message) => ({ ...message, type: 'query' })),
|
|
235
|
+
];
|
|
236
|
+
const deprecated = new Map(messages.filter((message) => message.deprecated).map((message) => [message.id, message]));
|
|
237
|
+
|
|
238
|
+
const services = await catalog.getServices({ latestOnly: true });
|
|
239
|
+
const violations = [];
|
|
240
|
+
|
|
241
|
+
for (const service of services) {
|
|
242
|
+
for (const pointer of [...(service.sends ?? []), ...(service.receives ?? [])]) {
|
|
243
|
+
const message = deprecated.get(pointer.id);
|
|
244
|
+
if (!message) continue;
|
|
245
|
+
|
|
246
|
+
violations.push({
|
|
247
|
+
message: `${service.name} still uses ${message.name}, which is deprecated`,
|
|
248
|
+
resources: [
|
|
249
|
+
{ type: 'service', id: service.id, version: service.version },
|
|
250
|
+
{ type: message.type, id: message.id, version: message.version },
|
|
251
|
+
],
|
|
252
|
+
// Use the deprecation message from the frontmatter when there is one
|
|
253
|
+
details: typeof message.deprecated === 'object' ? message.deprecated.message : undefined,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return violations;
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
```
|
|
264
|
+
! Services do not use deprecated messages 1 warning
|
|
265
|
+
• Order Service still uses Order Amended, which is deprecated
|
|
266
|
+
Resources: order-service (service), order-amended (event)
|
|
267
|
+
Use OrderUpdated instead
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## Business-critical services are documented
|
|
271
|
+
|
|
272
|
+
Some services need more documentation than others. This rule finds services marked as business-critical with a badge, and checks each one has a repository, owners and an API specification.
|
|
273
|
+
|
|
274
|
+
```ts title="fitness/critical-services.fitness.ts"
|
|
275
|
+
import { defineFitnessFunction } from '@eventcatalog/fitness';
|
|
276
|
+
|
|
277
|
+
export default defineFitnessFunction({
|
|
278
|
+
id: 'critical-services-documented',
|
|
279
|
+
name: 'Business-critical services are fully documented',
|
|
280
|
+
|
|
281
|
+
async run({ catalog }) {
|
|
282
|
+
const services = await catalog.getServices({ latestOnly: true });
|
|
283
|
+
|
|
284
|
+
// Services are marked as critical with a badge in their frontmatter
|
|
285
|
+
const critical = services.filter((service) =>
|
|
286
|
+
service.badges?.some((badge) => badge.content === 'Business Critical')
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
const violations = [];
|
|
290
|
+
for (const service of critical) {
|
|
291
|
+
const resources = [{ type: 'service', id: service.id, version: service.version }];
|
|
292
|
+
if (!service.repository?.url) {
|
|
293
|
+
violations.push({ message: `${service.name} has no repository`, resources });
|
|
294
|
+
}
|
|
295
|
+
if (!service.owners?.length) {
|
|
296
|
+
violations.push({ message: `${service.name} has no owners`, resources });
|
|
297
|
+
}
|
|
298
|
+
if (!service.specifications) {
|
|
299
|
+
violations.push({ message: `${service.name} has no API specification`, resources });
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return violations;
|
|
303
|
+
},
|
|
304
|
+
});
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
The badge is one way to mark a service. You can use any frontmatter your catalog agrees on, for example a custom `x-tier: critical` property, and check `service['x-tier'] === 'critical'` instead.
|
|
308
|
+
|
|
309
|
+
## Domain boundaries
|
|
310
|
+
|
|
311
|
+
A domain's services should only use the domain's own data stores. This rule lives in the domain's folder, so it's given the domain as `resource`, and the domain team owns it. The full example is on [Global and resource fitness functions](./03-global-and-resource-functions.md#resource-fitness-functions).
|
|
312
|
+
|
|
313
|
+
## Share code between fitness functions
|
|
314
|
+
|
|
315
|
+
Only files ending in `.fitness.ts` are run, so you can keep shared code in other files in a `fitness/` folder and import it. Type the catalog with `ReadOnlyCatalog`:
|
|
316
|
+
|
|
317
|
+
```ts title="fitness/helpers.ts"
|
|
318
|
+
import type { ReadOnlyCatalog } from '@eventcatalog/fitness';
|
|
319
|
+
|
|
320
|
+
/** The latest version of every event, command and query, with its type */
|
|
321
|
+
export const getMessages = async (catalog: ReadOnlyCatalog) => [
|
|
322
|
+
...(await catalog.getEvents({ latestOnly: true })).map((message) => ({ ...message, type: 'event' as const })),
|
|
323
|
+
...(await catalog.getCommands({ latestOnly: true })).map((message) => ({ ...message, type: 'command' as const })),
|
|
324
|
+
...(await catalog.getQueries({ latestOnly: true })).map((message) => ({ ...message, type: 'query' as const })),
|
|
325
|
+
];
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
```ts title="fitness/message-summaries.fitness.ts"
|
|
329
|
+
import { defineFitnessFunction } from '@eventcatalog/fitness';
|
|
330
|
+
import { getMessages } from './helpers';
|
|
331
|
+
|
|
332
|
+
export default defineFitnessFunction({
|
|
333
|
+
id: 'message-summaries',
|
|
334
|
+
name: 'Every message has a summary',
|
|
335
|
+
severity: 'warning',
|
|
336
|
+
|
|
337
|
+
async run({ catalog }) {
|
|
338
|
+
const messages = await getMessages(catalog);
|
|
339
|
+
|
|
340
|
+
return messages
|
|
341
|
+
.filter((message) => !message.summary?.trim())
|
|
342
|
+
.map((message) => ({
|
|
343
|
+
message: `${message.name} has no summary`,
|
|
344
|
+
resources: [{ type: message.type, id: message.id, version: message.version }],
|
|
345
|
+
}));
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
## Related
|
|
351
|
+
|
|
352
|
+
- [Create a fitness function](./02-create-fitness-function.md)
|
|
353
|
+
- [API reference](./05-api-reference.md): everything `run` is given and can return
|
|
354
|
+
- [Run in CI](./04-run-in-ci.md)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"label": "Fitness functions",
|
|
3
|
+
"position": 7,
|
|
4
|
+
"collapsible": true,
|
|
5
|
+
"collapsed": true,
|
|
6
|
+
"link": {
|
|
7
|
+
"type": "generated-index",
|
|
8
|
+
"slug": "/fitness-functions",
|
|
9
|
+
"description": "Write your architecture rules as TypeScript functions, run them against your catalog locally or in CI, and see which resources break them."
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -78,18 +78,6 @@ email: payments@example.com
|
|
|
78
78
|
---
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
-
### `hidden` {#hidden}
|
|
82
|
-
|
|
83
|
-
- Type: `boolean`
|
|
84
|
-
|
|
85
|
-
Hide the team from generated lists.
|
|
86
|
-
|
|
87
|
-
```md title="Example"
|
|
88
|
-
---
|
|
89
|
-
hidden: true
|
|
90
|
-
---
|
|
91
|
-
```
|
|
92
|
-
|
|
93
81
|
### `slackDirectMessageUrl` {#slackDirectMessageUrl}
|
|
94
82
|
|
|
95
83
|
- Type: `string`
|
|
@@ -90,18 +90,6 @@ email: david@example.com
|
|
|
90
90
|
---
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
-
### `hidden` {#hidden}
|
|
94
|
-
|
|
95
|
-
- Type: `boolean`
|
|
96
|
-
|
|
97
|
-
Hide the user from generated lists.
|
|
98
|
-
|
|
99
|
-
```md title="Example"
|
|
100
|
-
---
|
|
101
|
-
hidden: true
|
|
102
|
-
---
|
|
103
|
-
```
|
|
104
|
-
|
|
105
93
|
### `slackDirectMessageUrl` {#slackDirectMessageUrl}
|
|
106
94
|
|
|
107
95
|
- Type: `string`
|
|
@@ -328,7 +328,7 @@ Read more about the [service Resources page](/docs/development/guides/resources/
|
|
|
328
328
|
|
|
329
329
|
The MCP server has a new `showResource` tool. With `view: "architecture"` it returns a resource's diagram, built the same way as its Diagram page. With `view: "schema"` it shows a message's schema. In MCP clients that support MCP Apps (for example Claude, ChatGPT and VS Code) these open as interactive views. Other clients get Mermaid or the schema code.
|
|
330
330
|
|
|
331
|
-
Every MCP tool now has a title and is read-only, versions are optional
|
|
331
|
+
Every MCP tool now has a title and is read-only, and versions are optional. Read more in [interactive diagrams and schemas](/docs/development/ask-your-architecture/mcp-server/getting-started#interactive-diagrams-and-schemas).
|
|
332
332
|
|
|
333
333
|
### Settings > MCP lists domain MCP servers
|
|
334
334
|
|
package/dist/eventcatalog.cjs
CHANGED
|
@@ -174,7 +174,7 @@ var verifyRequiredFieldsAreInCatalogConfigFile = async (projectDirectory) => {
|
|
|
174
174
|
var import_picocolors = __toESM(require("picocolors"), 1);
|
|
175
175
|
|
|
176
176
|
// package.json
|
|
177
|
-
var version = "5.
|
|
177
|
+
var version = "5.4.0";
|
|
178
178
|
|
|
179
179
|
// src/constants.ts
|
|
180
180
|
var VERSION = version;
|
|
@@ -377,6 +377,14 @@ var CUSTOM_ROUTE_PATTERNS = {
|
|
|
377
377
|
};
|
|
378
378
|
var DEFAULT_IGNORES = ["**/versioned/**", "**/dist/**", "**/node_modules/**"];
|
|
379
379
|
var CUSTOM_ROUTE_IGNORES = [...DEFAULT_IGNORES, "pages/**/_*/**", "pages/**/_*"];
|
|
380
|
+
async function countFitnessFunctions(projectDir) {
|
|
381
|
+
try {
|
|
382
|
+
const { discoverFitnessFiles } = await import("@eventcatalog/fitness");
|
|
383
|
+
return discoverFitnessFiles(projectDir).files.length;
|
|
384
|
+
} catch {
|
|
385
|
+
return void 0;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
380
388
|
async function countResources(projectDir) {
|
|
381
389
|
const counts = {};
|
|
382
390
|
for (const [type, patterns] of Object.entries({ ...RESOURCE_PATTERNS, ...CUSTOM_ROUTE_PATTERNS })) {
|
|
@@ -390,6 +398,8 @@ async function countResources(projectDir) {
|
|
|
390
398
|
}
|
|
391
399
|
counts[type] = total;
|
|
392
400
|
}
|
|
401
|
+
const fitnessFunctions = await countFitnessFunctions(projectDir);
|
|
402
|
+
if (fitnessFunctions !== void 0) counts.fitnessFunctions = fitnessFunctions;
|
|
393
403
|
return counts;
|
|
394
404
|
}
|
|
395
405
|
async function hashCatalogContent(projectDir) {
|
|
@@ -1714,7 +1724,7 @@ var deriveRecordFromPath = async ({
|
|
|
1714
1724
|
const segments = normalizedRelativePath.split("/").filter(Boolean);
|
|
1715
1725
|
const fileName = segments[segments.length - 1] || "";
|
|
1716
1726
|
const fileNameWithoutExtension = removeExtension(fileName);
|
|
1717
|
-
if (data.
|
|
1727
|
+
if (data.draft) {
|
|
1718
1728
|
return null;
|
|
1719
1729
|
}
|
|
1720
1730
|
if (segments[0] === "docs") {
|
|
@@ -3622,6 +3632,20 @@ program.command("generate [siteDir]").description("Start the generator scripts."
|
|
|
3622
3632
|
}
|
|
3623
3633
|
await generate(dir);
|
|
3624
3634
|
});
|
|
3635
|
+
var fitness = program.command("fitness").description("Run the architecture fitness functions in your catalog.");
|
|
3636
|
+
fitness.command("check").description("Run every *.fitness.ts function in the catalog, print the results and write them to a JSON report.").option("-o, --output <path>", "Where to write the report, relative to the catalog", ".eventcatalog/fitness/results.json").option("--format <format>", "Output format: text or json", "text").action(async (commandOptions) => {
|
|
3637
|
+
if (import_fs.default.existsSync(import_node_path23.default.join(dir, ".env"))) {
|
|
3638
|
+
import_dotenv.default.config({ path: import_node_path23.default.join(dir, ".env") });
|
|
3639
|
+
}
|
|
3640
|
+
const { runFitnessCheck } = await import("@eventcatalog/fitness");
|
|
3641
|
+
process.exitCode = await runFitnessCheck({
|
|
3642
|
+
dir,
|
|
3643
|
+
output: commandOptions.output,
|
|
3644
|
+
format: commandOptions.format,
|
|
3645
|
+
color: Boolean(process.stdout.isTTY) && !process.env.NO_COLOR
|
|
3646
|
+
});
|
|
3647
|
+
await new Promise((resolve2) => process.stdout.write("", () => resolve2()));
|
|
3648
|
+
});
|
|
3625
3649
|
var reportFederationProgress = (event, verbose = false) => {
|
|
3626
3650
|
switch (event.type) {
|
|
3627
3651
|
case "configured":
|
package/dist/eventcatalog.js
CHANGED
|
@@ -1,36 +1,34 @@
|
|
|
1
|
-
import {
|
|
2
|
-
stopChildWithParent
|
|
3
|
-
} from "./chunk-S5GWAUG5.js";
|
|
4
1
|
import {
|
|
5
2
|
runMigrations
|
|
6
3
|
} from "./chunk-XUAF2H54.js";
|
|
7
4
|
import "./chunk-CA4U2JP7.js";
|
|
5
|
+
import {
|
|
6
|
+
stopChildWithParent
|
|
7
|
+
} from "./chunk-S5GWAUG5.js";
|
|
8
8
|
import {
|
|
9
9
|
log_build_default
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-EPDVXHC7.js";
|
|
11
11
|
import {
|
|
12
12
|
getLicenseStatus,
|
|
13
13
|
printLicenseStatus
|
|
14
14
|
} from "./chunk-AD7CL772.js";
|
|
15
|
-
import "./chunk-
|
|
16
|
-
import "./chunk-ZAZHAVKB.js";
|
|
15
|
+
import "./chunk-YRZK7HKX.js";
|
|
17
16
|
import "./chunk-CA6WCQ2T.js";
|
|
18
|
-
import "./chunk-CPW6G62V.js";
|
|
19
17
|
import {
|
|
20
18
|
FederationConflictError,
|
|
21
19
|
FederationDiagnosticError,
|
|
22
20
|
federateCatalog
|
|
23
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-OX6TSVHY.js";
|
|
24
22
|
import "./chunk-WRNH5C3U.js";
|
|
25
23
|
import "./chunk-5EPNFDT5.js";
|
|
26
|
-
import "./chunk-
|
|
27
|
-
import "./chunk-SDZQJTJW.js";
|
|
24
|
+
import "./chunk-6K7XZAYI.js";
|
|
28
25
|
import "./chunk-352FGP6W.js";
|
|
29
26
|
import "./chunk-R7Z5ALYI.js";
|
|
30
27
|
import {
|
|
31
28
|
getFederationDiagnosticCounts,
|
|
32
29
|
getVisibleFederationDiagnostics
|
|
33
30
|
} from "./chunk-JJPB6EOZ.js";
|
|
31
|
+
import "./chunk-SDZQJTJW.js";
|
|
34
32
|
import {
|
|
35
33
|
getProjectOutDir,
|
|
36
34
|
isAuthEnabled,
|
|
@@ -39,16 +37,16 @@ import {
|
|
|
39
37
|
} from "./chunk-NKNOXQ62.js";
|
|
40
38
|
import {
|
|
41
39
|
generate
|
|
42
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-TERJXCLK.js";
|
|
43
41
|
import {
|
|
44
42
|
logger
|
|
45
|
-
} from "./chunk-
|
|
43
|
+
} from "./chunk-O2K4VPDO.js";
|
|
46
44
|
import {
|
|
47
45
|
resolve_catalog_dependencies_default
|
|
48
46
|
} from "./chunk-PD7KTSYN.js";
|
|
49
47
|
import {
|
|
50
48
|
buildSearchIndex
|
|
51
|
-
} from "./chunk-
|
|
49
|
+
} from "./chunk-7RJN4FNG.js";
|
|
52
50
|
import {
|
|
53
51
|
getEventCatalogUpdateMessage,
|
|
54
52
|
resolveInstalledCoreVersion
|
|
@@ -56,6 +54,8 @@ import {
|
|
|
56
54
|
import {
|
|
57
55
|
watch
|
|
58
56
|
} from "./chunk-U63GDPPY.js";
|
|
57
|
+
import "./chunk-PAS36QHK.js";
|
|
58
|
+
import "./chunk-CPW6G62V.js";
|
|
59
59
|
import {
|
|
60
60
|
hasCustomExports,
|
|
61
61
|
isCatalogAuthEnabled,
|
|
@@ -79,7 +79,7 @@ import {
|
|
|
79
79
|
} from "./chunk-LFQJXLHG.js";
|
|
80
80
|
import {
|
|
81
81
|
VERSION
|
|
82
|
-
} from "./chunk-
|
|
82
|
+
} from "./chunk-OT2U5OES.js";
|
|
83
83
|
import {
|
|
84
84
|
getEventCatalogConfigFile,
|
|
85
85
|
verifyRequiredFieldsAreInCatalogConfigFile
|
|
@@ -1191,6 +1191,20 @@ program.command("generate [siteDir]").description("Start the generator scripts."
|
|
|
1191
1191
|
}
|
|
1192
1192
|
await generate(dir);
|
|
1193
1193
|
});
|
|
1194
|
+
var fitness = program.command("fitness").description("Run the architecture fitness functions in your catalog.");
|
|
1195
|
+
fitness.command("check").description("Run every *.fitness.ts function in the catalog, print the results and write them to a JSON report.").option("-o, --output <path>", "Where to write the report, relative to the catalog", ".eventcatalog/fitness/results.json").option("--format <format>", "Output format: text or json", "text").action(async (commandOptions) => {
|
|
1196
|
+
if (fs3.existsSync(path2.join(dir, ".env"))) {
|
|
1197
|
+
dotenv.config({ path: path2.join(dir, ".env") });
|
|
1198
|
+
}
|
|
1199
|
+
const { runFitnessCheck } = await import("@eventcatalog/fitness");
|
|
1200
|
+
process.exitCode = await runFitnessCheck({
|
|
1201
|
+
dir,
|
|
1202
|
+
output: commandOptions.output,
|
|
1203
|
+
format: commandOptions.format,
|
|
1204
|
+
color: Boolean(process.stdout.isTTY) && !process.env.NO_COLOR
|
|
1205
|
+
});
|
|
1206
|
+
await new Promise((resolve) => process.stdout.write("", () => resolve()));
|
|
1207
|
+
});
|
|
1194
1208
|
var reportFederationProgress = (event, verbose = false) => {
|
|
1195
1209
|
switch (event.type) {
|
|
1196
1210
|
case "configured":
|
|
@@ -2,14 +2,14 @@ import {
|
|
|
2
2
|
FederationConflictError,
|
|
3
3
|
FederationDiagnosticError,
|
|
4
4
|
federateCatalog
|
|
5
|
-
} from "../chunk-
|
|
5
|
+
} from "../chunk-OX6TSVHY.js";
|
|
6
6
|
import "../chunk-WRNH5C3U.js";
|
|
7
7
|
import "../chunk-5EPNFDT5.js";
|
|
8
|
-
import "../chunk-
|
|
9
|
-
import "../chunk-SDZQJTJW.js";
|
|
8
|
+
import "../chunk-6K7XZAYI.js";
|
|
10
9
|
import "../chunk-352FGP6W.js";
|
|
11
10
|
import "../chunk-R7Z5ALYI.js";
|
|
12
11
|
import "../chunk-JJPB6EOZ.js";
|
|
12
|
+
import "../chunk-SDZQJTJW.js";
|
|
13
13
|
import "../chunk-4G7FSRPN.js";
|
|
14
14
|
export {
|
|
15
15
|
FederationConflictError,
|
package/dist/generate.cjs
CHANGED
|
@@ -105,7 +105,7 @@ var getEventCatalogConfigFile = async (projectDirectory) => {
|
|
|
105
105
|
var import_picocolors = __toESM(require("picocolors"), 1);
|
|
106
106
|
|
|
107
107
|
// package.json
|
|
108
|
-
var version = "5.
|
|
108
|
+
var version = "5.4.0";
|
|
109
109
|
|
|
110
110
|
// src/constants.ts
|
|
111
111
|
var VERSION = version;
|
package/dist/generate.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
generate
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
5
|
-
import "./chunk-
|
|
3
|
+
} from "./chunk-TERJXCLK.js";
|
|
4
|
+
import "./chunk-O2K4VPDO.js";
|
|
5
|
+
import "./chunk-OT2U5OES.js";
|
|
6
6
|
import "./chunk-4G7FSRPN.js";
|
|
7
7
|
export {
|
|
8
8
|
generate
|