@hamak/smart-data-dico 1.0.4 → 1.1.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.
Files changed (49) hide show
  1. package/backend/dist/server.mjs +82213 -0
  2. package/bin/cli.js +28 -17
  3. package/package.json +28 -27
  4. package/backend/package.json +0 -51
  5. package/backend/src/__tests__/integration/api.test.ts +0 -149
  6. package/backend/src/__tests__/setup.ts +0 -24
  7. package/backend/src/__tests__/utils/testUtils.ts +0 -76
  8. package/backend/src/adapters/EntityFileAdapter.ts +0 -154
  9. package/backend/src/adapters/YamlFileInfoEnricher.ts +0 -52
  10. package/backend/src/controllers/authController.ts +0 -131
  11. package/backend/src/controllers/diagramController.ts +0 -143
  12. package/backend/src/controllers/dictionaryController.ts +0 -306
  13. package/backend/src/controllers/importExportController.ts +0 -64
  14. package/backend/src/controllers/perspectiveController.ts +0 -90
  15. package/backend/src/controllers/serviceController.ts +0 -418
  16. package/backend/src/controllers/stereotypeController.ts +0 -59
  17. package/backend/src/controllers/versionController.ts +0 -226
  18. package/backend/src/kernel/config.ts +0 -43
  19. package/backend/src/middleware/auth.ts +0 -128
  20. package/backend/src/middleware/jwtAuth.ts +0 -100
  21. package/backend/src/models/Dictionary.ts +0 -38
  22. package/backend/src/models/EntitySchema.ts +0 -393
  23. package/backend/src/models/__tests__/Dictionary.test.ts +0 -92
  24. package/backend/src/models/__tests__/EntitySchema.test.ts +0 -119
  25. package/backend/src/routes/index.ts +0 -120
  26. package/backend/src/scripts/migrate-to-uuid.ts +0 -24
  27. package/backend/src/server.ts +0 -158
  28. package/backend/src/services/__mocks__/entityService.ts +0 -38
  29. package/backend/src/services/__mocks__/serviceService.ts +0 -88
  30. package/backend/src/services/__mocks__/versionService.ts +0 -38
  31. package/backend/src/services/__tests__/dictionaryService.test.ts +0 -74
  32. package/backend/src/services/diagramService.ts +0 -165
  33. package/backend/src/services/dictionaryService.ts +0 -582
  34. package/backend/src/services/entityService.ts +0 -102
  35. package/backend/src/services/exportService.ts +0 -172
  36. package/backend/src/services/importService.ts +0 -208
  37. package/backend/src/services/perspectiveService.ts +0 -276
  38. package/backend/src/services/qualityService.ts +0 -121
  39. package/backend/src/services/serviceService.ts +0 -763
  40. package/backend/src/services/stereotypeService.ts +0 -98
  41. package/backend/src/services/versionService.ts +0 -135
  42. package/backend/src/setupTests.ts +0 -12
  43. package/backend/src/utils/__mocks__/fileOperations.ts +0 -116
  44. package/backend/src/utils/fileOperations.ts +0 -602
  45. package/backend/src/utils/logger.ts +0 -38
  46. package/backend/src/utils/migration.ts +0 -254
  47. package/backend/src/utils/swagger.ts +0 -358
  48. package/backend/src/utils/uuid.ts +0 -41
  49. package/backend/tsconfig.json +0 -20
