aact 2.1.4 → 3.0.0-beta.2
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 +50 -18
- package/dist/chunks/index.mjs +422 -0
- package/dist/chunks/index2.mjs +248 -0
- package/dist/chunks/index3.mjs +72 -0
- package/dist/cli/index.mjs +312 -542
- package/dist/index.d.mts +520 -229
- package/dist/index.d.ts +520 -229
- package/dist/index.mjs +5 -89
- package/dist/shared/aact.BpV1UCZJ.mjs +3 -0
- package/dist/shared/aact.CfImn7en.mjs +138 -0
- package/dist/shared/aact.CxegP3pU.mjs +900 -0
- package/package.json +44 -14
- package/dist/shared/aact.BzhD7c9t.mjs +0 -884
|
@@ -1,884 +0,0 @@
|
|
|
1
|
-
import * as v from 'valibot';
|
|
2
|
-
import YAML from 'yaml';
|
|
3
|
-
import fs from 'node:fs/promises';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import { Comment, Stdlib_C4_Dynamic_Rel, Relationship, parse, Stdlib_C4_Container_Component, Stdlib_C4_Context, Stdlib_C4_Boundary } from 'plantuml-parser';
|
|
6
|
-
|
|
7
|
-
const EXTERNAL_SYSTEM_TYPE = "System_Ext";
|
|
8
|
-
const SYSTEM_TYPE = "System";
|
|
9
|
-
const CONTAINER_TYPE = "Container";
|
|
10
|
-
const CONTAINER_DB_TYPE = "ContainerDb";
|
|
11
|
-
const COMPONENT_TYPE = "Component";
|
|
12
|
-
const BOUNDARY_TYPE = "Boundary";
|
|
13
|
-
const SYSTEM_BOUNDARY_TYPE = "System_Boundary";
|
|
14
|
-
const CONTAINER_BOUNDARY_TYPE = "Container_Boundary";
|
|
15
|
-
const PERSON_TYPE = "Person";
|
|
16
|
-
|
|
17
|
-
const DEFAULT_API_TECHNOLOGIES = ["http", "grpc", "tcp"];
|
|
18
|
-
const allRelations = (model) => model.allContainers.flatMap(
|
|
19
|
-
(container) => container.relations.map((relation) => ({ from: container, relation }))
|
|
20
|
-
);
|
|
21
|
-
const classifyRelation = (names, childNames, parentBoundary, from, relation, result, parentResult) => {
|
|
22
|
-
if (!names.has(from.name)) return;
|
|
23
|
-
if (names.has(relation.to.name)) {
|
|
24
|
-
result.cohesion++;
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
const isInParentSibling = childNames?.has(relation.to.name) ?? false;
|
|
28
|
-
if (!parentBoundary || isInParentSibling) {
|
|
29
|
-
result.coupling++;
|
|
30
|
-
result.couplingRelations.push({ from: from.name, to: relation.to.name });
|
|
31
|
-
if (parentResult) parentResult.cohesion++;
|
|
32
|
-
} else if (parentResult) {
|
|
33
|
-
parentResult.coupling++;
|
|
34
|
-
parentResult.couplingRelations.push({
|
|
35
|
-
from: from.name,
|
|
36
|
-
to: relation.to.name
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
|
-
const buildBoundaryLookups = (boundaries) => {
|
|
41
|
-
const nameSets = new Map(
|
|
42
|
-
boundaries.map((b) => [b.name, new Set(b.containers.map((c) => c.name))])
|
|
43
|
-
);
|
|
44
|
-
const parentMap = /* @__PURE__ */ new Map();
|
|
45
|
-
for (const b of boundaries) {
|
|
46
|
-
for (const child of b.boundaries) parentMap.set(child.name, b);
|
|
47
|
-
}
|
|
48
|
-
const result = /* @__PURE__ */ new Map();
|
|
49
|
-
for (const b of boundaries) {
|
|
50
|
-
const parentBoundary = parentMap.get(b.name);
|
|
51
|
-
let childNames;
|
|
52
|
-
if (parentBoundary) {
|
|
53
|
-
childNames = /* @__PURE__ */ new Set();
|
|
54
|
-
for (const sibling of parentBoundary.boundaries) {
|
|
55
|
-
for (const c of sibling.containers) childNames.add(c.name);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
result.set(b.name, {
|
|
59
|
-
nameSet: nameSets.get(b.name),
|
|
60
|
-
childNames,
|
|
61
|
-
parentBoundary
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
return result;
|
|
65
|
-
};
|
|
66
|
-
const isSyncApiCall = (it, externalType, apiTechnologies) => {
|
|
67
|
-
if (it.relation.tags?.includes("async")) return false;
|
|
68
|
-
if (it.relation.to.type === externalType) return true;
|
|
69
|
-
return apiTechnologies.some(
|
|
70
|
-
(t) => (it.relation.technology ?? "").toLowerCase().includes(t)
|
|
71
|
-
);
|
|
72
|
-
};
|
|
73
|
-
const analyzeModel = (model, options) => {
|
|
74
|
-
const apiTechnologies = options?.apiTechnologies ?? DEFAULT_API_TECHNOLOGIES;
|
|
75
|
-
const externalType = options?.externalType ?? EXTERNAL_SYSTEM_TYPE;
|
|
76
|
-
const dbType = options?.dbType ?? CONTAINER_DB_TYPE;
|
|
77
|
-
const relations = allRelations(model);
|
|
78
|
-
const asyncApiCalls = relations.filter(
|
|
79
|
-
(it) => it.relation.tags?.includes("async")
|
|
80
|
-
);
|
|
81
|
-
const syncApiCalls = relations.filter(
|
|
82
|
-
(it) => isSyncApiCall(it, externalType, apiTechnologies)
|
|
83
|
-
);
|
|
84
|
-
const lookups = buildBoundaryLookups(model.boundaries);
|
|
85
|
-
const boundaryResults = /* @__PURE__ */ new Map();
|
|
86
|
-
for (const boundary of model.boundaries) {
|
|
87
|
-
boundaryResults.set(boundary.name, {
|
|
88
|
-
name: boundary.name,
|
|
89
|
-
label: boundary.label,
|
|
90
|
-
cohesion: 0,
|
|
91
|
-
coupling: 0,
|
|
92
|
-
couplingRelations: []
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
for (const boundary of model.boundaries) {
|
|
96
|
-
const { nameSet, childNames, parentBoundary } = lookups.get(boundary.name);
|
|
97
|
-
const result = boundaryResults.get(boundary.name);
|
|
98
|
-
const parentResult = parentBoundary ? boundaryResults.get(parentBoundary.name) : void 0;
|
|
99
|
-
for (const { from, relation } of relations) {
|
|
100
|
-
classifyRelation(
|
|
101
|
-
nameSet,
|
|
102
|
-
childNames,
|
|
103
|
-
parentBoundary,
|
|
104
|
-
from,
|
|
105
|
-
relation,
|
|
106
|
-
result,
|
|
107
|
-
parentResult
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
return {
|
|
112
|
-
elementsCount: model.allContainers.length,
|
|
113
|
-
syncApiCalls: syncApiCalls.length,
|
|
114
|
-
asyncApiCalls: asyncApiCalls.length,
|
|
115
|
-
databases: analyzeDatabases(model, dbType),
|
|
116
|
-
boundaries: [...boundaryResults.values()]
|
|
117
|
-
};
|
|
118
|
-
};
|
|
119
|
-
const analyzeDatabases = (model, dbType) => {
|
|
120
|
-
const dbNames = new Set(
|
|
121
|
-
model.allContainers.filter((it) => it.type === dbType).map((it) => it.name)
|
|
122
|
-
);
|
|
123
|
-
let consumes = 0;
|
|
124
|
-
for (const container of model.allContainers) {
|
|
125
|
-
for (const r of container.relations) {
|
|
126
|
-
if (dbNames.has(r.to.name)) consumes++;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
return {
|
|
130
|
-
count: dbNames.size,
|
|
131
|
-
consumes
|
|
132
|
-
};
|
|
133
|
-
};
|
|
134
|
-
const analyzeArchitecture = (model, options) => {
|
|
135
|
-
return {
|
|
136
|
-
model,
|
|
137
|
-
report: analyzeModel(model, options)
|
|
138
|
-
};
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
const ruleOption = (entries) => v.optional(v.union([v.boolean(), v.strictObject(entries)]));
|
|
142
|
-
const AactConfigSchema = v.strictObject({
|
|
143
|
-
source: v.strictObject({
|
|
144
|
-
type: v.picklist(["plantuml", "structurizr"]),
|
|
145
|
-
path: v.string(),
|
|
146
|
-
writePath: v.optional(v.string())
|
|
147
|
-
}),
|
|
148
|
-
rules: v.optional(
|
|
149
|
-
v.strictObject({
|
|
150
|
-
acl: ruleOption({
|
|
151
|
-
tag: v.optional(v.string()),
|
|
152
|
-
externalType: v.optional(v.string())
|
|
153
|
-
}),
|
|
154
|
-
acyclic: v.optional(v.boolean()),
|
|
155
|
-
apiGateway: ruleOption({
|
|
156
|
-
aclTag: v.optional(v.string()),
|
|
157
|
-
externalType: v.optional(v.string()),
|
|
158
|
-
gatewayPattern: v.optional(v.instance(RegExp))
|
|
159
|
-
}),
|
|
160
|
-
crud: ruleOption({
|
|
161
|
-
repoTags: v.optional(v.array(v.string())),
|
|
162
|
-
dbType: v.optional(v.string())
|
|
163
|
-
}),
|
|
164
|
-
dbPerService: ruleOption({
|
|
165
|
-
dbType: v.optional(v.string()),
|
|
166
|
-
ownerTags: v.optional(v.array(v.string()))
|
|
167
|
-
}),
|
|
168
|
-
cohesion: ruleOption({
|
|
169
|
-
externalType: v.optional(v.string()),
|
|
170
|
-
internalType: v.optional(v.string())
|
|
171
|
-
}),
|
|
172
|
-
stableDependencies: ruleOption({
|
|
173
|
-
externalType: v.optional(v.string())
|
|
174
|
-
}),
|
|
175
|
-
commonReuse: v.optional(v.boolean())
|
|
176
|
-
})
|
|
177
|
-
),
|
|
178
|
-
generate: v.optional(
|
|
179
|
-
v.strictObject({
|
|
180
|
-
kubernetes: v.optional(
|
|
181
|
-
v.strictObject({
|
|
182
|
-
path: v.optional(v.string()),
|
|
183
|
-
exclude: v.optional(v.array(v.string()))
|
|
184
|
-
})
|
|
185
|
-
),
|
|
186
|
-
boundaryLabel: v.optional(v.string())
|
|
187
|
-
})
|
|
188
|
-
)
|
|
189
|
-
});
|
|
190
|
-
const defineConfig = (config) => config;
|
|
191
|
-
|
|
192
|
-
const toKebab = (name) => name.replaceAll("_", "-");
|
|
193
|
-
const toEnvKey = (name) => name.replaceAll("-", "_").toUpperCase();
|
|
194
|
-
const buildEnvVar = (relation, sourceKebab, options) => {
|
|
195
|
-
const targetType = relation.to.type;
|
|
196
|
-
const targetKebab = toKebab(relation.to.name);
|
|
197
|
-
const targetUpper = toEnvKey(targetKebab);
|
|
198
|
-
if (targetType === CONTAINER_DB_TYPE) {
|
|
199
|
-
const value = options.dbConnectionTemplate.replaceAll(
|
|
200
|
-
"{name}",
|
|
201
|
-
sourceKebab
|
|
202
|
-
);
|
|
203
|
-
return { key: "PG_CONNECTION_STRING", value };
|
|
204
|
-
}
|
|
205
|
-
if (relation.tags?.includes("async")) {
|
|
206
|
-
const value = relation.technology ?? targetKebab;
|
|
207
|
-
return { key: `KAFKA_${targetUpper}_TOPIC`, value };
|
|
208
|
-
}
|
|
209
|
-
if (targetType === EXTERNAL_SYSTEM_TYPE) {
|
|
210
|
-
const value = relation.technology ?? `https://${targetKebab}`;
|
|
211
|
-
return { key: `${targetUpper}_BASE_URL`, value };
|
|
212
|
-
}
|
|
213
|
-
if (targetType === CONTAINER_TYPE) {
|
|
214
|
-
const value = relation.technology ?? `http://${targetKebab}:${options.defaultPort}`;
|
|
215
|
-
return { key: `${targetUpper}_BASE_URL`, value };
|
|
216
|
-
}
|
|
217
|
-
return void 0;
|
|
218
|
-
};
|
|
219
|
-
const generateKubernetes = (model, options) => {
|
|
220
|
-
const defaultPort = options?.defaultPort ?? 8080;
|
|
221
|
-
const dbConnectionTemplate = options?.dbConnectionTemplate ?? "postgresql://{name}:pass-{name}@postgresql:5432/{name}";
|
|
222
|
-
const resolvedOptions = { defaultPort, dbConnectionTemplate };
|
|
223
|
-
const containers = model.allContainers.filter(
|
|
224
|
-
(c) => c.type !== CONTAINER_DB_TYPE && c.type !== EXTERNAL_SYSTEM_TYPE && c.type !== PERSON_TYPE
|
|
225
|
-
);
|
|
226
|
-
return containers.map((container) => {
|
|
227
|
-
const kebabName = toKebab(container.name);
|
|
228
|
-
const envEntries = [];
|
|
229
|
-
for (const relation of container.relations) {
|
|
230
|
-
const entry = buildEnvVar(relation, kebabName, resolvedOptions);
|
|
231
|
-
if (entry) {
|
|
232
|
-
envEntries.push(entry);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
envEntries.sort((a, b) => a.key.localeCompare(b.key));
|
|
236
|
-
const doc = { name: kebabName };
|
|
237
|
-
if (envEntries.length > 0) {
|
|
238
|
-
const environment = {};
|
|
239
|
-
for (const { key, value } of envEntries) {
|
|
240
|
-
environment[key] = { default: value };
|
|
241
|
-
}
|
|
242
|
-
doc.environment = environment;
|
|
243
|
-
}
|
|
244
|
-
return {
|
|
245
|
-
fileName: `${kebabName}.yml`,
|
|
246
|
-
content: YAML.stringify(doc)
|
|
247
|
-
};
|
|
248
|
-
});
|
|
249
|
-
};
|
|
250
|
-
|
|
251
|
-
const PLANTUML_CONTAINER = "Container";
|
|
252
|
-
const PLANTUML_CONTAINER_DB = "ContainerDb";
|
|
253
|
-
const PLANTUML_SYSTEM_EXT = "System_Ext";
|
|
254
|
-
const PLANTUML_SYSTEM = "System";
|
|
255
|
-
const PLANTUML_PERSON = "Person";
|
|
256
|
-
const PLANTUML_COMPONENT = "Component";
|
|
257
|
-
const PLANTUML_SYSTEM_BOUNDARY = "System_Boundary";
|
|
258
|
-
const PLANTUML_CONTAINER_BOUNDARY = "Container_Boundary";
|
|
259
|
-
const PLANTUML_BOUNDARY = "Boundary";
|
|
260
|
-
|
|
261
|
-
const containerTypeMap = {
|
|
262
|
-
[CONTAINER_TYPE]: PLANTUML_CONTAINER,
|
|
263
|
-
[CONTAINER_DB_TYPE]: PLANTUML_CONTAINER_DB,
|
|
264
|
-
[EXTERNAL_SYSTEM_TYPE]: PLANTUML_SYSTEM_EXT,
|
|
265
|
-
[PERSON_TYPE]: PLANTUML_PERSON
|
|
266
|
-
};
|
|
267
|
-
const renderContainer = (container) => {
|
|
268
|
-
const type = containerTypeMap[container.type ?? CONTAINER_TYPE] ?? PLANTUML_CONTAINER;
|
|
269
|
-
const tags = container.tags && container.tags.length > 0 ? `, $tags="${container.tags.join("+")}"` : "";
|
|
270
|
-
const desc = container.description ? `, "${container.description}"` : "";
|
|
271
|
-
return `${type}(${container.name}, "${container.label}"${desc}${tags})`;
|
|
272
|
-
};
|
|
273
|
-
const renderBoundary = (boundary, indent) => {
|
|
274
|
-
const inner = indent + " ";
|
|
275
|
-
const children = [
|
|
276
|
-
...boundary.boundaries.map((child) => renderBoundary(child, inner)),
|
|
277
|
-
...boundary.containers.map(
|
|
278
|
-
(container) => `${inner}${renderContainer(container)}`
|
|
279
|
-
)
|
|
280
|
-
];
|
|
281
|
-
return [
|
|
282
|
-
`${indent}Boundary(${boundary.name}, "${boundary.label}") {`,
|
|
283
|
-
...children,
|
|
284
|
-
`${indent}}`
|
|
285
|
-
].join("\n");
|
|
286
|
-
};
|
|
287
|
-
const renderRelation = (container, relation) => {
|
|
288
|
-
const tech = relation.technology ? `, "${relation.technology}"` : "";
|
|
289
|
-
const tags = relation.tags && relation.tags.length > 0 ? `, $tags="${relation.tags.join("+")}"` : "";
|
|
290
|
-
return `Rel(${container.name}, ${relation.to.name}, ""${tech}${tags})`;
|
|
291
|
-
};
|
|
292
|
-
const collectBoundaryContainerNames = (boundaries) => {
|
|
293
|
-
const names = /* @__PURE__ */ new Set();
|
|
294
|
-
const collect = (boundary) => {
|
|
295
|
-
for (const c of boundary.containers) names.add(c.name);
|
|
296
|
-
for (const b of boundary.boundaries) collect(b);
|
|
297
|
-
};
|
|
298
|
-
for (const boundary of boundaries) collect(boundary);
|
|
299
|
-
return names;
|
|
300
|
-
};
|
|
301
|
-
const renderBody = (model, standaloneContainers, boundaryLabel) => {
|
|
302
|
-
if (boundaryLabel) {
|
|
303
|
-
return [
|
|
304
|
-
`Boundary(project, "${boundaryLabel}") {`,
|
|
305
|
-
...model.boundaries.map((b) => renderBoundary(b, " ")),
|
|
306
|
-
...standaloneContainers.map((c) => ` ${renderContainer(c)}`),
|
|
307
|
-
`}`
|
|
308
|
-
];
|
|
309
|
-
}
|
|
310
|
-
return [
|
|
311
|
-
...model.boundaries.map((b) => renderBoundary(b, "")),
|
|
312
|
-
...standaloneContainers.map((c) => renderContainer(c))
|
|
313
|
-
];
|
|
314
|
-
};
|
|
315
|
-
const generatePlantumlFromModel = (model, options) => {
|
|
316
|
-
const boundaryNames = collectBoundaryContainerNames(model.boundaries);
|
|
317
|
-
const standaloneContainers = model.allContainers.filter(
|
|
318
|
-
(c) => !boundaryNames.has(c.name)
|
|
319
|
-
);
|
|
320
|
-
const relations = model.allContainers.flatMap(
|
|
321
|
-
(container) => container.relations.map((rel) => renderRelation(container, rel))
|
|
322
|
-
);
|
|
323
|
-
return [
|
|
324
|
-
`@startuml`,
|
|
325
|
-
`!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml`,
|
|
326
|
-
`LAYOUT_WITH_LEGEND()`,
|
|
327
|
-
`AddRelTag("async", $lineStyle = DottedLine())`,
|
|
328
|
-
"",
|
|
329
|
-
...renderBody(model, standaloneContainers, options?.boundaryLabel),
|
|
330
|
-
"",
|
|
331
|
-
...relations,
|
|
332
|
-
"@enduml"
|
|
333
|
-
].join("\n");
|
|
334
|
-
};
|
|
335
|
-
|
|
336
|
-
const filterElements = (elements) => {
|
|
337
|
-
const result = [];
|
|
338
|
-
for (const element of elements) {
|
|
339
|
-
if (element instanceof Comment) continue;
|
|
340
|
-
if (element.type_.name === PLANTUML_CONTAINER || element.type_.name === PLANTUML_CONTAINER_DB || element.type_.name === PLANTUML_COMPONENT || element.type_.name === PLANTUML_SYSTEM_EXT || element.type_.name === PLANTUML_SYSTEM || element.type_.name === PLANTUML_PERSON || element instanceof Stdlib_C4_Dynamic_Rel || element instanceof Relationship) {
|
|
341
|
-
result.push(element);
|
|
342
|
-
}
|
|
343
|
-
const elementAsBoundary = element;
|
|
344
|
-
if ([
|
|
345
|
-
PLANTUML_SYSTEM_BOUNDARY,
|
|
346
|
-
PLANTUML_CONTAINER_BOUNDARY,
|
|
347
|
-
PLANTUML_BOUNDARY
|
|
348
|
-
].includes(elementAsBoundary.type_.name)) {
|
|
349
|
-
result.push(elementAsBoundary);
|
|
350
|
-
const resultFromBoundary = filterElements(elementAsBoundary.elements);
|
|
351
|
-
result.push(...resultFromBoundary);
|
|
352
|
-
}
|
|
353
|
-
if (Array.isArray(element)) {
|
|
354
|
-
const resultFromArray = filterElements(element);
|
|
355
|
-
result.push(...resultFromArray);
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
return result;
|
|
359
|
-
};
|
|
360
|
-
|
|
361
|
-
const loadPlantumlElements = async (filePath) => {
|
|
362
|
-
const filepath = path.resolve(filePath);
|
|
363
|
-
let data = await fs.readFile(filepath, "utf8");
|
|
364
|
-
data = data.replaceAll(/, \$tags=(".+?")/g, ", $1").replaceAll('""', '" "');
|
|
365
|
-
const [{ elements }] = parse(data);
|
|
366
|
-
for (const element of elements) {
|
|
367
|
-
if (element instanceof Comment) continue;
|
|
368
|
-
const relation = element;
|
|
369
|
-
if (relation.type_.name.startsWith("Rel_Back")) {
|
|
370
|
-
const from = relation.from;
|
|
371
|
-
relation.from = relation.to;
|
|
372
|
-
relation.to = from;
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
return filterElements(elements);
|
|
376
|
-
};
|
|
377
|
-
|
|
378
|
-
const addDependency = (containers, relation) => {
|
|
379
|
-
const containerFrom = containers.find((x) => x.name === relation.from);
|
|
380
|
-
if (!containerFrom) return;
|
|
381
|
-
const containerTo = containers.find((x) => x.name === relation.to);
|
|
382
|
-
if (!containerTo) return;
|
|
383
|
-
containerFrom.relations.push({
|
|
384
|
-
to: containerTo,
|
|
385
|
-
technology: relation.techn,
|
|
386
|
-
tags: relation.descr?.split(",").map((t) => t.trim())
|
|
387
|
-
});
|
|
388
|
-
};
|
|
389
|
-
const mapContainersFromPlantumlElements = (elements) => {
|
|
390
|
-
const containers = elements.filter(
|
|
391
|
-
(element) => element instanceof Stdlib_C4_Container_Component || element instanceof Stdlib_C4_Context
|
|
392
|
-
).map((element) => {
|
|
393
|
-
const component = element;
|
|
394
|
-
return {
|
|
395
|
-
name: component.alias,
|
|
396
|
-
label: component.label,
|
|
397
|
-
type: component.type_.name,
|
|
398
|
-
relations: [],
|
|
399
|
-
tags: component.sprite ? [component.sprite] : void 0,
|
|
400
|
-
description: component.descr
|
|
401
|
-
};
|
|
402
|
-
});
|
|
403
|
-
for (const element of elements) {
|
|
404
|
-
if (element instanceof Stdlib_C4_Container_Component) {
|
|
405
|
-
continue;
|
|
406
|
-
}
|
|
407
|
-
if (element instanceof Stdlib_C4_Dynamic_Rel) {
|
|
408
|
-
addDependency(containers, element);
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
const boundaries = elements.filter((element) => element instanceof Stdlib_C4_Boundary).map((element) => {
|
|
412
|
-
const component = element;
|
|
413
|
-
return {
|
|
414
|
-
name: component.alias,
|
|
415
|
-
label: component.label,
|
|
416
|
-
type: component.type_.name,
|
|
417
|
-
boundaries: [],
|
|
418
|
-
containers: containers.filter(
|
|
419
|
-
(container) => component.elements.filter(
|
|
420
|
-
(element2) => element2 instanceof Stdlib_C4_Container_Component
|
|
421
|
-
).some((e) => e.alias == container.name)
|
|
422
|
-
)
|
|
423
|
-
};
|
|
424
|
-
});
|
|
425
|
-
for (const boundary of boundaries) {
|
|
426
|
-
const component = elements.find(
|
|
427
|
-
(element) => element instanceof Stdlib_C4_Boundary && element.alias == boundary.name
|
|
428
|
-
);
|
|
429
|
-
boundary.boundaries = boundaries.filter(
|
|
430
|
-
(b) => component.elements.filter((element) => element instanceof Stdlib_C4_Boundary).some((e) => e.alias == b.name)
|
|
431
|
-
);
|
|
432
|
-
}
|
|
433
|
-
return {
|
|
434
|
-
allContainers: containers.toSorted((a, b) => a.name.localeCompare(b.name)),
|
|
435
|
-
boundaries
|
|
436
|
-
};
|
|
437
|
-
};
|
|
438
|
-
|
|
439
|
-
const STRUCTURIZR_LOCATION_EXTERNAL = "External";
|
|
440
|
-
const STRUCTURIZR_INTERACTION_ASYNC = "Asynchronous";
|
|
441
|
-
const STRUCTURIZR_TAG_ASYNC = "async";
|
|
442
|
-
|
|
443
|
-
const dslId = (id, properties) => properties?.["structurizr.dsl.identifier"] ?? id;
|
|
444
|
-
const DATABASE_TECHNOLOGIES = [
|
|
445
|
-
"postgresql",
|
|
446
|
-
"postgres",
|
|
447
|
-
"mysql",
|
|
448
|
-
"mariadb",
|
|
449
|
-
"mongodb",
|
|
450
|
-
"mongo",
|
|
451
|
-
"redis",
|
|
452
|
-
"elasticsearch",
|
|
453
|
-
"dynamodb",
|
|
454
|
-
"cassandra",
|
|
455
|
-
"sqlite",
|
|
456
|
-
"oracle",
|
|
457
|
-
"sqlserver",
|
|
458
|
-
"mssql",
|
|
459
|
-
"database",
|
|
460
|
-
"db"
|
|
461
|
-
];
|
|
462
|
-
const isDatabase = (technology, name) => {
|
|
463
|
-
const techLower = technology?.toLowerCase() ?? "";
|
|
464
|
-
const nameLower = name?.toLowerCase() ?? "";
|
|
465
|
-
if (DATABASE_TECHNOLOGIES.some((db) => techLower.includes(db))) {
|
|
466
|
-
return true;
|
|
467
|
-
}
|
|
468
|
-
if (nameLower.endsWith(" db") || nameLower.endsWith("_db") || nameLower.endsWith("database")) {
|
|
469
|
-
return true;
|
|
470
|
-
}
|
|
471
|
-
return false;
|
|
472
|
-
};
|
|
473
|
-
const enrichTags = (existingTags, name) => {
|
|
474
|
-
const tags = existingTags?.split(",").map((t) => t.trim()).filter(Boolean) ?? [];
|
|
475
|
-
const nameLower = name?.toLowerCase() ?? "";
|
|
476
|
-
if (nameLower.includes("crud") && !tags.includes("repo")) {
|
|
477
|
-
tags.push("repo");
|
|
478
|
-
}
|
|
479
|
-
if (nameLower.includes("acl") && !tags.includes("acl")) {
|
|
480
|
-
tags.push("acl");
|
|
481
|
-
}
|
|
482
|
-
return tags;
|
|
483
|
-
};
|
|
484
|
-
const loadStructurizrWorkspace = async (filePath) => {
|
|
485
|
-
const filepath = path.resolve(filePath);
|
|
486
|
-
const data = await fs.readFile(filepath, "utf8");
|
|
487
|
-
return JSON.parse(data);
|
|
488
|
-
};
|
|
489
|
-
const processExternalSystem = (system, registry) => {
|
|
490
|
-
const container = {
|
|
491
|
-
name: dslId(system.id, system.properties),
|
|
492
|
-
label: system.name,
|
|
493
|
-
type: EXTERNAL_SYSTEM_TYPE,
|
|
494
|
-
tags: system.tags?.split(",").map((t) => t.trim()).filter(Boolean),
|
|
495
|
-
description: system.description ?? "",
|
|
496
|
-
relations: []
|
|
497
|
-
};
|
|
498
|
-
registry.containers.push(container);
|
|
499
|
-
registry.allElements.set(system.id, container);
|
|
500
|
-
};
|
|
501
|
-
const processInternalSystem = (system, registry) => {
|
|
502
|
-
const systemContainers = [];
|
|
503
|
-
for (const cont of system.containers ?? []) {
|
|
504
|
-
const container = {
|
|
505
|
-
name: dslId(cont.id, cont.properties),
|
|
506
|
-
label: cont.name,
|
|
507
|
-
type: isDatabase(cont.technology, cont.name) ? CONTAINER_DB_TYPE : CONTAINER_TYPE,
|
|
508
|
-
tags: enrichTags(cont.tags, cont.name),
|
|
509
|
-
description: cont.description ?? "",
|
|
510
|
-
relations: []
|
|
511
|
-
};
|
|
512
|
-
systemContainers.push(container);
|
|
513
|
-
registry.containers.push(container);
|
|
514
|
-
registry.allElements.set(cont.id, container);
|
|
515
|
-
}
|
|
516
|
-
registry.boundaries.push({
|
|
517
|
-
name: dslId(system.id, system.properties),
|
|
518
|
-
label: system.name,
|
|
519
|
-
type: BOUNDARY_TYPE,
|
|
520
|
-
boundaries: [],
|
|
521
|
-
containers: systemContainers
|
|
522
|
-
});
|
|
523
|
-
};
|
|
524
|
-
const addRelations = (allElements, sourceId, relationships) => {
|
|
525
|
-
const sourceContainer = allElements.get(sourceId);
|
|
526
|
-
if (!sourceContainer || !relationships) return;
|
|
527
|
-
for (const rel of relationships) {
|
|
528
|
-
const targetContainer = allElements.get(rel.destinationId);
|
|
529
|
-
if (!targetContainer) continue;
|
|
530
|
-
let tags = rel.tags?.split(",").map((t) => t.trim());
|
|
531
|
-
if (rel.interactionStyle === STRUCTURIZR_INTERACTION_ASYNC) {
|
|
532
|
-
tags = [...tags ?? [], STRUCTURIZR_TAG_ASYNC];
|
|
533
|
-
}
|
|
534
|
-
const relation = {
|
|
535
|
-
to: targetContainer,
|
|
536
|
-
technology: rel.technology ?? (rel.description?.includes(" ") ? void 0 : rel.description),
|
|
537
|
-
tags
|
|
538
|
-
};
|
|
539
|
-
sourceContainer.relations.push(relation);
|
|
540
|
-
}
|
|
541
|
-
};
|
|
542
|
-
const mapContainersFromStructurizr = (workspace) => {
|
|
543
|
-
const registry = {
|
|
544
|
-
allElements: /* @__PURE__ */ new Map(),
|
|
545
|
-
containers: [],
|
|
546
|
-
boundaries: []
|
|
547
|
-
};
|
|
548
|
-
for (const system of workspace.model.softwareSystems ?? []) {
|
|
549
|
-
if (system.location === STRUCTURIZR_LOCATION_EXTERNAL || system.tags?.includes(STRUCTURIZR_LOCATION_EXTERNAL)) {
|
|
550
|
-
processExternalSystem(system, registry);
|
|
551
|
-
} else {
|
|
552
|
-
processInternalSystem(system, registry);
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
for (const person of workspace.model.people ?? []) {
|
|
556
|
-
const container = {
|
|
557
|
-
name: dslId(person.id, person.properties),
|
|
558
|
-
label: person.name,
|
|
559
|
-
type: PERSON_TYPE,
|
|
560
|
-
tags: person.tags?.split(",").map((t) => t.trim()).filter(Boolean),
|
|
561
|
-
description: person.description ?? "",
|
|
562
|
-
relations: []
|
|
563
|
-
};
|
|
564
|
-
registry.containers.push(container);
|
|
565
|
-
registry.allElements.set(person.id, container);
|
|
566
|
-
}
|
|
567
|
-
for (const system of workspace.model.softwareSystems ?? []) {
|
|
568
|
-
addRelations(registry.allElements, system.id, system.relationships);
|
|
569
|
-
for (const cont of system.containers ?? []) {
|
|
570
|
-
addRelations(registry.allElements, cont.id, cont.relationships);
|
|
571
|
-
for (const comp of cont.components ?? []) {
|
|
572
|
-
addRelations(registry.allElements, comp.id, comp.relationships);
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
for (const person of workspace.model.people ?? []) {
|
|
577
|
-
addRelations(registry.allElements, person.id, person.relationships);
|
|
578
|
-
}
|
|
579
|
-
return {
|
|
580
|
-
allContainers: registry.containers.toSorted(
|
|
581
|
-
(a, b) => a.name.localeCompare(b.name)
|
|
582
|
-
),
|
|
583
|
-
boundaries: registry.boundaries
|
|
584
|
-
};
|
|
585
|
-
};
|
|
586
|
-
const loadStructurizrElements = async (filePath) => {
|
|
587
|
-
const workspace = await loadStructurizrWorkspace(filePath);
|
|
588
|
-
return mapContainersFromStructurizr(workspace);
|
|
589
|
-
};
|
|
590
|
-
|
|
591
|
-
const structurizrDslSyntax = {
|
|
592
|
-
containerPattern: (name) => `${name} = container`,
|
|
593
|
-
containerDecl: (name, label, tags) => {
|
|
594
|
-
if (tags) {
|
|
595
|
-
return `${name} = container "${label}" {
|
|
596
|
-
tags "${tags}"
|
|
597
|
-
}`;
|
|
598
|
-
}
|
|
599
|
-
return `${name} = container "${label}"`;
|
|
600
|
-
},
|
|
601
|
-
relationPattern: (from, to) => `${from} -> ${to}`,
|
|
602
|
-
relationDecl: (from, to, tech, tags) => {
|
|
603
|
-
const techPart = tech ? ` "${tech}"` : "";
|
|
604
|
-
if (tags) {
|
|
605
|
-
return `${from} -> ${to}${techPart} {
|
|
606
|
-
tags "${tags}"
|
|
607
|
-
}`;
|
|
608
|
-
}
|
|
609
|
-
return `${from} -> ${to}${techPart}`;
|
|
610
|
-
}
|
|
611
|
-
};
|
|
612
|
-
|
|
613
|
-
const checkAcl = (containers, options) => {
|
|
614
|
-
const tag = options?.tag ?? "acl";
|
|
615
|
-
const externalType = options?.externalType ?? EXTERNAL_SYSTEM_TYPE;
|
|
616
|
-
const violations = [];
|
|
617
|
-
for (const container of containers) {
|
|
618
|
-
const externalRelations = container.relations.filter(
|
|
619
|
-
(r) => r.to.type === externalType
|
|
620
|
-
);
|
|
621
|
-
if (!container.tags?.includes(tag) && externalRelations.length > 0) {
|
|
622
|
-
const names = externalRelations.map((r) => r.to.name).join(", ");
|
|
623
|
-
const label = externalRelations.length === 1 ? "system" : "systems";
|
|
624
|
-
violations.push({
|
|
625
|
-
container: container.name,
|
|
626
|
-
message: `calls external ${label} ${names} without an ACL layer`
|
|
627
|
-
});
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
return violations;
|
|
631
|
-
};
|
|
632
|
-
|
|
633
|
-
const checkAcyclic = (containers) => {
|
|
634
|
-
const violations = [];
|
|
635
|
-
const findCycle = (relations, sourceContainerName, visited = /* @__PURE__ */ new Set()) => {
|
|
636
|
-
for (const rel of relations) {
|
|
637
|
-
if (rel.to.name === sourceContainerName) {
|
|
638
|
-
return true;
|
|
639
|
-
}
|
|
640
|
-
if (visited.has(rel.to.name)) continue;
|
|
641
|
-
visited.add(rel.to.name);
|
|
642
|
-
if (findCycle(rel.to.relations, sourceContainerName, visited)) {
|
|
643
|
-
return true;
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
return false;
|
|
647
|
-
};
|
|
648
|
-
for (const container of containers) {
|
|
649
|
-
if (findCycle(container.relations, container.name)) {
|
|
650
|
-
violations.push({
|
|
651
|
-
container: container.name,
|
|
652
|
-
message: "participates in a dependency cycle"
|
|
653
|
-
});
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
return violations;
|
|
657
|
-
};
|
|
658
|
-
|
|
659
|
-
const checkApiGateway = (containers, options) => {
|
|
660
|
-
const aclTag = options?.aclTag ?? "acl";
|
|
661
|
-
const externalType = options?.externalType ?? EXTERNAL_SYSTEM_TYPE;
|
|
662
|
-
const gatewayPattern = options?.gatewayPattern ?? /gateway/i;
|
|
663
|
-
const violations = [];
|
|
664
|
-
for (const container of containers) {
|
|
665
|
-
if (!container.tags?.includes(aclTag)) continue;
|
|
666
|
-
for (const rel of container.relations) {
|
|
667
|
-
if (rel.to.type !== externalType) continue;
|
|
668
|
-
const techs = rel.technology?.split(", ") ?? [];
|
|
669
|
-
if (!techs.some((t) => gatewayPattern.test(t))) {
|
|
670
|
-
violations.push({
|
|
671
|
-
container: container.name,
|
|
672
|
-
message: `calls external "${rel.to.name}" without going through an API Gateway`
|
|
673
|
-
});
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
return violations;
|
|
678
|
-
};
|
|
679
|
-
|
|
680
|
-
const getBoundaryCohesion = (boundary) => {
|
|
681
|
-
const names = new Set(boundary.containers.map((c) => c.name));
|
|
682
|
-
let result = 0;
|
|
683
|
-
for (const container of boundary.containers) {
|
|
684
|
-
result += container.relations.filter((r) => names.has(r.to.name)).length;
|
|
685
|
-
}
|
|
686
|
-
for (const innerBoundary of boundary.boundaries) {
|
|
687
|
-
result += getBoundaryCoupling(innerBoundary);
|
|
688
|
-
}
|
|
689
|
-
return result;
|
|
690
|
-
};
|
|
691
|
-
const getBoundaryCoupling = (boundary, externalType = EXTERNAL_SYSTEM_TYPE, internalType = CONTAINER_TYPE) => {
|
|
692
|
-
const names = new Set(boundary.containers.map((c) => c.name));
|
|
693
|
-
let result = 0;
|
|
694
|
-
for (const container of boundary.containers) {
|
|
695
|
-
result += container.relations.filter(
|
|
696
|
-
(r) => r.to.type === internalType && !names.has(r.to.name)
|
|
697
|
-
).length;
|
|
698
|
-
}
|
|
699
|
-
for (const innerBoundary of boundary.boundaries) {
|
|
700
|
-
for (const container of innerBoundary.containers) {
|
|
701
|
-
result += container.relations.filter(
|
|
702
|
-
(r) => r.to.type === externalType
|
|
703
|
-
).length;
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
return result;
|
|
707
|
-
};
|
|
708
|
-
const checkCohesion = (model, options) => {
|
|
709
|
-
const externalType = options?.externalType ?? EXTERNAL_SYSTEM_TYPE;
|
|
710
|
-
const internalType = options?.internalType ?? CONTAINER_TYPE;
|
|
711
|
-
const violations = [];
|
|
712
|
-
for (const boundary of model.boundaries) {
|
|
713
|
-
const cohesion = getBoundaryCohesion(boundary);
|
|
714
|
-
const coupling = getBoundaryCoupling(boundary, externalType, internalType);
|
|
715
|
-
if (cohesion <= coupling) {
|
|
716
|
-
violations.push({
|
|
717
|
-
container: boundary.name,
|
|
718
|
-
message: `coupling (${coupling}) \u2265 cohesion (${cohesion}) \u2014 more cross-boundary dependencies than internal connections`
|
|
719
|
-
});
|
|
720
|
-
}
|
|
721
|
-
if (boundary.boundaries.length > 0) {
|
|
722
|
-
const innerCohesionSum = boundary.boundaries.reduce(
|
|
723
|
-
(sum, current) => sum + getBoundaryCohesion(current),
|
|
724
|
-
0
|
|
725
|
-
);
|
|
726
|
-
if (cohesion >= innerCohesionSum) {
|
|
727
|
-
violations.push({
|
|
728
|
-
container: boundary.name,
|
|
729
|
-
message: `parent cohesion (${cohesion}) \u2265 sum of inner cohesions (${innerCohesionSum}) \u2014 parent boundary should be less cohesive than its sub-boundaries`
|
|
730
|
-
});
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
return violations;
|
|
735
|
-
};
|
|
736
|
-
|
|
737
|
-
const buildBoundaryLookup = (model) => {
|
|
738
|
-
const map = /* @__PURE__ */ new Map();
|
|
739
|
-
for (const boundary of model.boundaries) {
|
|
740
|
-
for (const c of boundary.containers) {
|
|
741
|
-
map.set(c.name, boundary);
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
return map;
|
|
745
|
-
};
|
|
746
|
-
const collectPublicAndUsage = (model, boundaryOf) => {
|
|
747
|
-
const publicOf = /* @__PURE__ */ new Map();
|
|
748
|
-
const used = /* @__PURE__ */ new Map();
|
|
749
|
-
for (const source of model.allContainers) {
|
|
750
|
-
const srcBoundary = boundaryOf.get(source.name);
|
|
751
|
-
if (!srcBoundary) continue;
|
|
752
|
-
for (const rel of source.relations) {
|
|
753
|
-
const tgtBoundary = boundaryOf.get(rel.to.name);
|
|
754
|
-
if (!tgtBoundary || tgtBoundary === srcBoundary) continue;
|
|
755
|
-
let pub = publicOf.get(tgtBoundary);
|
|
756
|
-
if (!pub) {
|
|
757
|
-
pub = /* @__PURE__ */ new Set();
|
|
758
|
-
publicOf.set(tgtBoundary, pub);
|
|
759
|
-
}
|
|
760
|
-
pub.add(rel.to.name);
|
|
761
|
-
const key = `${srcBoundary.name}\0${tgtBoundary.name}`;
|
|
762
|
-
let u = used.get(key);
|
|
763
|
-
if (!u) {
|
|
764
|
-
u = /* @__PURE__ */ new Set();
|
|
765
|
-
used.set(key, u);
|
|
766
|
-
}
|
|
767
|
-
u.add(rel.to.name);
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
|
-
return { publicOf, used };
|
|
771
|
-
};
|
|
772
|
-
const checkCommonReuse = (model) => {
|
|
773
|
-
const boundaryOf = buildBoundaryLookup(model);
|
|
774
|
-
const { publicOf, used } = collectPublicAndUsage(model, boundaryOf);
|
|
775
|
-
const violations = [];
|
|
776
|
-
for (const [provider, pubNames] of publicOf) {
|
|
777
|
-
if (pubNames.size < 2) continue;
|
|
778
|
-
for (const consumer of model.boundaries) {
|
|
779
|
-
if (consumer === provider) continue;
|
|
780
|
-
const key = `${consumer.name}\0${provider.name}`;
|
|
781
|
-
const usedNames = used.get(key);
|
|
782
|
-
if (!usedNames || usedNames.size >= pubNames.size) continue;
|
|
783
|
-
const missing = [...pubNames].filter((n) => !usedNames.has(n));
|
|
784
|
-
violations.push({
|
|
785
|
-
container: consumer.name,
|
|
786
|
-
message: `uses ${[...usedNames].join(", ")} of "${provider.name}" but not ${missing.join(", ")} \u2014 all public services of a context should be used together`
|
|
787
|
-
});
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
return violations;
|
|
791
|
-
};
|
|
792
|
-
|
|
793
|
-
const checkCrud = (containers, options) => {
|
|
794
|
-
const repoTags = options?.repoTags ?? ["repo", "relay"];
|
|
795
|
-
const dbType = options?.dbType ?? CONTAINER_DB_TYPE;
|
|
796
|
-
const violations = [];
|
|
797
|
-
for (const container of containers) {
|
|
798
|
-
const dbRelations = container.relations.filter((r) => r.to.type === dbType);
|
|
799
|
-
const isRepo = repoTags.some((tag) => container.tags?.includes(tag));
|
|
800
|
-
if (!isRepo && dbRelations.length > 0) {
|
|
801
|
-
violations.push({
|
|
802
|
-
container: container.name,
|
|
803
|
-
message: `directly accesses database ${dbRelations.map((r) => r.to.name).join(", ")} \u2014 add a repo or relay`
|
|
804
|
-
});
|
|
805
|
-
}
|
|
806
|
-
if (isRepo && container.relations.some((r) => r.to.type !== dbType)) {
|
|
807
|
-
violations.push({
|
|
808
|
-
container: container.name,
|
|
809
|
-
message: `repo has non-database dependencies: ${container.relations.filter((r) => r.to.type !== dbType).map((r) => r.to.name).join(", ")} \u2014 repos should only access databases`
|
|
810
|
-
});
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
return violations;
|
|
814
|
-
};
|
|
815
|
-
|
|
816
|
-
const checkDbPerService = (containers, options) => {
|
|
817
|
-
const dbType = options?.dbType ?? CONTAINER_DB_TYPE;
|
|
818
|
-
const violations = [];
|
|
819
|
-
const dbAccessMap = /* @__PURE__ */ new Map();
|
|
820
|
-
for (const container of containers) {
|
|
821
|
-
for (const rel of container.relations) {
|
|
822
|
-
if (rel.to.type === dbType) {
|
|
823
|
-
const accessors = dbAccessMap.get(rel.to.name) ?? [];
|
|
824
|
-
accessors.push(container.name);
|
|
825
|
-
dbAccessMap.set(rel.to.name, accessors);
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
for (const [db, accessors] of dbAccessMap) {
|
|
830
|
-
if (accessors.length > 1) {
|
|
831
|
-
violations.push({
|
|
832
|
-
container: db,
|
|
833
|
-
message: `shared between ${accessors.join(", ")} \u2014 each database should have a single owner`
|
|
834
|
-
});
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
return violations;
|
|
838
|
-
};
|
|
839
|
-
|
|
840
|
-
const computeCoupling = (internal, internalNames) => {
|
|
841
|
-
const ca = /* @__PURE__ */ new Map();
|
|
842
|
-
const ce = /* @__PURE__ */ new Map();
|
|
843
|
-
for (const c of internal) {
|
|
844
|
-
ca.set(c.name, 0);
|
|
845
|
-
ce.set(c.name, 0);
|
|
846
|
-
}
|
|
847
|
-
for (const c of internal) {
|
|
848
|
-
for (const rel of c.relations) {
|
|
849
|
-
if (!internalNames.has(rel.to.name)) continue;
|
|
850
|
-
ce.set(c.name, ce.get(c.name) + 1);
|
|
851
|
-
ca.set(rel.to.name, ca.get(rel.to.name) + 1);
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
return { ca, ce };
|
|
855
|
-
};
|
|
856
|
-
const checkStableDependencies = (containers, options) => {
|
|
857
|
-
const externalType = options?.externalType ?? EXTERNAL_SYSTEM_TYPE;
|
|
858
|
-
const violations = [];
|
|
859
|
-
const internal = containers.filter((c) => c.type !== externalType);
|
|
860
|
-
const internalNames = new Set(internal.map((c) => c.name));
|
|
861
|
-
const { ca, ce } = computeCoupling(internal, internalNames);
|
|
862
|
-
const instability = (name) => {
|
|
863
|
-
const afferent = ca.get(name);
|
|
864
|
-
const efferent = ce.get(name);
|
|
865
|
-
if (afferent + efferent === 0) return 1;
|
|
866
|
-
return efferent / (afferent + efferent);
|
|
867
|
-
};
|
|
868
|
-
for (const c of internal) {
|
|
869
|
-
for (const rel of c.relations) {
|
|
870
|
-
if (!internalNames.has(rel.to.name)) continue;
|
|
871
|
-
const iSource = instability(c.name);
|
|
872
|
-
const iTarget = instability(rel.to.name);
|
|
873
|
-
if (iSource < iTarget) {
|
|
874
|
-
violations.push({
|
|
875
|
-
container: c.name,
|
|
876
|
-
message: `stable module (I=${iSource.toFixed(2)}) depends on less stable "${rel.to.name}" (I=${iTarget.toFixed(2)}) \u2014 dependencies should point toward stability`
|
|
877
|
-
});
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
return violations;
|
|
882
|
-
};
|
|
883
|
-
|
|
884
|
-
export { AactConfigSchema as A, BOUNDARY_TYPE as B, COMPONENT_TYPE as C, generateKubernetes as D, EXTERNAL_SYSTEM_TYPE as E, generatePlantumlFromModel as F, loadPlantumlElements as G, loadStructurizrElements as H, loadStructurizrWorkspace as I, mapContainersFromPlantumlElements as J, mapContainersFromStructurizr as K, structurizrDslSyntax as L, PERSON_TYPE as P, STRUCTURIZR_INTERACTION_ASYNC as S, CONTAINER_BOUNDARY_TYPE as a, CONTAINER_DB_TYPE as b, CONTAINER_TYPE as c, PLANTUML_BOUNDARY as d, PLANTUML_COMPONENT as e, PLANTUML_CONTAINER as f, PLANTUML_CONTAINER_BOUNDARY as g, PLANTUML_CONTAINER_DB as h, PLANTUML_PERSON as i, PLANTUML_SYSTEM as j, PLANTUML_SYSTEM_BOUNDARY as k, PLANTUML_SYSTEM_EXT as l, STRUCTURIZR_LOCATION_EXTERNAL as m, STRUCTURIZR_TAG_ASYNC as n, SYSTEM_BOUNDARY_TYPE as o, SYSTEM_TYPE as p, analyzeArchitecture as q, checkAcl as r, checkAcyclic as s, checkApiGateway as t, checkCohesion as u, checkCommonReuse as v, checkCrud as w, checkDbPerService as x, checkStableDependencies as y, defineConfig as z };
|