@hamak/smart-data-dico 1.0.3 → 1.1.0
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 +82212 -0
- package/bin/cli.js +28 -17
- package/frontend/dist/assets/index-1b53cffe.js +431 -0
- package/frontend/dist/assets/index-fa72a51a.css +1 -0
- package/frontend/dist/index.html +15 -0
- 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,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();
|