@@ -1,763 +0,0 @@
1
- import { Entity, Relationship, Cardinality, EntityStatus, ReviewComment, LineageNode, LineageResult } from '../models/EntitySchema.js';
2
- import { stereotypeService } from './stereotypeService.js';
3
- import { logger } from '../utils/logger.js';
4
- import {
5
- listMicroservices,
6
- listMicroserviceEntities,
7
- readEntityFile,
8
- writeEntityFile,
9
- deleteEntityFile,
10
- listAllEntities,
11
- readRelationshipsFile,
12
- writeRelationshipsFile,
13
- getPackagePath,
14
- getAllRelationships,
15
- readComments,
16
- writeComments
17
- } from '../utils/fileOperations.js';
18
- import { generateUUID } from '../utils/uuid.js';
19
-
20
- /**
21
- * Interface for search result
22
- */
23
- interface SearchFilters {
24
- type?: string;
25
- service?: string;
26
- stereotype?: string;
27
- hasMetadata?: string;
28
- }
29
-
30
- interface SearchResult {
31
- type: 'entity' | 'attribute' | 'metadata' | 'relationship' | 'package';
32
- service: string;
33
- entityName: string;
34
- attributeName?: string;
35
- name: string;
36
- description: string;
37
- path: string;
38
- score: number;
39
- matchContext?: string;
40
- }
41
-
42
- interface ImpactAnalysis {
43
- relationships: { uuid: string; description: string; service: string; sourceEntity: string; targetEntity: string }[];
44
- perspectives: { uuid: string; name: string; path: string }[];
45
- diagrams: { id: string; name: string }[];
46
- }
47
-
48
- /**
49
- * Interface for graph data
50
- */
51
- interface GraphNode {
52
- id: string;
53
- label: string;
54
- type: 'entity';
55
- service: string;
56
- }
57
-
58
- interface GraphEdge {
59
- id: string;
60
- source: string;
61
- target: string;
62
- label: string;
63
- sourceCardinality: string;
64
- targetCardinality: string;
65
- }
66
-
67
- interface GraphData {
68
- nodes: GraphNode[];
69
- edges: GraphEdge[];
70
- }
71
-
72
- /**
73
- * Service for managing services and entities
74
- */
75
- export class ServiceService {
76
- async getAllServices(): Promise<string[]> {
77
- logger.info('Getting all services');
78
- try {
79
- return await listMicroservices();
80
- } catch (error) {
81
- logger.error(`Error getting all services: ${error}`);
82
- return [];
83
- }
84
- }
85
-
86
- async getServiceEntities(service: string): Promise<Entity[]> {
87
- logger.info(`Getting entities for service: ${service}`);
88
- const startTime = process.hrtime();
89
-
90
- try {
91
- const listStartTime = process.hrtime();
92
- const entityNames = await listMicroserviceEntities(service);
93
- const listEndTime = process.hrtime(listStartTime);
94
- const listTimeMs = Number((listEndTime[0] * 1e3 + listEndTime[1] / 1e6).toFixed(2));
95
- logger.info(`Listed ${entityNames.length} entity names for service ${service} in ${listTimeMs}ms`);
96
-
97
- const entities: Entity[] = [];
98
-
99
- const readStartTime = process.hrtime();
100
- for (const entityName of entityNames) {
101
- const entity = await readEntityFile(service, entityName);
102
- if (entity) {
103
- entities.push(entity);
104
- }
105
- }
106
- const readEndTime = process.hrtime(readStartTime);
107
- const readTimeMs = Number((readEndTime[0] * 1e3 + readEndTime[1] / 1e6).toFixed(2));
108
- logger.info(`Read ${entities.length} entity files for service ${service} in ${readTimeMs}ms`);
109
-
110
- const endTime = process.hrtime(startTime);
111
- const totalTimeMs = Number((endTime[0] * 1e3 + endTime[1] / 1e6).toFixed(2));
112
- logger.info(`Total time to get entities for service ${service}: ${totalTimeMs}ms (list: ${listTimeMs}ms, read: ${readTimeMs}ms)`);
113
-
114
- return entities;
115
- } catch (error) {
116
- logger.error(`Error getting service entities: ${error}`);
117
- return [];
118
- }
119
- }
120
-
121
- async getEntitySchema(service: string, entityName: string): Promise<Entity | null> {
122
- logger.info(`Getting entity schema: ${service}.${entityName}`);
123
- try {
124
- return await readEntityFile(service, entityName);
125
- } catch (error) {
126
- logger.error(`Error getting entity schema: ${error}`);
127
- return null;
128
- }
129
- }
130
-
131
- async createEntity(service: string, entity: Entity): Promise<{ success: boolean; errors: string[] }> {
132
- logger.info(`Creating entity: ${service}.${entity.name}`);
133
-
134
- try {
135
- const existingEntity = await readEntityFile(service, entity.name);
136
- if (existingEntity) {
137
- return {
138
- success: false,
139
- errors: [`Entity ${service}.${entity.name} already exists`]
140
- };
141
- }
142
-
143
- // Validate metadata against stereotype
144
- if (entity.stereotype) {
145
- const stereotype = await stereotypeService.getStereotype(entity.stereotype);
146
- if (stereotype) {
147
- const metadataErrors = stereotypeService.validateMetadata(stereotype, entity.metadata);
148
- if (metadataErrors.length > 0) {
149
- return { success: false, errors: metadataErrors };
150
- }
151
- }
152
- }
153
-
154
- entity.status = entity.status || EntityStatus.DRAFT;
155
- entity.createdAt = new Date().toISOString();
156
- entity.updatedAt = new Date().toISOString();
157
-
158
- const result = await writeEntityFile(entity, service);
159
-
160
- return {
161
- success: result,
162
- errors: result ? [] : ['Failed to write entity file']
163
- };
164
- } catch (error) {
165
- logger.error(`Error creating entity: ${error}`);
166
- return {
167
- success: false,
168
- errors: [`Error creating entity: ${error}`]
169
- };
170
- }
171
- }
172
-
173
- async updateEntity(service: string, entity: Entity): Promise<{ success: boolean; errors: string[] }> {
174
- logger.info(`Updating entity: ${service}.${entity.name}`);
175
-
176
- try {
177
- const existingEntity = await readEntityFile(service, entity.name);
178
- if (!existingEntity) {
179
- return {
180
- success: false,
181
- errors: [`Entity ${service}.${entity.name} not found`]
182
- };
183
- }
184
-
185
- // Validate metadata against stereotype
186
- if (entity.stereotype) {
187
- const stereotype = await stereotypeService.getStereotype(entity.stereotype);
188
- if (stereotype) {
189
- const metadataErrors = stereotypeService.validateMetadata(stereotype, entity.metadata);
190
- if (metadataErrors.length > 0) {
191
- return { success: false, errors: metadataErrors };
192
- }
193
- }
194
- }
195
-
196
- entity.createdAt = existingEntity.createdAt;
197
- entity.updatedAt = new Date().toISOString();
198
-
199
- const result = await writeEntityFile(entity, service);
200
-
201
- return {
202
- success: result,
203
- errors: result ? [] : ['Failed to write entity file']
204
- };
205
- } catch (error) {
206
- logger.error(`Error updating entity: ${error}`);
207
- return {
208
- success: false,
209
- errors: [`Error updating entity: ${error}`]
210
- };
211
- }
212
- }
213
-
214
- async deleteEntity(service: string, entityName: string): Promise<{ success: boolean; errors: string[] }> {
215
- logger.info(`Deleting entity: ${service}.${entityName}`);
216
-
217
- try {
218
- const existingEntity = await readEntityFile(service, entityName);
219
- if (!existingEntity) {
220
- return {
221
- success: false,
222
- errors: [`Entity ${service}.${entityName} not found`]
223
- };
224
- }
225
-
226
- // Check if entity is referenced in package-level relationships
227
- const packagePath = getPackagePath(service);
228
- const relationships = await readRelationshipsFile(packagePath);
229
- const referencingRels = relationships.filter(
230
- rel => rel.source.entity === existingEntity.uuid || rel.target.entity === existingEntity.uuid
231
- );
232
-
233
- if (referencingRels.length > 0) {
234
- return {
235
- success: false,
236
- errors: [
237
- `Cannot delete entity ${service}.${entityName} because it is referenced in ${referencingRels.length} relationship(s). Remove the relationships first.`
238
- ]
239
- };
240
- }
241
-
242
- const result = await deleteEntityFile(service, entityName);
243
-
244
- return {
245
- success: result,
246
- errors: result ? [] : ['Failed to delete entity file']
247
- };
248
- } catch (error) {
249
- logger.error(`Error deleting entity: ${error}`);
250
- return {
251
- success: false,
252
- errors: [`Error deleting entity: ${error}`]
253
- };
254
- }
255
- }
256
-
257
- // --- Relationship CRUD ---
258
-
259
- async getPackageRelationships(packageName: string): Promise<Relationship[]> {
260
- const packagePath = getPackagePath(packageName);
261
- return await readRelationshipsFile(packagePath);
262
- }
263
-
264
- async createRelationship(packageName: string, relationship: Relationship): Promise<{ success: boolean; errors: string[]; relationship?: Relationship }> {
265
- try {
266
- if (!relationship.uuid) {
267
- relationship.uuid = generateUUID();
268
- }
269
-
270
- const packagePath = getPackagePath(packageName);
271
- const relationships = await readRelationshipsFile(packagePath);
272
- relationships.push(relationship);
273
-
274
- const result = await writeRelationshipsFile(packagePath, relationships);
275
- return {
276
- success: result,
277
- errors: result ? [] : ['Failed to write relationships file'],
278
- relationship: result ? relationship : undefined
279
- };
280
- } catch (error) {
281
- logger.error(`Error creating relationship: ${error}`);
282
- return { success: false, errors: [`Error creating relationship: ${error}`] };
283
- }
284
- }
285
-
286
- async updateRelationship(packageName: string, uuid: string, relationship: Relationship): Promise<{ success: boolean; errors: string[] }> {
287
- try {
288
- const packagePath = getPackagePath(packageName);
289
- const relationships = await readRelationshipsFile(packagePath);
290
- const index = relationships.findIndex(r => r.uuid === uuid);
291
-
292
- if (index === -1) {
293
- return { success: false, errors: [`Relationship ${uuid} not found`] };
294
- }
295
-
296
- relationships[index] = { ...relationship, uuid };
297
- const result = await writeRelationshipsFile(packagePath, relationships);
298
- return {
299
- success: result,
300
- errors: result ? [] : ['Failed to write relationships file']
301
- };
302
- } catch (error) {
303
- logger.error(`Error updating relationship: ${error}`);
304
- return { success: false, errors: [`Error updating relationship: ${error}`] };
305
- }
306
- }
307
-
308
- async deleteRelationship(packageName: string, uuid: string): Promise<{ success: boolean; errors: string[] }> {
309
- try {
310
- const packagePath = getPackagePath(packageName);
311
- const relationships = await readRelationshipsFile(packagePath);
312
- const index = relationships.findIndex(r => r.uuid === uuid);
313
-
314
- if (index === -1) {
315
- return { success: false, errors: [`Relationship ${uuid} not found`] };
316
- }
317
-
318
- relationships.splice(index, 1);
319
- const result = await writeRelationshipsFile(packagePath, relationships);
320
- return {
321
- success: result,
322
- errors: result ? [] : ['Failed to write relationships file']
323
- };
324
- } catch (error) {
325
- logger.error(`Error deleting relationship: ${error}`);
326
- return { success: false, errors: [`Error deleting relationship: ${error}`] };
327
- }
328
- }
329
-
330
- // --- Search ---
331
-
332
- async searchEntities(query: string, filters?: SearchFilters): Promise<SearchResult[]> {
333
- logger.info(`Searching with query: ${query}, filters: ${JSON.stringify(filters)}`);
334
-
335
- try {
336
- const results: SearchResult[] = [];
337
- const allEntities = await listAllEntities();
338
- const searchTerms = query.toLowerCase().split(/\s+/);
339
- const microservices = await listMicroservices();
340
-
341
- // Search packages
342
- if (!filters?.type || filters.type === 'package') {
343
- for (const ms of microservices) {
344
- if (filters?.service && filters.service !== ms) continue;
345
- const score = this.calculateMatchScore(ms, searchTerms);
346
- if (score > 0) {
347
- results.push({ type: 'package', service: ms, entityName: '', name: ms, description: '', path: ms, score });
348
- }
349
- }
350
- }
351
-
352
- for (const entityInfo of allEntities) {
353
- if (filters?.service && filters.service !== entityInfo.microservice) continue;
354
-
355
- // Extract clean entity name from UUID-prefixed filename (e.g. "uuid_Order" → "Order")
356
- const cleanName = entityInfo.name.includes('_') ? entityInfo.name.split('_').slice(1).join('_') : entityInfo.name;
357
- const entity = await readEntityFile(entityInfo.microservice, cleanName);
358
- if (!entity) continue;
359
-
360
- // Stereotype filter
361
- if (filters?.stereotype && entity.stereotype !== filters.stereotype) continue;
362
-
363
- // hasMetadata filter
364
- if (filters?.hasMetadata) {
365
- const hasIt = entity.metadata?.some(m => m.name === filters.hasMetadata) ||
366
- entity.attributes.some(a => a.metadata?.some(m => m.name === filters.hasMetadata));
367
- if (!hasIt) continue;
368
- }
369
-
370
- // Search entity name/description
371
- if (!filters?.type || filters.type === 'entity') {
372
- const entityNameMatch = this.calculateMatchScore(entity.name, searchTerms);
373
- const entityDescMatch = this.calculateMatchScore(entity.description || '', searchTerms);
374
- const stereotypeMatch = entity.stereotype ? this.calculateMatchScore(entity.stereotype, searchTerms) : 0;
375
-
376
- if (entityNameMatch > 0 || entityDescMatch > 0 || stereotypeMatch > 0) {
377
- results.push({
378
- type: 'entity', service: entityInfo.microservice, entityName: entity.name,
379
- name: entity.name, description: entity.description || '',
380
- path: `${entityInfo.microservice}/${entity.name}`,
381
- score: Math.max(entityNameMatch * 2, entityDescMatch, stereotypeMatch * 1.5),
382
- matchContext: stereotypeMatch > 0 ? `stereotype: ${entity.stereotype}` : undefined,
383
- });
384
- }
385
- }
386
-
387
- // Search attributes
388
- if (!filters?.type || filters.type === 'attribute') {
389
- for (const attr of entity.attributes) {
390
- const attrNameMatch = this.calculateMatchScore(attr.name, searchTerms);
391
- const attrDescMatch = this.calculateMatchScore(attr.description, searchTerms);
392
-
393
- if (attrNameMatch > 0 || attrDescMatch > 0) {
394
- results.push({
395
- type: 'attribute', service: entityInfo.microservice, entityName: entity.name,
396
- attributeName: attr.name, name: attr.name, description: attr.description,
397
- path: `${entityInfo.microservice}/${entity.name}/${attr.name}`,
398
- score: Math.max(attrNameMatch * 1.5, attrDescMatch),
399
- });
400
- }
401
- }
402
- }
403
-
404
- // Search entity metadata
405
- if (!filters?.type || filters.type === 'metadata') {
406
- // Handle metadata as array [{name,value}] or object {key: value}
407
- const entityMeta = this.normalizeMetadata(entity.metadata);
408
- for (const m of entityMeta) {
409
- const nameMatch = this.calculateMatchScore(m.name, searchTerms);
410
- const valueMatch = this.calculateMatchScore(String(m.value), searchTerms);
411
-
412
- if (nameMatch > 0 || valueMatch > 0) {
413
- results.push({
414
- type: 'metadata', service: entityInfo.microservice, entityName: entity.name,
415
- name: m.name, description: `${m.name} = ${m.value}`,
416
- path: `${entityInfo.microservice}/${entity.name}`,
417
- score: Math.max(nameMatch * 1.5, valueMatch),
418
- matchContext: `on entity ${entity.name}`,
419
- });
420
- }
421
- }
422
-
423
- // Search attribute metadata
424
- for (const attr of entity.attributes) {
425
- const attrMeta = this.normalizeMetadata(attr.metadata);
426
- for (const m of attrMeta) {
427
- const nameMatch = this.calculateMatchScore(m.name, searchTerms);
428
- const valueMatch = this.calculateMatchScore(String(m.value), searchTerms);
429
-
430
- if (nameMatch > 0 || valueMatch > 0) {
431
- results.push({
432
- type: 'metadata', service: entityInfo.microservice, entityName: entity.name,
433
- attributeName: attr.name, name: m.name, description: `${m.name} = ${m.value}`,
434
- path: `${entityInfo.microservice}/${entity.name}/${attr.name}`,
435
- score: Math.max(nameMatch * 1.5, valueMatch),
436
- matchContext: `on ${entity.name}.${attr.name}`,
437
- });
438
- }
439
- }
440
- }
441
- }
442
- }
443
-
444
- // Search relationships
445
- if (!filters?.type || filters.type === 'relationship') {
446
- const allRels = await getAllRelationships();
447
- // Build entity name map for display
448
- const entityNameMap = new Map<string, { name: string; service: string }>();
449
- for (const entityInfo of allEntities) {
450
- const e = await readEntityFile(entityInfo.microservice, entityInfo.name);
451
- if (e) entityNameMap.set(e.uuid, { name: e.name, service: entityInfo.microservice });
452
- }
453
-
454
- for (const { packageName, relationships } of allRels) {
455
- if (filters?.service && filters.service !== packageName) continue;
456
- for (const rel of relationships) {
457
- const descMatch = this.calculateMatchScore(rel.description || '', searchTerms);
458
- const srcInfo = entityNameMap.get(rel.source.entity);
459
- const tgtInfo = entityNameMap.get(rel.target.entity);
460
- const srcMatch = srcInfo ? this.calculateMatchScore(srcInfo.name, searchTerms) : 0;
461
- const tgtMatch = tgtInfo ? this.calculateMatchScore(tgtInfo.name, searchTerms) : 0;
462
-
463
- if (descMatch > 0 || srcMatch > 0 || tgtMatch > 0) {
464
- results.push({
465
- type: 'relationship', service: packageName, entityName: srcInfo?.name || rel.source.entity,
466
- name: rel.description || rel.uuid,
467
- description: `${srcInfo?.name || '?'} → ${tgtInfo?.name || '?'}`,
468
- path: `${packageName}/relationships/${rel.uuid}`,
469
- score: Math.max(descMatch, srcMatch, tgtMatch),
470
- });
471
- }
472
- }
473
- }
474
- }
475
-
476
- return results.sort((a, b) => b.score - a.score);
477
- } catch (error) {
478
- logger.error(`Error searching entities: ${error}`);
479
- return [];
480
- }
481
- }
482
-
483
- private normalizeMetadata(metadata: any): Array<{ name: string; value: any }> {
484
- if (!metadata) return [];
485
- if (Array.isArray(metadata)) return metadata;
486
- // Object format: { key: value } → [{name: key, value}]
487
- if (typeof metadata === 'object') {
488
- return Object.entries(metadata).map(([name, value]) => ({ name, value }));
489
- }
490
- return [];
491
- }
492
-
493
- private calculateMatchScore(text: string, searchTerms: string[]): number {
494
- if (!text) return 0;
495
-
496
- const normalizedText = text.toLowerCase();
497
- let score = 0;
498
-
499
- for (const term of searchTerms) {
500
- if (normalizedText.includes(term)) {
501
- score += 1;
502
- const regex = new RegExp(`\\b${term}\\b`, 'i');
503
- if (regex.test(normalizedText)) {
504
- score += 0.5;
505
- }
506
- }
507
- }
508
-
509
- return score;
510
- }
511
-
512
- // --- Impact Analysis ---
513
-
514
- async getImpactAnalysis(entityUuid: string): Promise<ImpactAnalysis> {
515
- const impact: ImpactAnalysis = { relationships: [], perspectives: [], diagrams: [] };
516
-
517
- try {
518
- // Find relationships referencing this entity
519
- const allRels = await getAllRelationships();
520
- const allEntities = await listAllEntities();
521
- const entityNameMap = new Map<string, string>();
522
- for (const info of allEntities) {
523
- const e = await readEntityFile(info.microservice, info.name);
524
- if (e) entityNameMap.set(e.uuid, e.name);
525
- }
526
-
527
- for (const { packageName, relationships } of allRels) {
528
- for (const rel of relationships) {
529
- if (rel.source.entity === entityUuid || rel.target.entity === entityUuid) {
530
- impact.relationships.push({
531
- uuid: rel.uuid,
532
- description: rel.description || '',
533
- service: packageName,
534
- sourceEntity: entityNameMap.get(rel.source.entity) || rel.source.entity,
535
- targetEntity: entityNameMap.get(rel.target.entity) || rel.target.entity,
536
- });
537
- }
538
- }
539
- }
540
-
541
- // Find perspectives containing this entity
542
- const { listPerspectives } = await import('../utils/fileOperations.js');
543
- const perspectives = await listPerspectives();
544
- const { perspectiveService } = await import('./perspectiveService.js');
545
- for (const p of perspectives) {
546
- const resolved = await perspectiveService.resolve(p.uuid);
547
- if (resolved?.resolvedNodes.some(n => n.entityUuid === entityUuid)) {
548
- const paths = resolved.resolvedNodes.filter(n => n.entityUuid === entityUuid).map(n => n.path);
549
- impact.perspectives.push({ uuid: p.uuid, name: p.name, path: paths[0] });
550
- }
551
- }
552
-
553
- // Find diagrams referencing this entity
554
- const { diagramService } = await import('./diagramService.js');
555
- const layouts = await diagramService.listDiagramLayouts();
556
- for (const layout of layouts) {
557
- if (layout.entities && layout.entities[entityUuid]) {
558
- impact.diagrams.push({ id: layout.id, name: layout.name });
559
- }
560
- }
561
- } catch (error) {
562
- logger.error(`Error getting impact analysis: ${error}`);
563
- }
564
-
565
- return impact;
566
- }
567
-
568
- // --- Lineage ---
569
-
570
- async getLineage(entityUuid: string): Promise<LineageResult> {
571
- // Build entity name map
572
- const allEntities = await listAllEntities();
573
- const entityNameMap = new Map<string, { name: string; service: string }>();
574
- for (const info of allEntities) {
575
- const cleanName = info.name.includes('_') ? info.name.split('_').slice(1).join('_') : info.name;
576
- const e = await readEntityFile(info.microservice, cleanName);
577
- if (e) entityNameMap.set(e.uuid, { name: e.name, service: info.microservice });
578
- }
579
-
580
- const entityInfo = entityNameMap.get(entityUuid) || { name: entityUuid, service: '' };
581
- const result: LineageResult = {
582
- entity: { uuid: entityUuid, name: entityInfo.name, service: entityInfo.service },
583
- upstream: [],
584
- downstream: [],
585
- };
586
-
587
- // Collect all lineage relationships
588
- const allRels = await getAllRelationships();
589
- const lineageRels: Relationship[] = [];
590
- for (const { relationships } of allRels) {
591
- for (const rel of relationships) {
592
- if (rel.type === 'lineage') lineageRels.push(rel);
593
- }
594
- }
595
-
596
- // Walk upstream (follow source chains): entity is target, find sources
597
- const visitedUp = new Set<string>();
598
- const walkUpstream = (uuid: string, depth: number) => {
599
- if (visitedUp.has(uuid) || depth > 10) return;
600
- visitedUp.add(uuid);
601
- for (const rel of lineageRels) {
602
- if (rel.target.entity === uuid) {
603
- const srcInfo = entityNameMap.get(rel.source.entity);
604
- if (srcInfo) {
605
- result.upstream.push({
606
- entityUuid: rel.source.entity,
607
- entityName: srcInfo.name,
608
- service: srcInfo.service,
609
- direction: 'upstream',
610
- depth,
611
- relationship: { uuid: rel.uuid, description: rel.description || '' },
612
- });
613
- walkUpstream(rel.source.entity, depth + 1);
614
- }
615
- }
616
- }
617
- };
618
-
619
- // Walk downstream (follow target chains): entity is source, find targets
620
- const visitedDown = new Set<string>();
621
- const walkDownstream = (uuid: string, depth: number) => {
622
- if (visitedDown.has(uuid) || depth > 10) return;
623
- visitedDown.add(uuid);
624
- for (const rel of lineageRels) {
625
- if (rel.source.entity === uuid) {
626
- const tgtInfo = entityNameMap.get(rel.target.entity);
627
- if (tgtInfo) {
628
- result.downstream.push({
629
- entityUuid: rel.target.entity,
630
- entityName: tgtInfo.name,
631
- service: tgtInfo.service,
632
- direction: 'downstream',
633
- depth,
634
- relationship: { uuid: rel.uuid, description: rel.description || '' },
635
- });
636
- walkDownstream(rel.target.entity, depth + 1);
637
- }
638
- }
639
- }
640
- };
641
-
642
- walkUpstream(entityUuid, 1);
643
- walkDownstream(entityUuid, 1);
644
-
645
- return result;
646
- }
647
-
648
- // --- Status transitions ---
649
-
650
- async changeEntityStatus(service: string, entityName: string, newStatus: EntityStatus): Promise<{ success: boolean; errors: string[] }> {
651
- const entity = await readEntityFile(service, entityName);
652
- if (!entity) return { success: false, errors: ['Entity not found'] };
653
-
654
- const current = entity.status || EntityStatus.DRAFT;
655
- const validTransitions: Record<string, EntityStatus[]> = {
656
- [EntityStatus.DRAFT]: [EntityStatus.SUBMITTED],
657
- [EntityStatus.SUBMITTED]: [EntityStatus.APPROVED, EntityStatus.RETURNED],
658
- [EntityStatus.RETURNED]: [EntityStatus.SUBMITTED],
659
- [EntityStatus.APPROVED]: [EntityStatus.DRAFT], // re-open for editing
660
- };
661
-
662
- if (!validTransitions[current]?.includes(newStatus)) {
663
- return { success: false, errors: [`Cannot transition from ${current} to ${newStatus}`] };
664
- }
665
-
666
- entity.status = newStatus;
667
- entity.updatedAt = new Date().toISOString();
668
- const result = await writeEntityFile(entity, service);
669
- return { success: result, errors: result ? [] : ['Failed to write entity'] };
670
- }
671
-
672
- // --- Comments ---
673
-
674
- async getComments(service: string, entityName: string): Promise<ReviewComment[]> {
675
- const entity = await readEntityFile(service, entityName);
676
- if (!entity) return [];
677
- return readComments(service, entity.uuid);
678
- }
679
-
680
- async addComment(service: string, entityName: string, comment: Omit<ReviewComment, 'id' | 'timestamp'>): Promise<{ success: boolean; comment?: ReviewComment; errors?: string[] }> {
681
- const entity = await readEntityFile(service, entityName);
682
- if (!entity) return { success: false, errors: ['Entity not found'] };
683
-
684
- const comments = await readComments(service, entity.uuid);
685
- const newComment: ReviewComment = {
686
- id: `c-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
687
- author: comment.author,
688
- timestamp: new Date().toISOString(),
689
- message: comment.message,
690
- targetField: comment.targetField,
691
- resolved: false,
692
- };
693
- comments.push(newComment);
694
- const ok = await writeComments(service, entity.uuid, comments);
695
- return ok ? { success: true, comment: newComment } : { success: false, errors: ['Failed to write comments'] };
696
- }
697
-
698
- async resolveComment(service: string, entityName: string, commentId: string): Promise<{ success: boolean; errors?: string[] }> {
699
- const entity = await readEntityFile(service, entityName);
700
- if (!entity) return { success: false, errors: ['Entity not found'] };
701
-
702
- const comments = await readComments(service, entity.uuid);
703
- const comment = comments.find(c => c.id === commentId);
704
- if (!comment) return { success: false, errors: ['Comment not found'] };
705
-
706
- comment.resolved = true;
707
- const ok = await writeComments(service, entity.uuid, comments);
708
- return ok ? { success: true } : { success: false, errors: ['Failed to write comments'] };
709
- }
710
-
711
- /**
712
- * Get graph data for visualization - reads relationships from package-level file
713
- */
714
- async getGraphData(service: string): Promise<GraphData> {
715
- logger.info(`Generating graph data for service: ${service}`);
716
-
717
- try {
718
- const graphData: GraphData = {
719
- nodes: [],
720
- edges: []
721
- };
722
-
723
- const entities = await this.getServiceEntities(service);
724
- const entityUuidToName = new Map<string, string>();
725
-
726
- for (const entity of entities) {
727
- entityUuidToName.set(entity.uuid, entity.name);
728
- graphData.nodes.push({
729
- id: entity.uuid,
730
- label: entity.name,
731
- type: 'entity',
732
- service
733
- });
734
- }
735
-
736
- // Read relationships from package-level file
737
- const packagePath = getPackagePath(service);
738
- const relationships = await readRelationshipsFile(packagePath);
739
-
740
- for (const rel of relationships) {
741
- const sourceName = entityUuidToName.get(rel.source.entity) || rel.source.entity;
742
- const targetName = entityUuidToName.get(rel.target.entity) || rel.target.entity;
743
-
744
- graphData.edges.push({
745
- id: rel.uuid,
746
- source: rel.source.entity,
747
- target: rel.target.entity,
748
- label: rel.target.name || `${sourceName} -> ${targetName}`,
749
- sourceCardinality: rel.source.cardinality,
750
- targetCardinality: rel.target.cardinality
751
- });
752
- }
753
-
754
- return graphData;
755
- } catch (error) {
756
- logger.error(`Error generating graph data: ${error}`);
757
- return { nodes: [], edges: [] };
758
- }
759
- }
760
- }
761
-
762
- // Export a singleton instance
763
- export const serviceService = new ServiceService();