@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.
- package/backend/dist/server.mjs +82213 -0
- package/bin/cli.js +28 -17
- package/package.json +28 -27
- package/backend/package.json +0 -51
- package/backend/src/__tests__/integration/api.test.ts +0 -149
- package/backend/src/__tests__/setup.ts +0 -24
- package/backend/src/__tests__/utils/testUtils.ts +0 -76
- package/backend/src/adapters/EntityFileAdapter.ts +0 -154
- package/backend/src/adapters/YamlFileInfoEnricher.ts +0 -52
- package/backend/src/controllers/authController.ts +0 -131
- package/backend/src/controllers/diagramController.ts +0 -143
- package/backend/src/controllers/dictionaryController.ts +0 -306
- package/backend/src/controllers/importExportController.ts +0 -64
- package/backend/src/controllers/perspectiveController.ts +0 -90
- package/backend/src/controllers/serviceController.ts +0 -418
- package/backend/src/controllers/stereotypeController.ts +0 -59
- package/backend/src/controllers/versionController.ts +0 -226
- package/backend/src/kernel/config.ts +0 -43
- package/backend/src/middleware/auth.ts +0 -128
- package/backend/src/middleware/jwtAuth.ts +0 -100
- package/backend/src/models/Dictionary.ts +0 -38
- package/backend/src/models/EntitySchema.ts +0 -393
- package/backend/src/models/__tests__/Dictionary.test.ts +0 -92
- package/backend/src/models/__tests__/EntitySchema.test.ts +0 -119
- package/backend/src/routes/index.ts +0 -120
- package/backend/src/scripts/migrate-to-uuid.ts +0 -24
- package/backend/src/server.ts +0 -158
- package/backend/src/services/__mocks__/entityService.ts +0 -38
- package/backend/src/services/__mocks__/serviceService.ts +0 -88
- package/backend/src/services/__mocks__/versionService.ts +0 -38
- package/backend/src/services/__tests__/dictionaryService.test.ts +0 -74
- package/backend/src/services/diagramService.ts +0 -165
- package/backend/src/services/dictionaryService.ts +0 -582
- package/backend/src/services/entityService.ts +0 -102
- package/backend/src/services/exportService.ts +0 -172
- package/backend/src/services/importService.ts +0 -208
- package/backend/src/services/perspectiveService.ts +0 -276
- package/backend/src/services/qualityService.ts +0 -121
- package/backend/src/services/serviceService.ts +0 -763
- package/backend/src/services/stereotypeService.ts +0 -98
- package/backend/src/services/versionService.ts +0 -135
- package/backend/src/setupTests.ts +0 -12
- package/backend/src/utils/__mocks__/fileOperations.ts +0 -116
- package/backend/src/utils/fileOperations.ts +0 -602
- package/backend/src/utils/logger.ts +0 -38
- package/backend/src/utils/migration.ts +0 -254
- package/backend/src/utils/swagger.ts +0 -358
- package/backend/src/utils/uuid.ts +0 -41
- 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();
|