@hamak/smart-data-dico 1.0.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/README.md +292 -0
- package/backend/package.json +51 -0
- package/backend/src/__tests__/integration/api.test.ts +149 -0
- package/backend/src/__tests__/setup.ts +24 -0
- package/backend/src/__tests__/utils/testUtils.ts +76 -0
- package/backend/src/adapters/EntityFileAdapter.ts +154 -0
- package/backend/src/adapters/YamlFileInfoEnricher.ts +52 -0
- package/backend/src/controllers/authController.ts +131 -0
- package/backend/src/controllers/diagramController.ts +143 -0
- package/backend/src/controllers/dictionaryController.ts +306 -0
- package/backend/src/controllers/importExportController.ts +64 -0
- package/backend/src/controllers/perspectiveController.ts +90 -0
- package/backend/src/controllers/serviceController.ts +418 -0
- package/backend/src/controllers/stereotypeController.ts +59 -0
- package/backend/src/controllers/versionController.ts +226 -0
- package/backend/src/kernel/config.ts +43 -0
- package/backend/src/middleware/auth.ts +128 -0
- package/backend/src/middleware/jwtAuth.ts +100 -0
- package/backend/src/models/Dictionary.ts +38 -0
- package/backend/src/models/EntitySchema.ts +393 -0
- package/backend/src/models/__tests__/Dictionary.test.ts +92 -0
- package/backend/src/models/__tests__/EntitySchema.test.ts +119 -0
- package/backend/src/routes/index.ts +120 -0
- package/backend/src/scripts/migrate-to-uuid.ts +24 -0
- package/backend/src/server.ts +140 -0
- package/backend/src/services/__mocks__/entityService.ts +38 -0
- package/backend/src/services/__mocks__/serviceService.ts +88 -0
- package/backend/src/services/__mocks__/versionService.ts +38 -0
- package/backend/src/services/__tests__/dictionaryService.test.ts +74 -0
- package/backend/src/services/diagramService.ts +165 -0
- package/backend/src/services/dictionaryService.ts +582 -0
- package/backend/src/services/entityService.ts +102 -0
- package/backend/src/services/exportService.ts +172 -0
- package/backend/src/services/importService.ts +208 -0
- package/backend/src/services/perspectiveService.ts +276 -0
- package/backend/src/services/qualityService.ts +121 -0
- package/backend/src/services/serviceService.ts +763 -0
- package/backend/src/services/stereotypeService.ts +98 -0
- package/backend/src/services/versionService.ts +135 -0
- package/backend/src/setupTests.ts +12 -0
- package/backend/src/utils/__mocks__/fileOperations.ts +116 -0
- package/backend/src/utils/fileOperations.ts +602 -0
- package/backend/src/utils/logger.ts +38 -0
- package/backend/src/utils/migration.ts +254 -0
- package/backend/src/utils/swagger.ts +358 -0
- package/backend/src/utils/uuid.ts +41 -0
- package/backend/tsconfig.json +20 -0
- package/bin/cli.js +129 -0
- package/data-dictionaries/stereotypes.yaml +91 -0
- package/package.json +42 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Dictionary, DictionaryEntry } from '../Dictionary.js';
|
|
2
|
+
|
|
3
|
+
describe('Dictionary Model', () => {
|
|
4
|
+
describe('Dictionary Interface', () => {
|
|
5
|
+
it('should create a valid Dictionary object', () => {
|
|
6
|
+
const dictionary: Dictionary = {
|
|
7
|
+
id: 'test-dict-1',
|
|
8
|
+
name: 'Test Dictionary',
|
|
9
|
+
description: 'A test dictionary',
|
|
10
|
+
version: '1.0.0',
|
|
11
|
+
createdAt: new Date(),
|
|
12
|
+
updatedAt: new Date(),
|
|
13
|
+
rootPackage: { id: 'root', name: 'Root', entities: [], subPackages: [] },
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
expect(dictionary).toBeDefined();
|
|
17
|
+
expect(dictionary.id).toBe('test-dict-1');
|
|
18
|
+
expect(dictionary.name).toBe('Test Dictionary');
|
|
19
|
+
expect(dictionary.description).toBe('A test dictionary');
|
|
20
|
+
expect(dictionary.version).toBe('1.0.0');
|
|
21
|
+
expect(dictionary.createdAt).toBeInstanceOf(Date);
|
|
22
|
+
expect(dictionary.updatedAt).toBeInstanceOf(Date);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should create a Dictionary with only required fields', () => {
|
|
26
|
+
const dictionary: Dictionary = {
|
|
27
|
+
id: 'test-dict-2',
|
|
28
|
+
name: 'Minimal Dictionary',
|
|
29
|
+
rootPackage: { id: 'root', name: 'Root', entities: [], subPackages: [] },
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
expect(dictionary).toBeDefined();
|
|
33
|
+
expect(dictionary.id).toBe('test-dict-2');
|
|
34
|
+
expect(dictionary.name).toBe('Minimal Dictionary');
|
|
35
|
+
expect(dictionary.description).toBeUndefined();
|
|
36
|
+
expect(dictionary.version).toBeUndefined();
|
|
37
|
+
expect(dictionary.createdAt).toBeUndefined();
|
|
38
|
+
expect(dictionary.updatedAt).toBeUndefined();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('DictionaryEntry Interface', () => {
|
|
43
|
+
it('should create a valid DictionaryEntry object', () => {
|
|
44
|
+
const entry: DictionaryEntry = {
|
|
45
|
+
id: 'entry-1',
|
|
46
|
+
name: 'Test Entry',
|
|
47
|
+
description: 'A test entry',
|
|
48
|
+
type: 'string',
|
|
49
|
+
format: 'email',
|
|
50
|
+
required: true,
|
|
51
|
+
defaultValue: 'test@example.com',
|
|
52
|
+
examples: ['user@example.com', 'admin@example.com'],
|
|
53
|
+
metadata: {
|
|
54
|
+
source: 'user',
|
|
55
|
+
lastUpdated: '2023-01-01',
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
expect(entry).toBeDefined();
|
|
60
|
+
expect(entry.id).toBe('entry-1');
|
|
61
|
+
expect(entry.name).toBe('Test Entry');
|
|
62
|
+
expect(entry.description).toBe('A test entry');
|
|
63
|
+
expect(entry.type).toBe('string');
|
|
64
|
+
expect(entry.format).toBe('email');
|
|
65
|
+
expect(entry.required).toBe(true);
|
|
66
|
+
expect(entry.defaultValue).toBe('test@example.com');
|
|
67
|
+
expect(entry.examples).toHaveLength(2);
|
|
68
|
+
expect(entry.examples).toContain('user@example.com');
|
|
69
|
+
expect(entry.metadata).toHaveProperty('source', 'user');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should create a DictionaryEntry with only required fields', () => {
|
|
73
|
+
const entry: DictionaryEntry = {
|
|
74
|
+
id: 'entry-2',
|
|
75
|
+
name: 'Minimal Entry',
|
|
76
|
+
description: 'A minimal entry',
|
|
77
|
+
type: 'number',
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
expect(entry).toBeDefined();
|
|
81
|
+
expect(entry.id).toBe('entry-2');
|
|
82
|
+
expect(entry.name).toBe('Minimal Entry');
|
|
83
|
+
expect(entry.description).toBe('A minimal entry');
|
|
84
|
+
expect(entry.type).toBe('number');
|
|
85
|
+
expect(entry.format).toBeUndefined();
|
|
86
|
+
expect(entry.required).toBeUndefined();
|
|
87
|
+
expect(entry.defaultValue).toBeUndefined();
|
|
88
|
+
expect(entry.examples).toBeUndefined();
|
|
89
|
+
expect(entry.metadata).toBeUndefined();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { AttributeType, validateEntity } from '../EntitySchema.js';
|
|
2
|
+
|
|
3
|
+
describe('EntitySchema', () => {
|
|
4
|
+
describe('validateEntity', () => {
|
|
5
|
+
it('should validate a valid entity', () => {
|
|
6
|
+
const validEntity = {
|
|
7
|
+
uuid: 'a38d1597-cc4f-4934-bb08-c876c023f693',
|
|
8
|
+
name: 'Test Entity',
|
|
9
|
+
attributes: [
|
|
10
|
+
{
|
|
11
|
+
uuid: 'b49e2608-dd5f-4045-aa09-d464c234e694',
|
|
12
|
+
name: 'id',
|
|
13
|
+
description: 'Primary identifier',
|
|
14
|
+
type: AttributeType.STRING,
|
|
15
|
+
required: true,
|
|
16
|
+
primaryKey: true,
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
uuid: 'c5af3719-ee6f-4156-bb1a-e575d345f7a5',
|
|
20
|
+
name: 'name',
|
|
21
|
+
description: 'Entity name',
|
|
22
|
+
type: AttributeType.STRING,
|
|
23
|
+
required: true,
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const result = validateEntity(validEntity);
|
|
29
|
+
expect(result.valid).toBe(true);
|
|
30
|
+
expect(result.errors).toHaveLength(0);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should invalidate an entity with missing required fields', () => {
|
|
34
|
+
const invalidEntity = {
|
|
35
|
+
// Missing uuid
|
|
36
|
+
// Missing name
|
|
37
|
+
description: 'A test entity',
|
|
38
|
+
attributes: [],
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const result = validateEntity(invalidEntity as any);
|
|
42
|
+
expect(result.valid).toBe(false);
|
|
43
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should validate an entity with constraints', () => {
|
|
47
|
+
const entityWithConstraints = {
|
|
48
|
+
uuid: 'd6b0482a-ff70-4267-8c2b-f686e456f8b6',
|
|
49
|
+
name: 'Test Entity',
|
|
50
|
+
description: 'A test entity',
|
|
51
|
+
attributes: [
|
|
52
|
+
{
|
|
53
|
+
uuid: 'e7c1593b-aa81-4378-9d3c-a797f567a9c7',
|
|
54
|
+
name: 'email',
|
|
55
|
+
description: 'Email address',
|
|
56
|
+
type: AttributeType.STRING,
|
|
57
|
+
required: true,
|
|
58
|
+
constraints: {
|
|
59
|
+
maxLength: 255,
|
|
60
|
+
pattern: '^[^@]+@[^@]+$',
|
|
61
|
+
format: 'email',
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const result = validateEntity(entityWithConstraints);
|
|
68
|
+
expect(result.valid).toBe(true);
|
|
69
|
+
expect(result.errors).toHaveLength(0);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should invalidate an entity with invalid attribute type', () => {
|
|
73
|
+
const entityWithInvalidAttribute = {
|
|
74
|
+
uuid: 'a38d1597-cc4f-4934-bb08-c876c023f693',
|
|
75
|
+
name: 'Test Entity',
|
|
76
|
+
description: 'A test entity',
|
|
77
|
+
attributes: [
|
|
78
|
+
{
|
|
79
|
+
uuid: 'b49e2608-dd5f-4045-aa09-d464c234e694',
|
|
80
|
+
name: 'id',
|
|
81
|
+
description: 'Primary identifier',
|
|
82
|
+
type: 'invalid-type', // Invalid type
|
|
83
|
+
required: true,
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const result = validateEntity(entityWithInvalidAttribute as any);
|
|
89
|
+
expect(result.valid).toBe(false);
|
|
90
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should validate an entity with metadata entries', () => {
|
|
94
|
+
const entityWithMetadata = {
|
|
95
|
+
uuid: 'a38d1597-cc4f-4934-bb08-c876c023f693',
|
|
96
|
+
name: 'Test Entity',
|
|
97
|
+
attributes: [
|
|
98
|
+
{
|
|
99
|
+
uuid: 'b49e2608-dd5f-4045-aa09-d464c234e694',
|
|
100
|
+
name: 'id',
|
|
101
|
+
description: 'Primary identifier',
|
|
102
|
+
type: AttributeType.STRING,
|
|
103
|
+
required: true,
|
|
104
|
+
metadata: [
|
|
105
|
+
{ name: 'sensitive', value: true },
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
metadata: [
|
|
110
|
+
{ name: 'owner', value: 'team-a' },
|
|
111
|
+
],
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const result = validateEntity(entityWithMetadata);
|
|
115
|
+
expect(result.valid).toBe(true);
|
|
116
|
+
expect(result.errors).toHaveLength(0);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
|
|
3
|
+
import { getCurrentUser, login } from '../controllers/authController.js';
|
|
4
|
+
import { diagramController } from '../controllers/diagramController.js';
|
|
5
|
+
import { createDictionary, getDictionaries, getDictionaryById, getDictionaryEntries, getEntityAttributes, getPackageByPath, getPackageHierarchy, getRelatedEntities, getTabularData, saveEntity, listAllPackagesAndEntities, getFlatEntitiesAndAttributes, getEntityHierarchy, createRootPackage, createPackageAtPath, updatePackageAtPath, deletePackageAtPath } from '../controllers/dictionaryController.js';
|
|
6
|
+
import { createEntity, deleteEntity, getAllServices, getEntitySchema, getGraphData, getServiceEntities, searchEntities, updateEntity, getPackageRelationships, createRelationship, updateRelationship, deleteRelationship, getImpactAnalysis, getLineage, submitEntity, approveEntity, returnEntity, getEntityComments, addEntityComment, resolveEntityComment } from '../controllers/serviceController.js';
|
|
7
|
+
import { getAllStereotypes, getStereotype, createStereotype, updateStereotype, deleteStereotype } from '../controllers/stereotypeController.js';
|
|
8
|
+
import { getAllPerspectives, getPerspective, createPerspective, updatePerspective, deletePerspective, resolvePerspective, getPerspectiveGraph, upsertPerspectiveNode } from '../controllers/perspectiveController.js';
|
|
9
|
+
import { commitChanges, getCommitHistory, revertToCommit } from '../controllers/versionController.js';
|
|
10
|
+
import { importJsonSchema, importSqlDdl, exportJsonSchema, exportMarkdown, getQualityReport } from '../controllers/importExportController.js';
|
|
11
|
+
import { authenticate, UserRole } from '../middleware/auth.js';
|
|
12
|
+
import { authorizeJwt, verifyToken } from '../middleware/jwtAuth.js';
|
|
13
|
+
|
|
14
|
+
const router = Router();
|
|
15
|
+
|
|
16
|
+
// API status route
|
|
17
|
+
router.get('/api/status', (req, res) => {
|
|
18
|
+
res.json({ status: 'operational' });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
router.get('/api/packages/hierarchy/:rootPackage', getPackageHierarchy);
|
|
22
|
+
router.get('/api/packages/tabular/:rootPackage', getTabularData);
|
|
23
|
+
|
|
24
|
+
// Package CRUD routes
|
|
25
|
+
router.post('/api/packages', authorizeJwt([UserRole.ADMIN, UserRole.EDITOR]), createRootPackage);
|
|
26
|
+
router.post('/api/packages/:rootPackage/subpackages/*', authorizeJwt([UserRole.ADMIN, UserRole.EDITOR]), createPackageAtPath);
|
|
27
|
+
router.put('/api/packages/:rootPackage/path/*', authorizeJwt([UserRole.ADMIN, UserRole.EDITOR]), updatePackageAtPath);
|
|
28
|
+
router.delete('/api/packages/:rootPackage/path/*', authorizeJwt([UserRole.ADMIN]), deletePackageAtPath);
|
|
29
|
+
|
|
30
|
+
router.get('/api/packages/:rootPackage/path/*', getPackageByPath);
|
|
31
|
+
|
|
32
|
+
// Auth routes
|
|
33
|
+
router.post('/api/auth/login', login);
|
|
34
|
+
router.get('/api/auth/me', verifyToken, getCurrentUser);
|
|
35
|
+
|
|
36
|
+
// Legacy Dictionary routes
|
|
37
|
+
router.get('/api/dictionaries', getDictionaries);
|
|
38
|
+
router.post('/api/dictionaries', createDictionary);
|
|
39
|
+
router.get('/api/dictionaries/:id', getDictionaryById);
|
|
40
|
+
router.get('/api/dictionaries/:id/entries', getDictionaryEntries);
|
|
41
|
+
router.get('/api/entities/:microservice/:entityName/attributes', getEntityAttributes);
|
|
42
|
+
router.get('/api/entities/:microservice/:entityName/related', getRelatedEntities);
|
|
43
|
+
router.post('/api/entities', saveEntity);
|
|
44
|
+
|
|
45
|
+
// Data Dictionary/Entity/Package API extensions
|
|
46
|
+
router.get('/api/packages/all', listAllPackagesAndEntities);
|
|
47
|
+
router.get('/api/entities/flat', getFlatEntitiesAndAttributes);
|
|
48
|
+
router.get('/api/entities/hierarchy/:microservice/:entityName', getEntityHierarchy);
|
|
49
|
+
|
|
50
|
+
// New Service/Entity API routes
|
|
51
|
+
router.get('/api/services', getAllServices);
|
|
52
|
+
router.get('/api/services/:service/entities', getServiceEntities);
|
|
53
|
+
router.get('/api/services/:service/entities/:entity', getEntitySchema);
|
|
54
|
+
router.post('/api/services/:service/entities', authorizeJwt([UserRole.ADMIN, UserRole.EDITOR]), createEntity);
|
|
55
|
+
router.put('/api/services/:service/entities/:entity', authorizeJwt([UserRole.ADMIN, UserRole.EDITOR]), updateEntity);
|
|
56
|
+
router.delete('/api/services/:service/entities/:entity', authorizeJwt([UserRole.ADMIN]), deleteEntity);
|
|
57
|
+
|
|
58
|
+
// Entity review workflow
|
|
59
|
+
router.post('/api/services/:service/entities/:entity/submit', authorizeJwt([UserRole.ADMIN, UserRole.EDITOR]), submitEntity);
|
|
60
|
+
router.post('/api/services/:service/entities/:entity/approve', authorizeJwt([UserRole.ADMIN]), approveEntity);
|
|
61
|
+
router.post('/api/services/:service/entities/:entity/return', authorizeJwt([UserRole.ADMIN]), returnEntity);
|
|
62
|
+
router.get('/api/services/:service/entities/:entity/comments', getEntityComments);
|
|
63
|
+
router.post('/api/services/:service/entities/:entity/comments', authorizeJwt([UserRole.ADMIN, UserRole.EDITOR]), addEntityComment);
|
|
64
|
+
router.put('/api/services/:service/entities/:entity/comments/:id', authorizeJwt([UserRole.ADMIN, UserRole.EDITOR]), resolveEntityComment);
|
|
65
|
+
|
|
66
|
+
// Package-level relationship CRUD routes
|
|
67
|
+
router.get('/api/packages/:packageName/relationships', getPackageRelationships);
|
|
68
|
+
router.post('/api/packages/:packageName/relationships', authorizeJwt([UserRole.ADMIN, UserRole.EDITOR]), createRelationship);
|
|
69
|
+
router.put('/api/packages/:packageName/relationships/:uuid', authorizeJwt([UserRole.ADMIN, UserRole.EDITOR]), updateRelationship);
|
|
70
|
+
router.delete('/api/packages/:packageName/relationships/:uuid', authorizeJwt([UserRole.ADMIN]), deleteRelationship);
|
|
71
|
+
|
|
72
|
+
// Stereotype API
|
|
73
|
+
router.get('/api/stereotypes', getAllStereotypes);
|
|
74
|
+
router.get('/api/stereotypes/:id', getStereotype);
|
|
75
|
+
router.post('/api/stereotypes', authorizeJwt([UserRole.ADMIN, UserRole.EDITOR]), createStereotype);
|
|
76
|
+
router.put('/api/stereotypes/:id', authorizeJwt([UserRole.ADMIN, UserRole.EDITOR]), updateStereotype);
|
|
77
|
+
router.delete('/api/stereotypes/:id', authorizeJwt([UserRole.ADMIN]), deleteStereotype);
|
|
78
|
+
|
|
79
|
+
// Perspective API
|
|
80
|
+
router.get('/api/perspectives', getAllPerspectives);
|
|
81
|
+
router.get('/api/perspectives/:id', getPerspective);
|
|
82
|
+
router.post('/api/perspectives', authorizeJwt([UserRole.ADMIN, UserRole.EDITOR]), createPerspective);
|
|
83
|
+
router.put('/api/perspectives/:id', authorizeJwt([UserRole.ADMIN, UserRole.EDITOR]), updatePerspective);
|
|
84
|
+
router.delete('/api/perspectives/:id', authorizeJwt([UserRole.ADMIN]), deletePerspective);
|
|
85
|
+
router.get('/api/perspectives/:id/resolve', resolvePerspective);
|
|
86
|
+
router.get('/api/perspectives/:id/graph', getPerspectiveGraph);
|
|
87
|
+
router.put('/api/perspectives/:id/nodes', authorizeJwt([UserRole.ADMIN, UserRole.EDITOR]), upsertPerspectiveNode);
|
|
88
|
+
|
|
89
|
+
// Search API
|
|
90
|
+
router.get('/api/search', searchEntities);
|
|
91
|
+
|
|
92
|
+
// Impact analysis & lineage
|
|
93
|
+
router.get('/api/entities/:uuid/impact', getImpactAnalysis);
|
|
94
|
+
router.get('/api/entities/:uuid/lineage', getLineage);
|
|
95
|
+
|
|
96
|
+
// Import/Export
|
|
97
|
+
router.post('/api/import/json-schema', authorizeJwt([UserRole.ADMIN, UserRole.EDITOR]), importJsonSchema);
|
|
98
|
+
router.post('/api/import/sql-ddl', authorizeJwt([UserRole.ADMIN, UserRole.EDITOR]), importSqlDdl);
|
|
99
|
+
router.get('/api/export/json-schema/:service', exportJsonSchema);
|
|
100
|
+
router.get('/api/export/markdown/:service', exportMarkdown);
|
|
101
|
+
|
|
102
|
+
// Quality
|
|
103
|
+
router.get('/api/quality/report', getQualityReport);
|
|
104
|
+
|
|
105
|
+
// Graph API for visualization
|
|
106
|
+
router.get('/api/graph/:service', getGraphData);
|
|
107
|
+
|
|
108
|
+
// Version control API
|
|
109
|
+
router.post('/api/commit', authorizeJwt([UserRole.ADMIN, UserRole.EDITOR]), commitChanges);
|
|
110
|
+
router.get('/api/history', getCommitHistory);
|
|
111
|
+
router.post('/api/revert', authorizeJwt([UserRole.ADMIN]), revertToCommit);
|
|
112
|
+
|
|
113
|
+
// Diagram layout API
|
|
114
|
+
router.get('/api/diagrams', diagramController.listDiagramLayouts.bind(diagramController));
|
|
115
|
+
router.post('/api/diagrams', authorizeJwt([UserRole.ADMIN, UserRole.EDITOR]), diagramController.saveDiagramLayout.bind(diagramController));
|
|
116
|
+
router.get('/api/diagrams/:id', diagramController.loadDiagramLayout.bind(diagramController));
|
|
117
|
+
router.put('/api/diagrams/:id', authorizeJwt([UserRole.ADMIN, UserRole.EDITOR]), diagramController.updateDiagramLayout.bind(diagramController));
|
|
118
|
+
router.delete('/api/diagrams/:id', authorizeJwt([UserRole.ADMIN]), diagramController.deleteDiagramLayout.bind(diagramController));
|
|
119
|
+
|
|
120
|
+
export default router;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env ts-node
|
|
2
|
+
|
|
3
|
+
import { runAllMigrations } from '../utils/migration.js';
|
|
4
|
+
import { logger } from '../utils/logger.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Migration script to convert existing entities to UUID format
|
|
8
|
+
*/
|
|
9
|
+
async function main() {
|
|
10
|
+
try {
|
|
11
|
+
logger.info('Starting migration to UUID format...');
|
|
12
|
+
await runAllMigrations();
|
|
13
|
+
logger.info('Migration completed successfully!');
|
|
14
|
+
process.exit(0);
|
|
15
|
+
} catch (error) {
|
|
16
|
+
logger.error('Migration failed:', error);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Run the migration if this script is executed directly
|
|
22
|
+
if (require.main === module) {
|
|
23
|
+
main();
|
|
24
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import express, { Request, Response } from 'express';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import cors from 'cors';
|
|
4
|
+
import dotenv from 'dotenv';
|
|
5
|
+
import routes from './routes/index.js';
|
|
6
|
+
import { setupSwagger } from './utils/swagger.js';
|
|
7
|
+
import { logger } from './utils/logger.js';
|
|
8
|
+
import { config } from './kernel/config.js';
|
|
9
|
+
import { initializeFileSystem, getFileRouter } from './adapters/EntityFileAdapter.js';
|
|
10
|
+
import { createYamlFileInfoEnricher } from './adapters/YamlFileInfoEnricher.js';
|
|
11
|
+
|
|
12
|
+
// Load environment variables
|
|
13
|
+
dotenv.config();
|
|
14
|
+
|
|
15
|
+
// Initialize Express app
|
|
16
|
+
const app = express();
|
|
17
|
+
const port = config.port;
|
|
18
|
+
|
|
19
|
+
// Middleware
|
|
20
|
+
app.use(cors());
|
|
21
|
+
// Access logging middleware
|
|
22
|
+
app.use((req: Request, res: Response, next) => {
|
|
23
|
+
const start = process.hrtime();
|
|
24
|
+
res.on('finish', () => {
|
|
25
|
+
const diff = process.hrtime(start);
|
|
26
|
+
const executionTimeMs = Number((diff[0] * 1e3 + diff[1] / 1e6).toFixed(2));
|
|
27
|
+
logger.info('HTTP Access', {
|
|
28
|
+
method: req.method,
|
|
29
|
+
path: req.originalUrl,
|
|
30
|
+
status: res.statusCode,
|
|
31
|
+
executionTimeMs
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
next();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
app.use(express.json());
|
|
38
|
+
|
|
39
|
+
// Basic route
|
|
40
|
+
app.get('/', (req: Request, res: Response) => {
|
|
41
|
+
res.json({ message: 'Welcome to the Data Dictionary Management System API' });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Health check endpoint
|
|
45
|
+
app.get('/health', (req: Request, res: Response) => {
|
|
46
|
+
res.json({ status: 'ok' });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// API routes
|
|
50
|
+
app.use(routes);
|
|
51
|
+
|
|
52
|
+
// Setup Swagger documentation
|
|
53
|
+
setupSwagger(app);
|
|
54
|
+
|
|
55
|
+
// =============================================================================
|
|
56
|
+
// Framework Filesystem & Git Routes (Phase 1)
|
|
57
|
+
// =============================================================================
|
|
58
|
+
|
|
59
|
+
async function mountFrameworkRoutes() {
|
|
60
|
+
try {
|
|
61
|
+
// Initialize filesystem framework
|
|
62
|
+
const { enricherRegistry } = await initializeFileSystem();
|
|
63
|
+
|
|
64
|
+
// Register YAML entity enricher
|
|
65
|
+
const yamlEnricher = createYamlFileInfoEnricher();
|
|
66
|
+
enricherRegistry.register(yamlEnricher);
|
|
67
|
+
|
|
68
|
+
// Initialize git service and routes
|
|
69
|
+
try {
|
|
70
|
+
const gitModule = await import('@hamak/ui-remote-git-fs-backend');
|
|
71
|
+
const workspaceRoots = new Map<string, string>([
|
|
72
|
+
['dictionaries', config.dataDir],
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
const gitService = gitModule.createGitService(workspaceRoots);
|
|
76
|
+
const gitEnricher = gitModule.createGitFileInfoEnricher({
|
|
77
|
+
gitService,
|
|
78
|
+
workspaceRoots,
|
|
79
|
+
});
|
|
80
|
+
enricherRegistry.register(gitEnricher);
|
|
81
|
+
|
|
82
|
+
const gitRoutes = gitModule.createGitRoutes({ gitService, debug: !config.isProduction });
|
|
83
|
+
app.use('/api/git', gitRoutes as any);
|
|
84
|
+
app.use('/api/git', gitModule.gitErrorHandler as any);
|
|
85
|
+
|
|
86
|
+
logger.info('Git routes mounted at /api/git');
|
|
87
|
+
} catch (gitError) {
|
|
88
|
+
logger.warn(`Git integration not available: ${gitError}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Mount filesystem routes
|
|
92
|
+
const fsRouter = getFileRouter();
|
|
93
|
+
app.use('/fs', fsRouter);
|
|
94
|
+
|
|
95
|
+
logger.info('Filesystem routes mounted at /fs');
|
|
96
|
+
} catch (error) {
|
|
97
|
+
logger.warn(`Framework filesystem not initialized: ${error}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Mount framework routes (non-blocking — existing routes work regardless)
|
|
102
|
+
mountFrameworkRoutes().catch((err) => {
|
|
103
|
+
logger.warn(`Failed to mount framework routes: ${err}`);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
// Error handling middleware
|
|
108
|
+
app.use((err: any, req: Request, res: Response, next: any) => {
|
|
109
|
+
logger.error(`Unhandled error: ${err.message}`);
|
|
110
|
+
res.status(500).json({
|
|
111
|
+
message: 'Internal server error',
|
|
112
|
+
error: config.isProduction ? undefined : err.message
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Serve frontend static files in production
|
|
117
|
+
if (config.isProduction) {
|
|
118
|
+
const publicDir = path.join(process.cwd(), 'public');
|
|
119
|
+
app.use(express.static(publicDir));
|
|
120
|
+
app.get('*', (req, res) => {
|
|
121
|
+
if (!req.path.startsWith('/api') && !req.path.startsWith('/fs') && !req.path.startsWith('/api-docs')) {
|
|
122
|
+
res.sendFile(path.join(publicDir, 'index.html'));
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
logger.info(`Serving frontend from ${publicDir}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Start server only when run directly (not when imported by tests)
|
|
129
|
+
const isMainModule = process.argv[1] && (
|
|
130
|
+
process.argv[1].endsWith('server.ts') || process.argv[1].endsWith('server.js')
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
if (isMainModule) {
|
|
134
|
+
app.listen(port, () => {
|
|
135
|
+
logger.info(`Server running on port ${port}`);
|
|
136
|
+
logger.info(`API documentation available at http://localhost:${port}/api-docs`);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export default app;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Entity, AttributeType } from '../../models/EntitySchema.js';
|
|
2
|
+
|
|
3
|
+
const mockEntities: Record<string, Entity> = {
|
|
4
|
+
'user-service.User': {
|
|
5
|
+
uuid: 'a38d1597-cc4f-4934-bb08-c876c023f693',
|
|
6
|
+
name: 'User',
|
|
7
|
+
description: 'User entity',
|
|
8
|
+
attributes: [
|
|
9
|
+
{ uuid: 'b49e2608-dd5f-4045-aa09-d464c234e694', name: 'id', description: 'User ID', type: AttributeType.STRING, required: true, primaryKey: true },
|
|
10
|
+
{ uuid: 'c5af3719-ee6f-4156-bb1a-e575d345f7a5', name: 'email', description: 'User email', type: AttributeType.STRING, required: true },
|
|
11
|
+
],
|
|
12
|
+
} as Entity,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// Mock entity service matching actual EntityService API
|
|
16
|
+
class EntityServiceMock {
|
|
17
|
+
validateEntity(entity: Entity) {
|
|
18
|
+
return { valid: true, errors: [] as string[] };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async validateRelationships(packageName: string, relationships: any[]) {
|
|
22
|
+
return { valid: true, errors: [] as string[] };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async saveEntity(entity: Entity, packageName: string) {
|
|
26
|
+
return { success: true, errors: [] as string[] };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async getEntity(packageName: string, entityName: string): Promise<Entity | null> {
|
|
30
|
+
return mockEntities[`${packageName}.${entityName}`] || null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async getRelatedEntities(packageName: string, entityName: string): Promise<Entity[]> {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const entityService = new EntityServiceMock();
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { AttributeType } from '../../models/EntitySchema.js';
|
|
2
|
+
|
|
3
|
+
// Mock service service matching actual ServiceService API
|
|
4
|
+
class ServiceServiceMock {
|
|
5
|
+
async getAllServices(): Promise<string[]> {
|
|
6
|
+
return ['user-service', 'product-service', 'order-service'];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async getServiceEntities(service: string) {
|
|
10
|
+
const serviceEntities: Record<string, any[]> = {
|
|
11
|
+
'user-service': [
|
|
12
|
+
{
|
|
13
|
+
uuid: 'a38d1597-cc4f-4934-bb08-c876c023f693',
|
|
14
|
+
name: 'User', description: 'User entity',
|
|
15
|
+
attributes: [
|
|
16
|
+
{ uuid: 'b49e2608-dd5f-4045-aa09-d464c234e694', name: 'id', description: 'User ID', type: AttributeType.STRING, required: true, primaryKey: true },
|
|
17
|
+
{ uuid: 'c5af3719-ee6f-4156-bb1a-e575d345f7a5', name: 'email', description: 'User email', type: AttributeType.STRING, required: true },
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
'product-service': [],
|
|
22
|
+
'order-service': [],
|
|
23
|
+
};
|
|
24
|
+
return serviceEntities[service] || [];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async getEntitySchema(service: string, entity: string) {
|
|
28
|
+
if (service === 'user-service' && entity === 'User') {
|
|
29
|
+
return {
|
|
30
|
+
uuid: 'a38d1597-cc4f-4934-bb08-c876c023f693',
|
|
31
|
+
name: 'User', description: 'User entity',
|
|
32
|
+
attributes: [
|
|
33
|
+
{ uuid: 'b49e2608-dd5f-4045-aa09-d464c234e694', name: 'id', description: 'User ID', type: AttributeType.STRING, required: true, primaryKey: true },
|
|
34
|
+
{ uuid: 'c5af3719-ee6f-4156-bb1a-e575d345f7a5', name: 'email', description: 'User email', type: AttributeType.STRING, required: true },
|
|
35
|
+
],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async createEntity(service: string, entity: any) {
|
|
42
|
+
return { success: true, errors: [] };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async updateEntity(service: string, entity: any) {
|
|
46
|
+
return { success: true, errors: [] };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async deleteEntity(service: string, entityName: string) {
|
|
50
|
+
return { success: true, errors: [] };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async searchEntities(query: string) {
|
|
54
|
+
return [
|
|
55
|
+
{ entity: 'User', service: 'user-service', matches: ['name', 'description'] },
|
|
56
|
+
];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async getGraphData(service: string) {
|
|
60
|
+
return {
|
|
61
|
+
nodes: [
|
|
62
|
+
{ id: 'User', label: 'User', type: 'entity' },
|
|
63
|
+
{ id: 'Profile', label: 'Profile', type: 'entity' },
|
|
64
|
+
],
|
|
65
|
+
edges: [
|
|
66
|
+
{ source: 'User', target: 'Profile', label: 'hasOne' },
|
|
67
|
+
],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async getPackageRelationships(packageName: string) {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async createRelationship(packageName: string, relationship: any) {
|
|
76
|
+
return { success: true, errors: [], relationship };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async updateRelationship(packageName: string, uuid: string, relationship: any) {
|
|
80
|
+
return { success: true, errors: [] };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async deleteRelationship(packageName: string, uuid: string) {
|
|
84
|
+
return { success: true, errors: [] };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export const serviceService = new ServiceServiceMock();
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Mock version service matching actual VersionService API
|
|
2
|
+
class VersionServiceMock {
|
|
3
|
+
async commitChanges(message: string) {
|
|
4
|
+
return {
|
|
5
|
+
success: true,
|
|
6
|
+
errors: [] as string[],
|
|
7
|
+
commitHash: 'mock-commit-hash-abc123',
|
|
8
|
+
timestamp: new Date('2026-01-01T12:00:00Z'),
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async getCommitHistory(limit: number = 10) {
|
|
13
|
+
return [
|
|
14
|
+
{
|
|
15
|
+
hash: 'mock-commit-1',
|
|
16
|
+
message: 'Initial commit',
|
|
17
|
+
author: 'Test User',
|
|
18
|
+
date: '2023-01-01T12:00:00Z',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
hash: 'mock-commit-2',
|
|
22
|
+
message: 'Update User entity',
|
|
23
|
+
author: 'Test User',
|
|
24
|
+
date: '2023-01-02T12:00:00Z',
|
|
25
|
+
},
|
|
26
|
+
];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async revertToCommit(commitHash: string) {
|
|
30
|
+
return {
|
|
31
|
+
success: true,
|
|
32
|
+
errors: [] as string[],
|
|
33
|
+
newCommitHash: 'mock-revert-hash-def456',
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const versionService = new VersionServiceMock();
|