@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,172 +0,0 @@
1
- import { Entity, AttributeType, Attribute, Relationship } from '../models/EntitySchema.js';
2
- import { listMicroserviceEntities, readEntityFile, readRelationshipsFile, getPackagePath } from '../utils/fileOperations.js';
3
- import { logger } from '../utils/logger.js';
4
-
5
- const ATTR_TO_JSON_SCHEMA: Record<string, string> = {
6
- [AttributeType.STRING]: 'string',
7
- [AttributeType.NUMBER]: 'number',
8
- [AttributeType.INTEGER]: 'integer',
9
- [AttributeType.BOOLEAN]: 'boolean',
10
- [AttributeType.DATETIME]: 'string',
11
- [AttributeType.DATE]: 'string',
12
- [AttributeType.TIME]: 'string',
13
- [AttributeType.DATE_TIME]: 'string',
14
- [AttributeType.TIMESTAMP]: 'string',
15
- [AttributeType.DURATION]: 'string',
16
- [AttributeType.ENUM]: 'string',
17
- [AttributeType.OBJECT]: 'object',
18
- [AttributeType.ARRAY]: 'array',
19
- };
20
-
21
- class ExportService {
22
- async exportToJsonSchema(service: string): Promise<any> {
23
- const entityNames = await listMicroserviceEntities(service);
24
- const definitions: Record<string, any> = {};
25
-
26
- for (const rawName of entityNames) {
27
- const name = rawName.includes('_') ? rawName.split('_').slice(1).join('_') : rawName;
28
- const entity = await readEntityFile(service, name);
29
- if (!entity || !Array.isArray(entity.attributes)) continue;
30
-
31
- const properties: Record<string, any> = {};
32
- const required: string[] = [];
33
-
34
- for (const attr of entity.attributes) {
35
- properties[attr.name] = this.attributeToJsonSchema(attr);
36
- if (attr.required) required.push(attr.name);
37
- }
38
-
39
- definitions[entity.name] = {
40
- type: 'object',
41
- description: entity.description || undefined,
42
- properties,
43
- ...(required.length > 0 ? { required } : {}),
44
- };
45
- }
46
-
47
- return {
48
- $schema: 'http://json-schema.org/draft-07/schema#',
49
- title: `${service} Data Dictionary`,
50
- definitions,
51
- };
52
- }
53
-
54
- private attributeToJsonSchema(attr: Attribute): any {
55
- const schema: any = {
56
- type: ATTR_TO_JSON_SCHEMA[attr.type] || 'string',
57
- description: attr.description || undefined,
58
- };
59
-
60
- if (attr.constraints) {
61
- if (attr.constraints.minLength !== undefined) schema.minLength = attr.constraints.minLength;
62
- if (attr.constraints.maxLength !== undefined) schema.maxLength = attr.constraints.maxLength;
63
- if (attr.constraints.pattern) schema.pattern = attr.constraints.pattern;
64
- if (attr.constraints.minimum !== undefined) schema.minimum = attr.constraints.minimum;
65
- if (attr.constraints.maximum !== undefined) schema.maximum = attr.constraints.maximum;
66
- if (attr.constraints.enumValues) {
67
- schema.enum = attr.constraints.enumValues;
68
- }
69
- }
70
-
71
- if (attr.type === AttributeType.DATETIME || attr.type === AttributeType.DATE_TIME) {
72
- schema.format = 'date-time';
73
- } else if (attr.type === AttributeType.DATE) {
74
- schema.format = 'date';
75
- } else if (attr.type === AttributeType.TIME) {
76
- schema.format = 'time';
77
- }
78
-
79
- if (attr.defaultValue !== undefined) schema.default = attr.defaultValue;
80
-
81
- return schema;
82
- }
83
-
84
- async exportToMarkdown(service: string): Promise<string> {
85
- const entityNames = await listMicroserviceEntities(service);
86
- const entities: Entity[] = [];
87
- for (const rawName of entityNames) {
88
- const name = rawName.includes('_') ? rawName.split('_').slice(1).join('_') : rawName;
89
- const entity = await readEntityFile(service, name);
90
- if (entity) entities.push(entity);
91
- }
92
-
93
- let relationships: Relationship[] = [];
94
- try {
95
- relationships = await readRelationshipsFile(getPackagePath(service));
96
- } catch { /* ok */ }
97
-
98
- const lines: string[] = [];
99
- lines.push(`# ${service} Data Dictionary`);
100
- lines.push('');
101
- lines.push(`> Generated on ${new Date().toISOString().split('T')[0]}`);
102
- lines.push('');
103
- lines.push(`## Summary`);
104
- lines.push('');
105
- lines.push(`- **Entities**: ${entities.length}`);
106
- lines.push(`- **Relationships**: ${relationships.length}`);
107
- lines.push('');
108
-
109
- // Table of contents
110
- lines.push('## Entities');
111
- lines.push('');
112
- for (const entity of entities) {
113
- lines.push(`- [${entity.name}](#${entity.name.toLowerCase()})`);
114
- }
115
- lines.push('');
116
-
117
- // Entity details
118
- for (const entity of entities) {
119
- lines.push(`---`);
120
- lines.push('');
121
- lines.push(`### ${entity.name}`);
122
- lines.push('');
123
- if (entity.description) {
124
- lines.push(entity.description);
125
- lines.push('');
126
- }
127
- if (entity.stereotype) {
128
- lines.push(`**Stereotype**: ${entity.stereotype}`);
129
- lines.push('');
130
- }
131
-
132
- // Attributes table
133
- lines.push('| Attribute | Type | Required | Description |');
134
- lines.push('|-----------|------|----------|-------------|');
135
- for (const attr of entity.attributes) {
136
- const req = attr.required ? 'Yes' : 'No';
137
- const desc = (attr.description || '').replace(/\|/g, '\\|');
138
- const pk = attr.primaryKey ? ' (PK)' : '';
139
- lines.push(`| ${attr.name}${pk} | ${attr.type} | ${req} | ${desc} |`);
140
- }
141
- lines.push('');
142
-
143
- // Metadata
144
- if (entity.metadata && entity.metadata.length > 0) {
145
- lines.push('**Metadata**:');
146
- for (const m of entity.metadata) {
147
- lines.push(`- ${m.name}: ${m.value}`);
148
- }
149
- lines.push('');
150
- }
151
- }
152
-
153
- // Relationships
154
- if (relationships.length > 0) {
155
- lines.push('---');
156
- lines.push('');
157
- lines.push('## Relationships');
158
- lines.push('');
159
- lines.push('| Description | Source | Target | Cardinality |');
160
- lines.push('|-------------|--------|--------|-------------|');
161
- for (const rel of relationships) {
162
- const card = `${rel.source.cardinality}:${rel.target.cardinality}`;
163
- lines.push(`| ${rel.description || '-'} | ${rel.source.entity.slice(0, 8)}... | ${rel.target.entity.slice(0, 8)}... | ${card} |`);
164
- }
165
- lines.push('');
166
- }
167
-
168
- return lines.join('\n');
169
- }
170
- }
171
-
172
- export const exportService = new ExportService();
@@ -1,208 +0,0 @@
1
- import { Entity, Attribute, AttributeType, EntityStatus } from '../models/EntitySchema.js';
2
- import { writeEntityFile } from '../utils/fileOperations.js';
3
- import { generateUUID } from '../utils/uuid.js';
4
- import { logger } from '../utils/logger.js';
5
-
6
- const JSON_SCHEMA_TYPE_MAP: Record<string, AttributeType> = {
7
- string: AttributeType.STRING,
8
- number: AttributeType.NUMBER,
9
- integer: AttributeType.INTEGER,
10
- boolean: AttributeType.BOOLEAN,
11
- object: AttributeType.OBJECT,
12
- array: AttributeType.ARRAY,
13
- };
14
-
15
- const SQL_TYPE_MAP: Record<string, AttributeType> = {
16
- varchar: AttributeType.STRING,
17
- char: AttributeType.STRING,
18
- text: AttributeType.STRING,
19
- int: AttributeType.INTEGER,
20
- integer: AttributeType.INTEGER,
21
- bigint: AttributeType.INTEGER,
22
- smallint: AttributeType.INTEGER,
23
- decimal: AttributeType.NUMBER,
24
- numeric: AttributeType.NUMBER,
25
- float: AttributeType.NUMBER,
26
- double: AttributeType.NUMBER,
27
- real: AttributeType.NUMBER,
28
- boolean: AttributeType.BOOLEAN,
29
- bool: AttributeType.BOOLEAN,
30
- date: AttributeType.DATE,
31
- timestamp: AttributeType.TIMESTAMP,
32
- datetime: AttributeType.DATETIME,
33
- time: AttributeType.TIME,
34
- json: AttributeType.OBJECT,
35
- jsonb: AttributeType.OBJECT,
36
- uuid: AttributeType.STRING,
37
- serial: AttributeType.INTEGER,
38
- bigserial: AttributeType.INTEGER,
39
- };
40
-
41
- class ImportService {
42
- async importFromJsonSchema(schema: any, service: string): Promise<{ entities: Entity[]; errors: string[] }> {
43
- const entities: Entity[] = [];
44
- const errors: string[] = [];
45
-
46
- try {
47
- // Handle definitions/components/properties at root level
48
- const definitions = schema.definitions || schema.components?.schemas || schema.properties;
49
-
50
- if (!definitions || typeof definitions !== 'object') {
51
- return { entities: [], errors: ['No definitions found in JSON Schema'] };
52
- }
53
-
54
- for (const [name, def] of Object.entries(definitions)) {
55
- const schemaDef = def as any;
56
- if (schemaDef.type !== 'object' && !schemaDef.properties) continue;
57
-
58
- const requiredFields = new Set(schemaDef.required || []);
59
- const attributes: Attribute[] = [];
60
-
61
- for (const [propName, propDef] of Object.entries(schemaDef.properties || {})) {
62
- const prop = propDef as any;
63
- const attrType = JSON_SCHEMA_TYPE_MAP[prop.type] || AttributeType.STRING;
64
-
65
- const attr: Attribute = {
66
- uuid: generateUUID(),
67
- name: propName,
68
- description: prop.description || '',
69
- type: attrType,
70
- required: requiredFields.has(propName),
71
- };
72
-
73
- // Map constraints
74
- if (prop.minLength || prop.maxLength || prop.pattern || prop.minimum || prop.maximum || prop.enum) {
75
- attr.constraints = {};
76
- if (prop.minLength !== undefined) attr.constraints.minLength = prop.minLength;
77
- if (prop.maxLength !== undefined) attr.constraints.maxLength = prop.maxLength;
78
- if (prop.pattern) attr.constraints.pattern = prop.pattern;
79
- if (prop.minimum !== undefined) attr.constraints.minimum = prop.minimum;
80
- if (prop.maximum !== undefined) attr.constraints.maximum = prop.maximum;
81
- if (prop.enum) {
82
- attr.type = AttributeType.ENUM;
83
- attr.constraints.enumValues = prop.enum;
84
- }
85
- }
86
-
87
- attributes.push(attr);
88
- }
89
-
90
- const entity: Entity = {
91
- uuid: generateUUID(),
92
- name,
93
- description: schemaDef.description || '',
94
- status: EntityStatus.DRAFT,
95
- attributes,
96
- createdAt: new Date().toISOString(),
97
- updatedAt: new Date().toISOString(),
98
- };
99
-
100
- const ok = await writeEntityFile(entity, service);
101
- if (ok) {
102
- entities.push(entity);
103
- } else {
104
- errors.push(`Failed to write entity: ${name}`);
105
- }
106
- }
107
- } catch (error: any) {
108
- errors.push(`Import error: ${error.message}`);
109
- }
110
-
111
- return { entities, errors };
112
- }
113
-
114
- async importFromSqlDdl(sql: string, service: string): Promise<{ entities: Entity[]; errors: string[] }> {
115
- const entities: Entity[] = [];
116
- const errors: string[] = [];
117
-
118
- try {
119
- // Simple SQL DDL parser — handles CREATE TABLE statements
120
- const tableRegex = /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(?:`|")?(\w+)(?:`|")?\s*\(([\s\S]*?)\);/gi;
121
- let match;
122
-
123
- while ((match = tableRegex.exec(sql)) !== null) {
124
- const tableName = match[1];
125
- const columnsBlock = match[2];
126
- const attributes: Attribute[] = [];
127
- const primaryKeys = new Set<string>();
128
-
129
- // Extract PRIMARY KEY constraint
130
- const pkMatch = columnsBlock.match(/PRIMARY\s+KEY\s*\(([^)]+)\)/i);
131
- if (pkMatch) {
132
- pkMatch[1].split(',').forEach(k => primaryKeys.add(k.trim().replace(/[`"]/g, '')));
133
- }
134
-
135
- // Parse columns
136
- const lines = columnsBlock.split(',').map(l => l.trim()).filter(Boolean);
137
- for (const line of lines) {
138
- // Skip constraint lines
139
- if (/^\s*(PRIMARY|FOREIGN|UNIQUE|CHECK|CONSTRAINT|INDEX|KEY)/i.test(line)) continue;
140
-
141
- const colMatch = line.match(/^(?:`|")?(\w+)(?:`|")?\s+(\w+)(?:\(([^)]*)\))?(.*)/i);
142
- if (!colMatch) continue;
143
-
144
- const [, colName, sqlType, typeParams, rest] = colMatch;
145
- const normalizedType = sqlType.toLowerCase();
146
- const attrType = SQL_TYPE_MAP[normalizedType] || AttributeType.STRING;
147
- const isNotNull = /NOT\s+NULL/i.test(rest);
148
- const isPK = primaryKeys.has(colName) || /PRIMARY\s+KEY/i.test(rest);
149
-
150
- const attr: Attribute = {
151
- uuid: generateUUID(),
152
- name: colName,
153
- description: '',
154
- type: attrType,
155
- required: isNotNull || isPK,
156
- primaryKey: isPK || undefined,
157
- };
158
-
159
- // Map type params to constraints
160
- if (typeParams) {
161
- attr.constraints = {};
162
- const params = typeParams.split(',').map(p => parseInt(p.trim()));
163
- if (normalizedType === 'varchar' || normalizedType === 'char') {
164
- attr.constraints.maxLength = params[0];
165
- } else if (normalizedType === 'decimal' || normalizedType === 'numeric') {
166
- attr.constraints.precision = params[0];
167
- if (params[1] !== undefined) attr.constraints.scale = params[1];
168
- }
169
- }
170
-
171
- attributes.push(attr);
172
- }
173
-
174
- if (attributes.length === 0) continue;
175
-
176
- // Convert table name to PascalCase entity name
177
- const entityName = tableName.replace(/(^|_)(\w)/g, (_, __, c) => c.toUpperCase());
178
-
179
- const entity: Entity = {
180
- uuid: generateUUID(),
181
- name: entityName,
182
- description: `Imported from SQL table: ${tableName}`,
183
- status: EntityStatus.DRAFT,
184
- attributes,
185
- createdAt: new Date().toISOString(),
186
- updatedAt: new Date().toISOString(),
187
- };
188
-
189
- const ok = await writeEntityFile(entity, service);
190
- if (ok) {
191
- entities.push(entity);
192
- } else {
193
- errors.push(`Failed to write entity: ${entityName}`);
194
- }
195
- }
196
-
197
- if (entities.length === 0 && errors.length === 0) {
198
- errors.push('No CREATE TABLE statements found in the SQL');
199
- }
200
- } catch (error: any) {
201
- errors.push(`SQL import error: ${error.message}`);
202
- }
203
-
204
- return { entities, errors };
205
- }
206
- }
207
-
208
- export const importService = new ImportService();
@@ -1,276 +0,0 @@
1
- import { Perspective, ResolvedNode, ResolvedPerspective, PerspectiveNode, Relationship } from '../models/EntitySchema.js';
2
- import { listPerspectives, readPerspectiveFile, writePerspectiveFile, deletePerspectiveFile, getAllRelationships, listAllEntities, readEntityFile } from '../utils/fileOperations.js';
3
- import { generateUUID } from '../utils/uuid.js';
4
- import { logger } from '../utils/logger.js';
5
-
6
- interface AdjacencyEntry {
7
- neighborUuid: string;
8
- navName: string;
9
- relationshipUuid: string;
10
- }
11
-
12
- interface EntityInfo {
13
- uuid: string;
14
- name: string;
15
- service: string;
16
- }
17
-
18
- class PerspectiveService {
19
- // --- CRUD ---
20
-
21
- async getAll(): Promise<Perspective[]> {
22
- return listPerspectives();
23
- }
24
-
25
- async getById(uuid: string): Promise<Perspective | null> {
26
- return readPerspectiveFile(uuid);
27
- }
28
-
29
- async create(data: Partial<Perspective>): Promise<{ success: boolean; perspective?: Perspective; errors?: string[] }> {
30
- if (!data.name) return { success: false, errors: ['Name is required'] };
31
- if (!data.rootEntities?.length) return { success: false, errors: ['At least one root entity is required'] };
32
-
33
- const perspective: Perspective = {
34
- uuid: data.uuid || generateUUID(),
35
- name: data.name,
36
- description: data.description,
37
- rootEntities: data.rootEntities,
38
- nodes: data.nodes || [],
39
- maxDepth: data.maxDepth ?? 10,
40
- metadata: data.metadata || [],
41
- createdAt: new Date().toISOString(),
42
- updatedAt: new Date().toISOString(),
43
- };
44
-
45
- const ok = await writePerspectiveFile(perspective);
46
- if (!ok) return { success: false, errors: ['Failed to write perspective file'] };
47
- return { success: true, perspective };
48
- }
49
-
50
- async update(uuid: string, data: Partial<Perspective>): Promise<{ success: boolean; perspective?: Perspective; errors?: string[] }> {
51
- const existing = await readPerspectiveFile(uuid);
52
- if (!existing) return { success: false, errors: ['Perspective not found'] };
53
-
54
- const updated: Perspective = {
55
- ...existing,
56
- name: data.name ?? existing.name,
57
- description: data.description ?? existing.description,
58
- rootEntities: data.rootEntities ?? existing.rootEntities,
59
- nodes: data.nodes ?? existing.nodes,
60
- maxDepth: data.maxDepth ?? existing.maxDepth,
61
- metadata: data.metadata ?? existing.metadata,
62
- updatedAt: new Date().toISOString(),
63
- };
64
-
65
- const ok = await writePerspectiveFile(updated);
66
- if (!ok) return { success: false, errors: ['Failed to write perspective file'] };
67
- return { success: true, perspective: updated };
68
- }
69
-
70
- async delete(uuid: string): Promise<{ success: boolean; errors?: string[] }> {
71
- const ok = await deletePerspectiveFile(uuid);
72
- if (!ok) return { success: false, errors: ['Perspective not found'] };
73
- return { success: true };
74
- }
75
-
76
- // --- Node (annotation) management ---
77
-
78
- async upsertNode(uuid: string, node: PerspectiveNode): Promise<{ success: boolean; errors?: string[] }> {
79
- const perspective = await readPerspectiveFile(uuid);
80
- if (!perspective) return { success: false, errors: ['Perspective not found'] };
81
-
82
- const nodes = perspective.nodes || [];
83
- const idx = nodes.findIndex(n => n.path === node.path);
84
- if (idx >= 0) {
85
- nodes[idx] = node;
86
- } else {
87
- nodes.push(node);
88
- }
89
- perspective.nodes = nodes;
90
- perspective.updatedAt = new Date().toISOString();
91
-
92
- const ok = await writePerspectiveFile(perspective);
93
- if (!ok) return { success: false, errors: ['Failed to write'] };
94
- return { success: true };
95
- }
96
-
97
- // --- BFS Resolution ---
98
-
99
- async resolve(uuid: string): Promise<ResolvedPerspective | null> {
100
- const perspective = await readPerspectiveFile(uuid);
101
- if (!perspective) return null;
102
-
103
- // Build entity info map: uuid → { name, service }
104
- const entityMap = await this.buildEntityMap();
105
-
106
- // Build adjacency map from all relationships
107
- const adjacency = await this.buildAdjacencyMap();
108
-
109
- // Build node lookup for exclude/traverse checks
110
- const nodesByPath = new Map<string, PerspectiveNode>();
111
- for (const node of perspective.nodes || []) {
112
- nodesByPath.set(node.path, node);
113
- }
114
-
115
- const maxDepth = perspective.maxDepth ?? 10;
116
- const resolvedNodes: ResolvedNode[] = [];
117
-
118
- // BFS from each root entity
119
- const queue: { entityUuid: string; hopDistance: number; pathSegments: string[]; usedRelationships: Set<string> }[] = [];
120
-
121
- for (const rootUuid of perspective.rootEntities) {
122
- const info = entityMap.get(rootUuid);
123
- if (!info) continue;
124
- queue.push({ entityUuid: rootUuid, hopDistance: 0, pathSegments: [info.name], usedRelationships: new Set() });
125
- }
126
-
127
- // Track visited paths to avoid duplicate processing
128
- const visitedPaths = new Set<string>();
129
-
130
- while (queue.length > 0) {
131
- const { entityUuid, hopDistance, pathSegments, usedRelationships } = queue.shift()!;
132
- const currentPath = pathSegments.join('/');
133
-
134
- // Skip if already visited this exact path
135
- if (visitedPaths.has(currentPath)) continue;
136
- visitedPaths.add(currentPath);
137
-
138
- // Check for exclusion
139
- const node = nodesByPath.get(currentPath);
140
- if (node?.exclude) continue;
141
-
142
- // Check depth
143
- if (hopDistance > maxDepth) continue;
144
-
145
- const info = entityMap.get(entityUuid);
146
- if (!info) continue;
147
-
148
- const isFrontier = node?.traverse === false;
149
-
150
- resolvedNodes.push({
151
- entityUuid,
152
- entityName: info.name,
153
- service: info.service,
154
- path: currentPath,
155
- hopDistance,
156
- isRoot: hopDistance === 0,
157
- isFrontier,
158
- isManualInclusion: false,
159
- });
160
-
161
- // Don't traverse further from frontier nodes
162
- if (isFrontier) continue;
163
-
164
- // Enqueue neighbors — skip relationships already used in this path (prevents cycles)
165
- const neighbors = adjacency.get(entityUuid) || [];
166
- for (const { neighborUuid, navName, relationshipUuid } of neighbors) {
167
- if (usedRelationships.has(relationshipUuid)) continue;
168
-
169
- const neighborInfo = entityMap.get(neighborUuid);
170
- if (!neighborInfo) continue;
171
- const newPath = [...pathSegments, navName];
172
- const newPathStr = newPath.join('/');
173
-
174
- // Check if this path is excluded
175
- const prefixNode = nodesByPath.get(newPathStr);
176
- if (prefixNode?.exclude) continue;
177
-
178
- const newUsed = new Set(usedRelationships);
179
- newUsed.add(relationshipUuid);
180
-
181
- queue.push({
182
- entityUuid: neighborUuid,
183
- hopDistance: hopDistance + 1,
184
- pathSegments: newPath,
185
- usedRelationships: newUsed,
186
- });
187
- }
188
- }
189
-
190
- return { ...perspective, resolvedNodes };
191
- }
192
-
193
- // --- Graph Data ---
194
-
195
- async getGraphData(uuid: string): Promise<{ nodes: any[]; edges: any[] } | null> {
196
- const resolved = await this.resolve(uuid);
197
- if (!resolved) return null;
198
-
199
- // Deduplicate entities for graph display (same entity shown once as node)
200
- const entityUuids = new Set(resolved.resolvedNodes.map(n => n.entityUuid));
201
- const entityMap = await this.buildEntityMap();
202
-
203
- const nodes = [...entityUuids].map(eUuid => {
204
- const info = entityMap.get(eUuid);
205
- const resolvedNode = resolved.resolvedNodes.find(n => n.entityUuid === eUuid);
206
- return {
207
- id: eUuid,
208
- label: info?.name || eUuid,
209
- type: 'entity',
210
- service: info?.service || '',
211
- isRoot: resolvedNode?.isRoot || false,
212
- isFrontier: resolvedNode?.isFrontier || false,
213
- };
214
- });
215
-
216
- // Get all relationships that connect entities within the resolved set
217
- const allRels = await getAllRelationships();
218
- const edges: any[] = [];
219
- for (const { relationships } of allRels) {
220
- for (const rel of relationships) {
221
- if (entityUuids.has(rel.source.entity) && entityUuids.has(rel.target.entity)) {
222
- edges.push({
223
- id: rel.uuid,
224
- source: rel.source.entity,
225
- target: rel.target.entity,
226
- label: rel.description || '',
227
- sourceCardinality: rel.source.cardinality,
228
- targetCardinality: rel.target.cardinality,
229
- });
230
- }
231
- }
232
- }
233
-
234
- return { nodes, edges };
235
- }
236
-
237
- // --- Internal helpers ---
238
-
239
- private async buildEntityMap(): Promise<Map<string, EntityInfo>> {
240
- const map = new Map<string, EntityInfo>();
241
- const allEntities = await listAllEntities();
242
- for (const entry of allEntities) {
243
- const entity = await readEntityFile(entry.microservice, entry.name);
244
- if (entity) {
245
- map.set(entity.uuid, { uuid: entity.uuid, name: entity.name, service: entry.microservice });
246
- }
247
- }
248
- return map;
249
- }
250
-
251
- private async buildAdjacencyMap(): Promise<Map<string, AdjacencyEntry[]>> {
252
- const adjacency = new Map<string, AdjacencyEntry[]>();
253
-
254
- const allRels = await getAllRelationships();
255
- for (const { relationships } of allRels) {
256
- for (const rel of relationships) {
257
- const srcUuid = rel.source.entity;
258
- const tgtUuid = rel.target.entity;
259
- const srcNav = rel.source.name || rel.description || rel.uuid;
260
- const tgtNav = rel.target.name || rel.description || rel.uuid;
261
-
262
- // Source → Target (using target nav name or description)
263
- if (!adjacency.has(srcUuid)) adjacency.set(srcUuid, []);
264
- adjacency.get(srcUuid)!.push({ neighborUuid: tgtUuid, navName: tgtNav, relationshipUuid: rel.uuid });
265
-
266
- // Target → Source (using source nav name or description)
267
- if (!adjacency.has(tgtUuid)) adjacency.set(tgtUuid, []);
268
- adjacency.get(tgtUuid)!.push({ neighborUuid: srcUuid, navName: srcNav, relationshipUuid: rel.uuid });
269
- }
270
- }
271
-
272
- return adjacency;
273
- }
274
- }
275
-
276
- export const perspectiveService = new PerspectiveService();