aact 1.0.0 → 2.0.1
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/LICENSE +674 -0
- package/README.md +171 -0
- package/dist/cli/index.d.mts +2 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.mjs +506 -0
- package/dist/index.d.mts +248 -0
- package/dist/index.d.ts +248 -0
- package/dist/index.mjs +91 -0
- package/dist/shared/aact.Dqryafrg.mjs +744 -0
- package/package.json +82 -8
|
@@ -0,0 +1,744 @@
|
|
|
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 apiTechnologies = ["http", "grpc", "tcp"];
|
|
8
|
+
const allRelations = (model) => model.allContainers.flatMap(
|
|
9
|
+
(container) => container.relations.map((relation) => ({ from: container, relation }))
|
|
10
|
+
);
|
|
11
|
+
const boundaryContainsName = (boundary, name) => boundary.containers.some((c) => c.name === name);
|
|
12
|
+
const classifyRelation = (boundary, parentBoundary, from, relation, result, parentResult) => {
|
|
13
|
+
if (!boundaryContainsName(boundary, from.name)) return;
|
|
14
|
+
if (boundaryContainsName(boundary, relation.to.name)) {
|
|
15
|
+
result.cohesion++;
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const isInParentSibling = parentBoundary?.boundaries.some(
|
|
19
|
+
(b) => b.containers.some((c) => c.name === relation.to.name)
|
|
20
|
+
) ?? false;
|
|
21
|
+
if (!parentBoundary || isInParentSibling) {
|
|
22
|
+
result.coupling++;
|
|
23
|
+
result.couplingRelations.push({ from: from.name, to: relation.to.name });
|
|
24
|
+
if (parentResult) parentResult.cohesion++;
|
|
25
|
+
} else if (parentResult) {
|
|
26
|
+
parentResult.coupling++;
|
|
27
|
+
parentResult.couplingRelations.push({
|
|
28
|
+
from: from.name,
|
|
29
|
+
to: relation.to.name
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
const analyzeModel = (model) => {
|
|
34
|
+
const relations = allRelations(model);
|
|
35
|
+
const asyncApiCalls = relations.filter(
|
|
36
|
+
(it) => it.relation.tags?.includes("async")
|
|
37
|
+
);
|
|
38
|
+
const syncApiCalls = relations.filter((it) => {
|
|
39
|
+
const isExternalApi = it.relation.to.type === "System_Ext";
|
|
40
|
+
const isApiTechnology = apiTechnologies.some(
|
|
41
|
+
(apiTechn) => (it.relation.technology ?? "").toLowerCase().includes(apiTechn)
|
|
42
|
+
);
|
|
43
|
+
return !it.relation.tags?.includes("async") && (isExternalApi || isApiTechnology);
|
|
44
|
+
});
|
|
45
|
+
const boundaryResults = /* @__PURE__ */ new Map();
|
|
46
|
+
for (const boundary of model.boundaries) {
|
|
47
|
+
boundaryResults.set(boundary.name, {
|
|
48
|
+
name: boundary.name,
|
|
49
|
+
label: boundary.label,
|
|
50
|
+
cohesion: 0,
|
|
51
|
+
coupling: 0,
|
|
52
|
+
couplingRelations: []
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
for (const boundary of model.boundaries) {
|
|
56
|
+
const parentBoundary = model.boundaries.find(
|
|
57
|
+
(b) => b.boundaries.some((child) => child.name === boundary.name)
|
|
58
|
+
);
|
|
59
|
+
const result = boundaryResults.get(boundary.name);
|
|
60
|
+
const parentResult = parentBoundary ? boundaryResults.get(parentBoundary.name) : void 0;
|
|
61
|
+
for (const { from, relation } of relations) {
|
|
62
|
+
classifyRelation(
|
|
63
|
+
boundary,
|
|
64
|
+
parentBoundary,
|
|
65
|
+
from,
|
|
66
|
+
relation,
|
|
67
|
+
result,
|
|
68
|
+
parentResult
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
elementsCount: model.allContainers.length,
|
|
74
|
+
syncApiCalls: syncApiCalls.length,
|
|
75
|
+
asyncApiCalls: asyncApiCalls.length,
|
|
76
|
+
databases: analyzeDatabases(model),
|
|
77
|
+
boundaries: [...boundaryResults.values()]
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
const analyzeDatabases = (model) => {
|
|
81
|
+
const dbContainers = model.allContainers.filter(
|
|
82
|
+
(it) => it.type === "ContainerDb"
|
|
83
|
+
);
|
|
84
|
+
const dbRelations = model.allContainers.flatMap(
|
|
85
|
+
(container) => container.relations.filter(
|
|
86
|
+
(r) => dbContainers.some((db) => db.name === r.to.name)
|
|
87
|
+
)
|
|
88
|
+
);
|
|
89
|
+
return {
|
|
90
|
+
count: dbContainers.length,
|
|
91
|
+
consumes: dbRelations.length
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
const analyzeArchitecture = (model) => {
|
|
95
|
+
return {
|
|
96
|
+
model,
|
|
97
|
+
report: analyzeModel(model)
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const ruleOption = (entries) => v.optional(v.union([v.boolean(), v.strictObject(entries)]));
|
|
102
|
+
const AactConfigSchema = v.strictObject({
|
|
103
|
+
source: v.strictObject({
|
|
104
|
+
type: v.picklist(["plantuml", "structurizr"]),
|
|
105
|
+
path: v.string()
|
|
106
|
+
}),
|
|
107
|
+
rules: v.optional(
|
|
108
|
+
v.strictObject({
|
|
109
|
+
acl: ruleOption({
|
|
110
|
+
tag: v.optional(v.string()),
|
|
111
|
+
externalType: v.optional(v.string())
|
|
112
|
+
}),
|
|
113
|
+
acyclic: v.optional(v.boolean()),
|
|
114
|
+
apiGateway: ruleOption({
|
|
115
|
+
aclTag: v.optional(v.string()),
|
|
116
|
+
externalType: v.optional(v.string()),
|
|
117
|
+
gatewayPattern: v.optional(v.instance(RegExp))
|
|
118
|
+
}),
|
|
119
|
+
crud: ruleOption({
|
|
120
|
+
repoTags: v.optional(v.array(v.string())),
|
|
121
|
+
dbType: v.optional(v.string())
|
|
122
|
+
}),
|
|
123
|
+
dbPerService: ruleOption({
|
|
124
|
+
dbType: v.optional(v.string())
|
|
125
|
+
}),
|
|
126
|
+
cohesion: ruleOption({
|
|
127
|
+
externalType: v.optional(v.string()),
|
|
128
|
+
internalType: v.optional(v.string())
|
|
129
|
+
}),
|
|
130
|
+
stableDependencies: ruleOption({
|
|
131
|
+
externalType: v.optional(v.string())
|
|
132
|
+
})
|
|
133
|
+
})
|
|
134
|
+
),
|
|
135
|
+
generate: v.optional(
|
|
136
|
+
v.strictObject({
|
|
137
|
+
kubernetes: v.optional(
|
|
138
|
+
v.strictObject({
|
|
139
|
+
path: v.optional(v.string()),
|
|
140
|
+
exclude: v.optional(v.array(v.string()))
|
|
141
|
+
})
|
|
142
|
+
),
|
|
143
|
+
boundaryLabel: v.optional(v.string())
|
|
144
|
+
})
|
|
145
|
+
)
|
|
146
|
+
});
|
|
147
|
+
const defineConfig = (config) => config;
|
|
148
|
+
|
|
149
|
+
const toKebab = (name) => name.replaceAll("_", "-");
|
|
150
|
+
const toEnvKey = (name) => name.replaceAll("-", "_").toUpperCase();
|
|
151
|
+
const buildEnvVar = (relation, sourceKebab, options) => {
|
|
152
|
+
const targetType = relation.to.type;
|
|
153
|
+
const targetKebab = toKebab(relation.to.name);
|
|
154
|
+
const targetUpper = toEnvKey(targetKebab);
|
|
155
|
+
if (targetType === "ContainerDb") {
|
|
156
|
+
const value = options.dbConnectionTemplate.replaceAll(
|
|
157
|
+
"{name}",
|
|
158
|
+
sourceKebab
|
|
159
|
+
);
|
|
160
|
+
return { key: "PG_CONNECTION_STRING", value };
|
|
161
|
+
}
|
|
162
|
+
if (relation.tags?.includes("async")) {
|
|
163
|
+
const value = relation.technology ?? targetKebab;
|
|
164
|
+
return { key: `KAFKA_${targetUpper}_TOPIC`, value };
|
|
165
|
+
}
|
|
166
|
+
if (targetType === "System_Ext") {
|
|
167
|
+
const value = relation.technology ?? `https://${targetKebab}`;
|
|
168
|
+
return { key: `${targetUpper}_BASE_URL`, value };
|
|
169
|
+
}
|
|
170
|
+
if (targetType === "Container") {
|
|
171
|
+
const value = relation.technology ?? `http://${targetKebab}:${options.defaultPort}`;
|
|
172
|
+
return { key: `${targetUpper}_BASE_URL`, value };
|
|
173
|
+
}
|
|
174
|
+
return void 0;
|
|
175
|
+
};
|
|
176
|
+
const generateKubernetes = (model, options) => {
|
|
177
|
+
const defaultPort = options?.defaultPort ?? 8080;
|
|
178
|
+
const dbConnectionTemplate = options?.dbConnectionTemplate ?? "postgresql://{name}:pass-{name}@postgresql:5432/{name}";
|
|
179
|
+
const resolvedOptions = { defaultPort, dbConnectionTemplate };
|
|
180
|
+
const containers = model.allContainers.filter(
|
|
181
|
+
(c) => c.type !== "ContainerDb" && c.type !== "System_Ext"
|
|
182
|
+
);
|
|
183
|
+
return containers.map((container) => {
|
|
184
|
+
const kebabName = toKebab(container.name);
|
|
185
|
+
const envEntries = [];
|
|
186
|
+
for (const relation of container.relations) {
|
|
187
|
+
const entry = buildEnvVar(relation, kebabName, resolvedOptions);
|
|
188
|
+
if (entry) {
|
|
189
|
+
envEntries.push(entry);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
envEntries.sort((a, b) => a.key.localeCompare(b.key));
|
|
193
|
+
const doc = { name: kebabName };
|
|
194
|
+
if (envEntries.length > 0) {
|
|
195
|
+
const environment = {};
|
|
196
|
+
for (const { key, value } of envEntries) {
|
|
197
|
+
environment[key] = { default: value };
|
|
198
|
+
}
|
|
199
|
+
doc.environment = environment;
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
fileName: `${kebabName}.yml`,
|
|
203
|
+
content: YAML.stringify(doc)
|
|
204
|
+
};
|
|
205
|
+
});
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const containerTypeMap = {
|
|
209
|
+
Container: "Container",
|
|
210
|
+
ContainerDb: "ContainerDb",
|
|
211
|
+
System_Ext: "System_Ext",
|
|
212
|
+
Person: "Person"
|
|
213
|
+
};
|
|
214
|
+
const renderContainer = (container) => {
|
|
215
|
+
const type = containerTypeMap[container.type ?? "Container"] ?? "Container";
|
|
216
|
+
const tags = container.tags && container.tags.length > 0 ? `, $tags="${container.tags.join("+")}"` : "";
|
|
217
|
+
const desc = container.description ? `, "${container.description}"` : "";
|
|
218
|
+
return `${type}(${container.name}, "${container.label}"${desc}${tags})`;
|
|
219
|
+
};
|
|
220
|
+
const renderBoundary = (boundary, indent) => {
|
|
221
|
+
const inner = indent + " ";
|
|
222
|
+
const children = [
|
|
223
|
+
...boundary.boundaries.map((child) => renderBoundary(child, inner)),
|
|
224
|
+
...boundary.containers.map(
|
|
225
|
+
(container) => `${inner}${renderContainer(container)}`
|
|
226
|
+
)
|
|
227
|
+
];
|
|
228
|
+
return [
|
|
229
|
+
`${indent}Boundary(${boundary.name}, "${boundary.label}") {`,
|
|
230
|
+
...children,
|
|
231
|
+
`${indent}}`
|
|
232
|
+
].join("\n");
|
|
233
|
+
};
|
|
234
|
+
const renderRelation = (container, relation) => {
|
|
235
|
+
const tech = relation.technology ? `, "${relation.technology}"` : "";
|
|
236
|
+
const tags = relation.tags && relation.tags.length > 0 ? `, $tags="${relation.tags.join("+")}"` : "";
|
|
237
|
+
return `Rel(${container.name}, ${relation.to.name}, ""${tech}${tags})`;
|
|
238
|
+
};
|
|
239
|
+
const collectBoundaryContainerNames = (boundaries) => {
|
|
240
|
+
const names = /* @__PURE__ */ new Set();
|
|
241
|
+
const collect = (boundary) => {
|
|
242
|
+
for (const c of boundary.containers) names.add(c.name);
|
|
243
|
+
for (const b of boundary.boundaries) collect(b);
|
|
244
|
+
};
|
|
245
|
+
for (const boundary of boundaries) collect(boundary);
|
|
246
|
+
return names;
|
|
247
|
+
};
|
|
248
|
+
const renderBody = (model, standaloneContainers, boundaryLabel) => {
|
|
249
|
+
if (boundaryLabel) {
|
|
250
|
+
return [
|
|
251
|
+
`Boundary(project, "${boundaryLabel}") {`,
|
|
252
|
+
...model.boundaries.map((b) => renderBoundary(b, " ")),
|
|
253
|
+
...standaloneContainers.map((c) => ` ${renderContainer(c)}`),
|
|
254
|
+
`}`
|
|
255
|
+
];
|
|
256
|
+
}
|
|
257
|
+
return [
|
|
258
|
+
...model.boundaries.map((b) => renderBoundary(b, "")),
|
|
259
|
+
...standaloneContainers.map((c) => renderContainer(c))
|
|
260
|
+
];
|
|
261
|
+
};
|
|
262
|
+
const generatePlantumlFromModel = (model, options) => {
|
|
263
|
+
const boundaryNames = collectBoundaryContainerNames(model.boundaries);
|
|
264
|
+
const standaloneContainers = model.allContainers.filter(
|
|
265
|
+
(c) => !boundaryNames.has(c.name)
|
|
266
|
+
);
|
|
267
|
+
const relations = model.allContainers.flatMap(
|
|
268
|
+
(container) => container.relations.map((rel) => renderRelation(container, rel))
|
|
269
|
+
);
|
|
270
|
+
return [
|
|
271
|
+
`@startuml`,
|
|
272
|
+
`!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml`,
|
|
273
|
+
`LAYOUT_WITH_LEGEND()`,
|
|
274
|
+
`AddRelTag("async", $lineStyle = DottedLine())`,
|
|
275
|
+
"",
|
|
276
|
+
...renderBody(model, standaloneContainers, options?.boundaryLabel),
|
|
277
|
+
"",
|
|
278
|
+
...relations,
|
|
279
|
+
"@enduml"
|
|
280
|
+
].join("\n");
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const filterElements = (elements) => {
|
|
284
|
+
const result = [];
|
|
285
|
+
for (const element of elements) {
|
|
286
|
+
if (element instanceof Comment) continue;
|
|
287
|
+
if (element.type_.name === "Container" || element.type_.name === "ContainerDb" || element.type_.name === "Component" || element.type_.name === "System_Ext" || element.type_.name === "System" || element.type_.name === "Person" || element instanceof Stdlib_C4_Dynamic_Rel || element instanceof Relationship) {
|
|
288
|
+
result.push(element);
|
|
289
|
+
}
|
|
290
|
+
const elementAsBoundary = element;
|
|
291
|
+
if (["System_Boundary", "Container_Boundary", "Boundary"].includes(
|
|
292
|
+
elementAsBoundary.type_.name
|
|
293
|
+
)) {
|
|
294
|
+
result.push(elementAsBoundary);
|
|
295
|
+
const resultFromBoundary = filterElements(elementAsBoundary.elements);
|
|
296
|
+
result.push(...resultFromBoundary);
|
|
297
|
+
}
|
|
298
|
+
if (Array.isArray(element)) {
|
|
299
|
+
const resultFromArray = filterElements(element);
|
|
300
|
+
result.push(...resultFromArray);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return result;
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
const loadPlantumlElements = async (filePath) => {
|
|
307
|
+
const filepath = path.resolve(filePath);
|
|
308
|
+
let data = await fs.readFile(filepath, "utf8");
|
|
309
|
+
data = data.replaceAll(/, \$tags=(".+?")/g, ", $1").replaceAll('""', '" "');
|
|
310
|
+
const [{ elements }] = parse(data);
|
|
311
|
+
for (const element of elements) {
|
|
312
|
+
if (element instanceof Comment) continue;
|
|
313
|
+
const relation = element;
|
|
314
|
+
if (relation.type_.name.startsWith("Rel_Back")) {
|
|
315
|
+
const from = relation.from;
|
|
316
|
+
relation.from = relation.to;
|
|
317
|
+
relation.to = from;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return filterElements(elements);
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
const addDependency = (containers, relation) => {
|
|
324
|
+
const containerFrom = containers.find((x) => x.name === relation.from);
|
|
325
|
+
if (!containerFrom) return;
|
|
326
|
+
const containerTo = containers.find((x) => x.name === relation.to);
|
|
327
|
+
if (!containerTo) return;
|
|
328
|
+
containerFrom.relations.push({
|
|
329
|
+
to: containerTo,
|
|
330
|
+
technology: relation.techn,
|
|
331
|
+
tags: relation.descr?.split(",").map((t) => t.trim())
|
|
332
|
+
});
|
|
333
|
+
};
|
|
334
|
+
const mapContainersFromPlantumlElements = (elements) => {
|
|
335
|
+
const containers = elements.filter(
|
|
336
|
+
(element) => element instanceof Stdlib_C4_Container_Component || element instanceof Stdlib_C4_Context
|
|
337
|
+
).map((element) => {
|
|
338
|
+
const component = element;
|
|
339
|
+
return {
|
|
340
|
+
name: component.alias,
|
|
341
|
+
label: component.label,
|
|
342
|
+
type: component.type_.name,
|
|
343
|
+
relations: [],
|
|
344
|
+
tags: component.sprite ? [component.sprite] : void 0,
|
|
345
|
+
description: component.descr
|
|
346
|
+
};
|
|
347
|
+
});
|
|
348
|
+
for (const element of elements) {
|
|
349
|
+
if (element instanceof Stdlib_C4_Container_Component) {
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
if (element instanceof Stdlib_C4_Dynamic_Rel) {
|
|
353
|
+
addDependency(containers, element);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
const boundaries = elements.filter((element) => element instanceof Stdlib_C4_Boundary).map((element) => {
|
|
357
|
+
const component = element;
|
|
358
|
+
return {
|
|
359
|
+
name: component.alias,
|
|
360
|
+
label: component.label,
|
|
361
|
+
type: component.type_.name,
|
|
362
|
+
boundaries: [],
|
|
363
|
+
containers: containers.filter(
|
|
364
|
+
(container) => component.elements.filter(
|
|
365
|
+
(element2) => element2 instanceof Stdlib_C4_Container_Component
|
|
366
|
+
).some((e) => e.alias == container.name)
|
|
367
|
+
)
|
|
368
|
+
};
|
|
369
|
+
});
|
|
370
|
+
for (const boundary of boundaries) {
|
|
371
|
+
const component = elements.find(
|
|
372
|
+
(element) => element instanceof Stdlib_C4_Boundary && element.alias == boundary.name
|
|
373
|
+
);
|
|
374
|
+
boundary.boundaries = boundaries.filter(
|
|
375
|
+
(b) => component.elements.filter((element) => element instanceof Stdlib_C4_Boundary).some((e) => e.alias == b.name)
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
return {
|
|
379
|
+
allContainers: containers.toSorted((a, b) => a.name.localeCompare(b.name)),
|
|
380
|
+
boundaries
|
|
381
|
+
};
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
const DATABASE_TECHNOLOGIES = [
|
|
385
|
+
"postgresql",
|
|
386
|
+
"postgres",
|
|
387
|
+
"mysql",
|
|
388
|
+
"mariadb",
|
|
389
|
+
"mongodb",
|
|
390
|
+
"mongo",
|
|
391
|
+
"redis",
|
|
392
|
+
"elasticsearch",
|
|
393
|
+
"dynamodb",
|
|
394
|
+
"cassandra",
|
|
395
|
+
"sqlite",
|
|
396
|
+
"oracle",
|
|
397
|
+
"sqlserver",
|
|
398
|
+
"mssql",
|
|
399
|
+
"database",
|
|
400
|
+
"db"
|
|
401
|
+
];
|
|
402
|
+
const isDatabase = (technology, name) => {
|
|
403
|
+
const techLower = technology?.toLowerCase() ?? "";
|
|
404
|
+
const nameLower = name?.toLowerCase() ?? "";
|
|
405
|
+
if (DATABASE_TECHNOLOGIES.some((db) => techLower.includes(db))) {
|
|
406
|
+
return true;
|
|
407
|
+
}
|
|
408
|
+
if (nameLower.endsWith(" db") || nameLower.endsWith("_db") || nameLower.endsWith("database")) {
|
|
409
|
+
return true;
|
|
410
|
+
}
|
|
411
|
+
return false;
|
|
412
|
+
};
|
|
413
|
+
const enrichTags = (existingTags, name) => {
|
|
414
|
+
const tags = existingTags?.split(",").map((t) => t.trim()).filter(Boolean) ?? [];
|
|
415
|
+
const nameLower = name?.toLowerCase() ?? "";
|
|
416
|
+
if (nameLower.includes("crud") && !tags.includes("repo")) {
|
|
417
|
+
tags.push("repo");
|
|
418
|
+
}
|
|
419
|
+
if (nameLower.includes("acl") && !tags.includes("acl")) {
|
|
420
|
+
tags.push("acl");
|
|
421
|
+
}
|
|
422
|
+
return tags;
|
|
423
|
+
};
|
|
424
|
+
const loadStructurizrWorkspace = async (filePath) => {
|
|
425
|
+
const filepath = path.resolve(filePath);
|
|
426
|
+
const data = await fs.readFile(filepath, "utf8");
|
|
427
|
+
return JSON.parse(data);
|
|
428
|
+
};
|
|
429
|
+
const processExternalSystem = (system, registry) => {
|
|
430
|
+
const container = {
|
|
431
|
+
name: system.id,
|
|
432
|
+
label: system.name,
|
|
433
|
+
type: "System_Ext",
|
|
434
|
+
tags: system.tags?.split(",").map((t) => t.trim()).filter(Boolean),
|
|
435
|
+
description: system.description ?? "",
|
|
436
|
+
relations: []
|
|
437
|
+
};
|
|
438
|
+
registry.containers.push(container);
|
|
439
|
+
registry.allElements.set(system.id, container);
|
|
440
|
+
};
|
|
441
|
+
const processInternalSystem = (system, registry) => {
|
|
442
|
+
const systemContainers = [];
|
|
443
|
+
for (const cont of system.containers ?? []) {
|
|
444
|
+
const container = {
|
|
445
|
+
name: cont.id,
|
|
446
|
+
label: cont.name,
|
|
447
|
+
type: isDatabase(cont.technology, cont.name) ? "ContainerDb" : "Container",
|
|
448
|
+
tags: enrichTags(cont.tags, cont.name),
|
|
449
|
+
description: cont.description ?? "",
|
|
450
|
+
relations: []
|
|
451
|
+
};
|
|
452
|
+
systemContainers.push(container);
|
|
453
|
+
registry.containers.push(container);
|
|
454
|
+
registry.allElements.set(cont.id, container);
|
|
455
|
+
}
|
|
456
|
+
registry.boundaries.push({
|
|
457
|
+
name: system.id,
|
|
458
|
+
label: system.name,
|
|
459
|
+
type: "Boundary",
|
|
460
|
+
boundaries: [],
|
|
461
|
+
containers: systemContainers
|
|
462
|
+
});
|
|
463
|
+
};
|
|
464
|
+
const addRelations = (allElements, sourceId, relationships) => {
|
|
465
|
+
const sourceContainer = allElements.get(sourceId);
|
|
466
|
+
if (!sourceContainer || !relationships) return;
|
|
467
|
+
for (const rel of relationships) {
|
|
468
|
+
const targetContainer = allElements.get(rel.destinationId);
|
|
469
|
+
if (!targetContainer) continue;
|
|
470
|
+
let tags = rel.tags?.split(",").map((t) => t.trim());
|
|
471
|
+
if (rel.interactionStyle === "Asynchronous") {
|
|
472
|
+
tags = [...tags ?? [], "async"];
|
|
473
|
+
}
|
|
474
|
+
const relation = {
|
|
475
|
+
to: targetContainer,
|
|
476
|
+
technology: rel.technology,
|
|
477
|
+
tags
|
|
478
|
+
};
|
|
479
|
+
sourceContainer.relations.push(relation);
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
const mapContainersFromStructurizr = (workspace) => {
|
|
483
|
+
const registry = {
|
|
484
|
+
allElements: /* @__PURE__ */ new Map(),
|
|
485
|
+
containers: [],
|
|
486
|
+
boundaries: []
|
|
487
|
+
};
|
|
488
|
+
for (const system of workspace.model.softwareSystems ?? []) {
|
|
489
|
+
if (system.location === "External") {
|
|
490
|
+
processExternalSystem(system, registry);
|
|
491
|
+
} else {
|
|
492
|
+
processInternalSystem(system, registry);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
for (const person of workspace.model.people ?? []) {
|
|
496
|
+
const container = {
|
|
497
|
+
name: person.id,
|
|
498
|
+
label: person.name,
|
|
499
|
+
type: "Person",
|
|
500
|
+
tags: person.tags?.split(",").map((t) => t.trim()).filter(Boolean),
|
|
501
|
+
description: person.description ?? "",
|
|
502
|
+
relations: []
|
|
503
|
+
};
|
|
504
|
+
registry.containers.push(container);
|
|
505
|
+
registry.allElements.set(person.id, container);
|
|
506
|
+
}
|
|
507
|
+
for (const system of workspace.model.softwareSystems ?? []) {
|
|
508
|
+
addRelations(registry.allElements, system.id, system.relationships);
|
|
509
|
+
for (const cont of system.containers ?? []) {
|
|
510
|
+
addRelations(registry.allElements, cont.id, cont.relationships);
|
|
511
|
+
for (const comp of cont.components ?? []) {
|
|
512
|
+
addRelations(registry.allElements, comp.id, comp.relationships);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
for (const person of workspace.model.people ?? []) {
|
|
517
|
+
addRelations(registry.allElements, person.id, person.relationships);
|
|
518
|
+
}
|
|
519
|
+
return {
|
|
520
|
+
allContainers: registry.containers.toSorted(
|
|
521
|
+
(a, b) => a.name.localeCompare(b.name)
|
|
522
|
+
),
|
|
523
|
+
boundaries: registry.boundaries
|
|
524
|
+
};
|
|
525
|
+
};
|
|
526
|
+
const loadStructurizrElements = async (filePath) => {
|
|
527
|
+
const workspace = await loadStructurizrWorkspace(filePath);
|
|
528
|
+
return mapContainersFromStructurizr(workspace);
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
const checkAcl = (containers, options) => {
|
|
532
|
+
const tag = options?.tag ?? "acl";
|
|
533
|
+
const externalType = options?.externalType ?? "System_Ext";
|
|
534
|
+
const violations = [];
|
|
535
|
+
for (const container of containers) {
|
|
536
|
+
const externalRelations = container.relations.filter(
|
|
537
|
+
(r) => r.to.type === externalType
|
|
538
|
+
);
|
|
539
|
+
if (!container.tags?.includes(tag) && externalRelations.length > 0) {
|
|
540
|
+
violations.push({
|
|
541
|
+
container: container.name,
|
|
542
|
+
message: `depends on external systems: ${externalRelations.map((r) => r.to.name).join(", ")}`
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return violations;
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
const checkAcyclic = (containers) => {
|
|
550
|
+
const violations = [];
|
|
551
|
+
const findCycle = (relations, sourceContainerName, visited = /* @__PURE__ */ new Set()) => {
|
|
552
|
+
for (const rel of relations) {
|
|
553
|
+
if (rel.to.name === sourceContainerName) {
|
|
554
|
+
return true;
|
|
555
|
+
}
|
|
556
|
+
if (visited.has(rel.to.name)) continue;
|
|
557
|
+
visited.add(rel.to.name);
|
|
558
|
+
if (findCycle(rel.to.relations, sourceContainerName, visited)) {
|
|
559
|
+
return true;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
return false;
|
|
563
|
+
};
|
|
564
|
+
for (const container of containers) {
|
|
565
|
+
if (findCycle(container.relations, container.name)) {
|
|
566
|
+
violations.push({
|
|
567
|
+
container: container.name,
|
|
568
|
+
message: "participates in a dependency cycle"
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return violations;
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
const checkApiGateway = (containers, options) => {
|
|
576
|
+
const aclTag = options?.aclTag ?? "acl";
|
|
577
|
+
const externalType = options?.externalType ?? "System_Ext";
|
|
578
|
+
const gatewayPattern = options?.gatewayPattern ?? /gateway/i;
|
|
579
|
+
const violations = [];
|
|
580
|
+
for (const container of containers) {
|
|
581
|
+
if (!container.tags?.includes(aclTag)) continue;
|
|
582
|
+
for (const rel of container.relations) {
|
|
583
|
+
if (rel.to.type !== externalType) continue;
|
|
584
|
+
const techs = rel.technology?.split(", ") ?? [];
|
|
585
|
+
if (!techs.some((t) => gatewayPattern.test(t))) {
|
|
586
|
+
violations.push({
|
|
587
|
+
container: container.name,
|
|
588
|
+
message: `external relation to ${rel.to.name} does not go through API Gateway`
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return violations;
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
const getBoundaryCohesion = (boundary) => {
|
|
597
|
+
let result = 0;
|
|
598
|
+
for (const container of boundary.containers) {
|
|
599
|
+
result += container.relations.filter(
|
|
600
|
+
(r) => boundary.containers.some((c) => c.name === r.to.name)
|
|
601
|
+
).length;
|
|
602
|
+
}
|
|
603
|
+
for (const innerBoundary of boundary.boundaries) {
|
|
604
|
+
result += getBoundaryCoupling(innerBoundary);
|
|
605
|
+
}
|
|
606
|
+
return result;
|
|
607
|
+
};
|
|
608
|
+
const getBoundaryCoupling = (boundary, externalType = "System_Ext", internalType = "Container") => {
|
|
609
|
+
let result = 0;
|
|
610
|
+
for (const container of boundary.containers) {
|
|
611
|
+
result += container.relations.filter(
|
|
612
|
+
(r) => r.to.type === internalType && !boundary.containers.some((c) => c.name === r.to.name)
|
|
613
|
+
).length;
|
|
614
|
+
}
|
|
615
|
+
for (const innerBoundary of boundary.boundaries) {
|
|
616
|
+
for (const container of innerBoundary.containers) {
|
|
617
|
+
result += container.relations.filter(
|
|
618
|
+
(r) => r.to.type === externalType
|
|
619
|
+
).length;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
return result;
|
|
623
|
+
};
|
|
624
|
+
const checkCohesion = (model, options) => {
|
|
625
|
+
const externalType = options?.externalType ?? "System_Ext";
|
|
626
|
+
const internalType = options?.internalType ?? "Container";
|
|
627
|
+
const violations = [];
|
|
628
|
+
for (const boundary of model.boundaries) {
|
|
629
|
+
const cohesion = getBoundaryCohesion(boundary);
|
|
630
|
+
const coupling = getBoundaryCoupling(boundary, externalType, internalType);
|
|
631
|
+
if (cohesion <= coupling) {
|
|
632
|
+
violations.push({
|
|
633
|
+
container: boundary.name,
|
|
634
|
+
message: `cohesion (${cohesion}) is not greater than coupling (${coupling})`
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
if (boundary.boundaries.length > 0) {
|
|
638
|
+
const innerCohesionSum = boundary.boundaries.reduce(
|
|
639
|
+
(sum, current) => sum + getBoundaryCohesion(current),
|
|
640
|
+
0
|
|
641
|
+
);
|
|
642
|
+
if (cohesion >= innerCohesionSum) {
|
|
643
|
+
violations.push({
|
|
644
|
+
container: boundary.name,
|
|
645
|
+
message: `cohesion (${cohesion}) is not less than sum of inner boundary cohesions (${innerCohesionSum})`
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
return violations;
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
const checkCrud = (containers, options) => {
|
|
654
|
+
const repoTags = options?.repoTags ?? ["repo", "relay"];
|
|
655
|
+
const dbType = options?.dbType ?? "ContainerDb";
|
|
656
|
+
const violations = [];
|
|
657
|
+
for (const container of containers) {
|
|
658
|
+
const dbRelations = container.relations.filter((r) => r.to.type === dbType);
|
|
659
|
+
const isRepo = repoTags.some((tag) => container.tags?.includes(tag));
|
|
660
|
+
if (!isRepo && dbRelations.length > 0) {
|
|
661
|
+
violations.push({
|
|
662
|
+
container: container.name,
|
|
663
|
+
message: `accesses database without repo/relay tag: ${dbRelations.map((r) => r.to.name).join(", ")}`
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
if (container.tags?.includes("repo") && container.relations.some((r) => r.to.type !== dbType)) {
|
|
667
|
+
violations.push({
|
|
668
|
+
container: container.name,
|
|
669
|
+
message: `repo has non-database dependencies: ${container.relations.filter((r) => r.to.type !== dbType).map((r) => r.to.name).join(", ")}`
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
return violations;
|
|
674
|
+
};
|
|
675
|
+
|
|
676
|
+
const checkDbPerService = (containers, options) => {
|
|
677
|
+
const dbType = options?.dbType ?? "ContainerDb";
|
|
678
|
+
const violations = [];
|
|
679
|
+
const dbAccessMap = /* @__PURE__ */ new Map();
|
|
680
|
+
for (const container of containers) {
|
|
681
|
+
for (const rel of container.relations) {
|
|
682
|
+
if (rel.to.type === dbType) {
|
|
683
|
+
const accessors = dbAccessMap.get(rel.to.name) ?? [];
|
|
684
|
+
accessors.push(container.name);
|
|
685
|
+
dbAccessMap.set(rel.to.name, accessors);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
for (const [db, accessors] of dbAccessMap) {
|
|
690
|
+
if (accessors.length > 1) {
|
|
691
|
+
violations.push({
|
|
692
|
+
container: db,
|
|
693
|
+
message: `accessed by multiple services: ${accessors.join(", ")}`
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
return violations;
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
const computeCoupling = (internal, internalNames) => {
|
|
701
|
+
const ca = /* @__PURE__ */ new Map();
|
|
702
|
+
const ce = /* @__PURE__ */ new Map();
|
|
703
|
+
for (const c of internal) {
|
|
704
|
+
ca.set(c.name, 0);
|
|
705
|
+
ce.set(c.name, 0);
|
|
706
|
+
}
|
|
707
|
+
for (const c of internal) {
|
|
708
|
+
for (const rel of c.relations) {
|
|
709
|
+
if (!internalNames.has(rel.to.name)) continue;
|
|
710
|
+
ce.set(c.name, ce.get(c.name) + 1);
|
|
711
|
+
ca.set(rel.to.name, ca.get(rel.to.name) + 1);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
return { ca, ce };
|
|
715
|
+
};
|
|
716
|
+
const checkStableDependencies = (containers, options) => {
|
|
717
|
+
const externalType = options?.externalType ?? "System_Ext";
|
|
718
|
+
const violations = [];
|
|
719
|
+
const internal = containers.filter((c) => c.type !== externalType);
|
|
720
|
+
const internalNames = new Set(internal.map((c) => c.name));
|
|
721
|
+
const { ca, ce } = computeCoupling(internal, internalNames);
|
|
722
|
+
const instability = (name) => {
|
|
723
|
+
const afferent = ca.get(name);
|
|
724
|
+
const efferent = ce.get(name);
|
|
725
|
+
if (afferent + efferent === 0) return 1;
|
|
726
|
+
return efferent / (afferent + efferent);
|
|
727
|
+
};
|
|
728
|
+
for (const c of internal) {
|
|
729
|
+
for (const rel of c.relations) {
|
|
730
|
+
if (!internalNames.has(rel.to.name)) continue;
|
|
731
|
+
const iSource = instability(c.name);
|
|
732
|
+
const iTarget = instability(rel.to.name);
|
|
733
|
+
if (iSource < iTarget) {
|
|
734
|
+
violations.push({
|
|
735
|
+
container: c.name,
|
|
736
|
+
message: `depends on less stable ${rel.to.name} (I=${iSource.toFixed(2)} \u2192 I=${iTarget.toFixed(2)})`
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
return violations;
|
|
742
|
+
};
|
|
743
|
+
|
|
744
|
+
export { AactConfigSchema as A, analyzeArchitecture as a, checkAcyclic as b, checkAcl as c, checkApiGateway as d, checkCohesion as e, checkCrud as f, checkDbPerService as g, checkStableDependencies as h, defineConfig as i, generateKubernetes as j, generatePlantumlFromModel as k, loadPlantumlElements as l, loadStructurizrElements as m, loadStructurizrWorkspace as n, mapContainersFromPlantumlElements as o, mapContainersFromStructurizr as p };
|