@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,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized Configuration
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for backend configuration values.
|
|
5
|
+
* Reads from environment variables with sensible defaults.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import dotenv from 'dotenv';
|
|
10
|
+
|
|
11
|
+
dotenv.config();
|
|
12
|
+
|
|
13
|
+
const isProduction = process.env.NODE_ENV === 'production';
|
|
14
|
+
|
|
15
|
+
export const config = {
|
|
16
|
+
/** Server port */
|
|
17
|
+
port: parseInt(process.env.PORT || '3001', 10),
|
|
18
|
+
|
|
19
|
+
/** Base directory for data dictionaries (YAML files) */
|
|
20
|
+
dataDir: process.env.DATA_DIR
|
|
21
|
+
|| (isProduction
|
|
22
|
+
? path.join(process.cwd(), 'data-dictionaries')
|
|
23
|
+
: path.join(process.cwd(), '..', 'data-dictionaries')),
|
|
24
|
+
|
|
25
|
+
/** Deployment profile: local | team | server */
|
|
26
|
+
profile: (process.env.PROFILE || 'local') as 'local' | 'team' | 'server',
|
|
27
|
+
|
|
28
|
+
/** JWT configuration */
|
|
29
|
+
jwt: {
|
|
30
|
+
secret: process.env.JWT_SECRET || 'dev-secret-key',
|
|
31
|
+
expiresIn: process.env.JWT_EXPIRES_IN || '24h',
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
/** Git configuration */
|
|
35
|
+
git: {
|
|
36
|
+
/** Whether to auto-commit on entity changes */
|
|
37
|
+
autoCommit: process.env.GIT_AUTO_COMMIT !== 'false',
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
/** Environment */
|
|
41
|
+
nodeEnv: process.env.NODE_ENV || 'development',
|
|
42
|
+
isProduction,
|
|
43
|
+
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
import { logger } from '../utils/logger.js';
|
|
3
|
+
|
|
4
|
+
// Interface for user roles
|
|
5
|
+
export enum UserRole {
|
|
6
|
+
ADMIN = 'admin',
|
|
7
|
+
EDITOR = 'editor',
|
|
8
|
+
VIEWER = 'viewer'
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Interface for user information
|
|
12
|
+
export interface User {
|
|
13
|
+
id: string;
|
|
14
|
+
username: string;
|
|
15
|
+
role: UserRole;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Mock user database - in a real application, this would be stored in a database
|
|
20
|
+
* and passwords would be hashed
|
|
21
|
+
*/
|
|
22
|
+
const users: Record<string, { password: string; user: User }> = {
|
|
23
|
+
'admin': {
|
|
24
|
+
password: 'admin123',
|
|
25
|
+
user: {
|
|
26
|
+
id: '1',
|
|
27
|
+
username: 'admin',
|
|
28
|
+
role: UserRole.ADMIN
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
'editor': {
|
|
32
|
+
password: 'editor123',
|
|
33
|
+
user: {
|
|
34
|
+
id: '2',
|
|
35
|
+
username: 'editor',
|
|
36
|
+
role: UserRole.EDITOR
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
'viewer': {
|
|
40
|
+
password: 'viewer123',
|
|
41
|
+
user: {
|
|
42
|
+
id: '3',
|
|
43
|
+
username: 'viewer',
|
|
44
|
+
role: UserRole.VIEWER
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Basic authentication middleware
|
|
51
|
+
* Extracts credentials from Authorization header and validates them
|
|
52
|
+
* @param req Express request
|
|
53
|
+
* @param res Express response
|
|
54
|
+
* @param next Next function
|
|
55
|
+
*/
|
|
56
|
+
export const basicAuth = (req: Request, res: Response, next: NextFunction) => {
|
|
57
|
+
// Get authorization header
|
|
58
|
+
const authHeader = req.headers.authorization;
|
|
59
|
+
|
|
60
|
+
if (!authHeader || !authHeader.startsWith('Basic ')) {
|
|
61
|
+
return res.status(401).json({
|
|
62
|
+
message: 'Authentication required',
|
|
63
|
+
error: 'Missing or invalid Authorization header'
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Extract and decode credentials
|
|
68
|
+
try {
|
|
69
|
+
const base64Credentials = authHeader.split(' ')[1];
|
|
70
|
+
const credentials = Buffer.from(base64Credentials, 'base64').toString('utf-8');
|
|
71
|
+
const [username, password] = credentials.split(':');
|
|
72
|
+
|
|
73
|
+
// Validate credentials
|
|
74
|
+
const userRecord = users[username];
|
|
75
|
+
|
|
76
|
+
if (!userRecord || userRecord.password !== password) {
|
|
77
|
+
return res.status(401).json({
|
|
78
|
+
message: 'Authentication failed',
|
|
79
|
+
error: 'Invalid username or password'
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Add user to request object
|
|
84
|
+
(req as any).user = userRecord.user;
|
|
85
|
+
|
|
86
|
+
next();
|
|
87
|
+
} catch (error) {
|
|
88
|
+
logger.error(`Authentication error: ${error}`);
|
|
89
|
+
return res.status(401).json({
|
|
90
|
+
message: 'Authentication failed',
|
|
91
|
+
error: 'Invalid credentials format'
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Role-based authorization middleware
|
|
98
|
+
* Checks if the authenticated user has one of the required roles
|
|
99
|
+
* @param allowedRoles Array of allowed roles
|
|
100
|
+
* @returns Middleware function
|
|
101
|
+
*/
|
|
102
|
+
export const authenticate = (allowedRoles: UserRole[]) => {
|
|
103
|
+
return (req: Request, res: Response, next: NextFunction) => {
|
|
104
|
+
// First apply basic authentication
|
|
105
|
+
basicAuth(req, res, (err) => {
|
|
106
|
+
if (err) return next(err);
|
|
107
|
+
|
|
108
|
+
// Check if user has required role
|
|
109
|
+
const user = (req as any).user as User;
|
|
110
|
+
|
|
111
|
+
if (!user) {
|
|
112
|
+
return res.status(401).json({
|
|
113
|
+
message: 'Authentication required',
|
|
114
|
+
error: 'User not authenticated'
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!allowedRoles.includes(user.role)) {
|
|
119
|
+
return res.status(403).json({
|
|
120
|
+
message: 'Access denied',
|
|
121
|
+
error: 'Insufficient permissions'
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
next();
|
|
126
|
+
});
|
|
127
|
+
};
|
|
128
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
import jwt from 'jsonwebtoken';
|
|
3
|
+
import { logger } from '../utils/logger.js';
|
|
4
|
+
import { User } from './auth.js';
|
|
5
|
+
|
|
6
|
+
// JWT secret key - should be in environment variables in production
|
|
7
|
+
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* JWT authentication middleware
|
|
11
|
+
* Verifies JWT token from Authorization header
|
|
12
|
+
* @param req Express request
|
|
13
|
+
* @param res Express response
|
|
14
|
+
* @param next Next function
|
|
15
|
+
*/
|
|
16
|
+
export const verifyToken = (req: Request, res: Response, next: NextFunction) => {
|
|
17
|
+
// Development mode bypass for testing
|
|
18
|
+
if (process.env.NODE_ENV === 'development' && req.headers.authorization === 'Bearer mock-token-for-testing') {
|
|
19
|
+
logger.info('Using mock authentication token for development');
|
|
20
|
+
// Set a mock user for development
|
|
21
|
+
(req as any).user = {
|
|
22
|
+
id: 'dev-user',
|
|
23
|
+
username: 'developer',
|
|
24
|
+
email: 'dev@example.com',
|
|
25
|
+
role: 'admin'
|
|
26
|
+
};
|
|
27
|
+
return next();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Get authorization header
|
|
31
|
+
const authHeader = req.headers.authorization;
|
|
32
|
+
|
|
33
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
34
|
+
return res.status(401).json({
|
|
35
|
+
message: 'Authentication required',
|
|
36
|
+
error: 'Missing or invalid Authorization header'
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Extract token
|
|
41
|
+
const token = authHeader.split(' ')[1];
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
// Verify token
|
|
45
|
+
const decoded = jwt.verify(token, JWT_SECRET as jwt.Secret) as User;
|
|
46
|
+
|
|
47
|
+
// Add user to request object
|
|
48
|
+
(req as any).user = decoded;
|
|
49
|
+
|
|
50
|
+
next();
|
|
51
|
+
} catch (error: any) {
|
|
52
|
+
logger.error(`JWT verification error: ${error.message}`);
|
|
53
|
+
|
|
54
|
+
if (error.name === 'TokenExpiredError') {
|
|
55
|
+
return res.status(401).json({
|
|
56
|
+
message: 'Authentication failed',
|
|
57
|
+
error: 'Token expired'
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return res.status(401).json({
|
|
62
|
+
message: 'Authentication failed',
|
|
63
|
+
error: 'Invalid token'
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Role-based authorization middleware using JWT
|
|
70
|
+
* Checks if the authenticated user has one of the required roles
|
|
71
|
+
* @param allowedRoles Array of allowed roles
|
|
72
|
+
* @returns Middleware function
|
|
73
|
+
*/
|
|
74
|
+
export const authorizeJwt = (allowedRoles: string[]) => {
|
|
75
|
+
return (req: Request, res: Response, next: NextFunction) => {
|
|
76
|
+
// First verify token
|
|
77
|
+
verifyToken(req, res, (err) => {
|
|
78
|
+
if (err) return next(err);
|
|
79
|
+
|
|
80
|
+
// Check if user has required role
|
|
81
|
+
const user = (req as any).user as User;
|
|
82
|
+
|
|
83
|
+
if (!user) {
|
|
84
|
+
return res.status(401).json({
|
|
85
|
+
message: 'Authentication required',
|
|
86
|
+
error: 'User not authenticated'
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!allowedRoles.includes(user.role)) {
|
|
91
|
+
return res.status(403).json({
|
|
92
|
+
message: 'Access denied',
|
|
93
|
+
error: 'Insufficient permissions'
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
next();
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Entity, MetadataDefinition, MetadataEntry, Relationship } from './EntitySchema.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Package type annotation
|
|
5
|
+
*/
|
|
6
|
+
export enum PackageType {
|
|
7
|
+
PROJECT = 'project',
|
|
8
|
+
MICROSERVICE = 'microservice',
|
|
9
|
+
MODULE = 'module'
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Represents a hierarchical package, which can contain subpackages and/or entities.
|
|
14
|
+
*/
|
|
15
|
+
export interface Package {
|
|
16
|
+
id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
description?: string;
|
|
19
|
+
type?: PackageType | string;
|
|
20
|
+
entities: Entity[];
|
|
21
|
+
subPackages: Package[];
|
|
22
|
+
relationships: Relationship[];
|
|
23
|
+
metadata?: MetadataEntry[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Dictionary model interface.
|
|
28
|
+
* Supports a hierarchy of packages via the rootPackage property.
|
|
29
|
+
*/
|
|
30
|
+
export interface Dictionary {
|
|
31
|
+
id: string;
|
|
32
|
+
name: string;
|
|
33
|
+
description?: string;
|
|
34
|
+
metadataDefinitions?: MetadataDefinition[];
|
|
35
|
+
createdAt?: Date;
|
|
36
|
+
updatedAt?: Date;
|
|
37
|
+
rootPackage: Package;
|
|
38
|
+
}
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import { Validator } from 'jsonschema';
|
|
2
|
+
import { generateUUID, isValidUUID } from '../utils/uuid.js';
|
|
3
|
+
type Schema = any; // Using any as a workaround for missing type definitions
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Supported attribute types in the data dictionary
|
|
7
|
+
*/
|
|
8
|
+
export enum AttributeType {
|
|
9
|
+
STRING = 'string',
|
|
10
|
+
NUMBER = 'number',
|
|
11
|
+
INTEGER = 'integer',
|
|
12
|
+
BOOLEAN = 'boolean',
|
|
13
|
+
DATETIME = 'datetime',
|
|
14
|
+
DATE = 'date',
|
|
15
|
+
TIME = 'time',
|
|
16
|
+
DATE_TIME = 'date-time',
|
|
17
|
+
TIMESTAMP = 'timestamp',
|
|
18
|
+
DURATION = 'duration',
|
|
19
|
+
ENUM = 'enum',
|
|
20
|
+
OBJECT = 'object',
|
|
21
|
+
ARRAY = 'array'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Cardinality for relationship ends
|
|
26
|
+
*/
|
|
27
|
+
export enum Cardinality {
|
|
28
|
+
ONE = 'one',
|
|
29
|
+
MANY = 'many'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Metadata value types for typed metadata definitions
|
|
34
|
+
*/
|
|
35
|
+
export enum MetadataValueType {
|
|
36
|
+
STRING = 'string',
|
|
37
|
+
NUMBER = 'number',
|
|
38
|
+
BOOLEAN = 'boolean',
|
|
39
|
+
DATE = 'date',
|
|
40
|
+
FLAG = 'flag',
|
|
41
|
+
RULE = 'rule',
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export enum RuleSeverity {
|
|
45
|
+
INFO = 'info',
|
|
46
|
+
WARNING = 'warning',
|
|
47
|
+
ERROR = 'error',
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A metadata definition (schema for metadata entries)
|
|
52
|
+
*/
|
|
53
|
+
export interface MetadataDefinition {
|
|
54
|
+
name: string;
|
|
55
|
+
type: MetadataValueType;
|
|
56
|
+
description?: string;
|
|
57
|
+
required?: boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* A typed metadata entry
|
|
62
|
+
*/
|
|
63
|
+
export interface MetadataEntry {
|
|
64
|
+
name: string;
|
|
65
|
+
value: string | number | boolean;
|
|
66
|
+
severity?: RuleSeverity;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export enum EntityStatus {
|
|
70
|
+
DRAFT = 'draft',
|
|
71
|
+
SUBMITTED = 'submitted',
|
|
72
|
+
APPROVED = 'approved',
|
|
73
|
+
RETURNED = 'returned',
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface ReviewComment {
|
|
77
|
+
id: string;
|
|
78
|
+
author: string;
|
|
79
|
+
timestamp: string;
|
|
80
|
+
message: string;
|
|
81
|
+
targetField?: string;
|
|
82
|
+
resolved?: boolean;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export type StereotypeTarget = 'package' | 'entity' | 'attribute';
|
|
86
|
+
|
|
87
|
+
export interface Stereotype {
|
|
88
|
+
id: string;
|
|
89
|
+
name: string;
|
|
90
|
+
description?: string;
|
|
91
|
+
appliesTo: StereotypeTarget;
|
|
92
|
+
metadataDefinitions: MetadataDefinition[];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* A node in a perspective — annotates, sets frontier, or excludes a path
|
|
97
|
+
*/
|
|
98
|
+
export interface PerspectiveNode {
|
|
99
|
+
path: string;
|
|
100
|
+
traverse?: boolean;
|
|
101
|
+
exclude?: boolean;
|
|
102
|
+
metadata?: MetadataEntry[];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* A perspective is a business view over a subset of the data model
|
|
107
|
+
*/
|
|
108
|
+
export interface Perspective {
|
|
109
|
+
uuid: string;
|
|
110
|
+
name: string;
|
|
111
|
+
description?: string;
|
|
112
|
+
rootEntities: string[];
|
|
113
|
+
nodes?: PerspectiveNode[];
|
|
114
|
+
maxDepth?: number;
|
|
115
|
+
metadata?: MetadataEntry[];
|
|
116
|
+
createdAt?: string;
|
|
117
|
+
updatedAt?: string;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* A resolved node from BFS traversal of a perspective
|
|
122
|
+
*/
|
|
123
|
+
export interface ResolvedNode {
|
|
124
|
+
entityUuid: string;
|
|
125
|
+
entityName: string;
|
|
126
|
+
service: string;
|
|
127
|
+
path: string;
|
|
128
|
+
hopDistance: number;
|
|
129
|
+
isRoot: boolean;
|
|
130
|
+
isFrontier: boolean;
|
|
131
|
+
isManualInclusion: boolean;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* A perspective with its resolved entity graph
|
|
136
|
+
*/
|
|
137
|
+
export interface ResolvedPerspective extends Perspective {
|
|
138
|
+
resolvedNodes: ResolvedNode[];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Grouped constraint fields for attributes
|
|
143
|
+
*/
|
|
144
|
+
export interface AttributeConstraints {
|
|
145
|
+
minLength?: number;
|
|
146
|
+
maxLength?: number;
|
|
147
|
+
pattern?: string;
|
|
148
|
+
format?: string;
|
|
149
|
+
minimum?: number;
|
|
150
|
+
maximum?: number;
|
|
151
|
+
precision?: number;
|
|
152
|
+
scale?: number;
|
|
153
|
+
enumValues?: string[];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Interface for entity attribute definition
|
|
158
|
+
*/
|
|
159
|
+
export interface Attribute {
|
|
160
|
+
uuid: string;
|
|
161
|
+
name: string;
|
|
162
|
+
description: string;
|
|
163
|
+
type: AttributeType;
|
|
164
|
+
required: boolean;
|
|
165
|
+
unique?: boolean;
|
|
166
|
+
primaryKey?: boolean;
|
|
167
|
+
defaultValue?: any;
|
|
168
|
+
examples?: any[];
|
|
169
|
+
constraints?: AttributeConstraints;
|
|
170
|
+
|
|
171
|
+
// For object and array types
|
|
172
|
+
items?: Attribute;
|
|
173
|
+
properties?: Attribute[];
|
|
174
|
+
|
|
175
|
+
// Typed metadata
|
|
176
|
+
metadata?: MetadataEntry[];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* One end of a relationship
|
|
181
|
+
*/
|
|
182
|
+
export interface RelationshipEnd {
|
|
183
|
+
entity: string; // UUID of the entity
|
|
184
|
+
cardinality: Cardinality;
|
|
185
|
+
name?: string; // Navigation property name
|
|
186
|
+
referenceAttributes?: string[];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* A relationship between two entities, stored at package level
|
|
191
|
+
*/
|
|
192
|
+
export type RelationshipType = 'structural' | 'lineage';
|
|
193
|
+
|
|
194
|
+
export interface Relationship {
|
|
195
|
+
uuid: string;
|
|
196
|
+
description?: string;
|
|
197
|
+
type?: RelationshipType;
|
|
198
|
+
source: RelationshipEnd;
|
|
199
|
+
target: RelationshipEnd;
|
|
200
|
+
metadata?: MetadataEntry[];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export interface LineageNode {
|
|
204
|
+
entityUuid: string;
|
|
205
|
+
entityName: string;
|
|
206
|
+
service: string;
|
|
207
|
+
direction: 'upstream' | 'downstream';
|
|
208
|
+
depth: number;
|
|
209
|
+
relationship: { uuid: string; description: string };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export interface LineageResult {
|
|
213
|
+
entity: { uuid: string; name: string; service: string };
|
|
214
|
+
upstream: LineageNode[];
|
|
215
|
+
downstream: LineageNode[];
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Interface for entity definition
|
|
220
|
+
*/
|
|
221
|
+
export interface Entity {
|
|
222
|
+
uuid: string;
|
|
223
|
+
name: string;
|
|
224
|
+
description?: string;
|
|
225
|
+
stereotype?: string;
|
|
226
|
+
status?: EntityStatus;
|
|
227
|
+
attributes: Attribute[];
|
|
228
|
+
metadata?: MetadataEntry[];
|
|
229
|
+
createdAt?: string;
|
|
230
|
+
updatedAt?: string;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Backward-compatible alias
|
|
234
|
+
export type EntityAttribute = Attribute;
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* JSON Schema for validating entity definitions
|
|
238
|
+
*/
|
|
239
|
+
export const entitySchema: Schema = {
|
|
240
|
+
type: 'object',
|
|
241
|
+
required: ['uuid', 'name', 'attributes'],
|
|
242
|
+
properties: {
|
|
243
|
+
uuid: { type: 'string', pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' },
|
|
244
|
+
name: { type: 'string' },
|
|
245
|
+
description: { type: 'string' },
|
|
246
|
+
attributes: {
|
|
247
|
+
type: 'array',
|
|
248
|
+
items: {
|
|
249
|
+
type: 'object',
|
|
250
|
+
required: ['uuid', 'name', 'description', 'type', 'required'],
|
|
251
|
+
properties: {
|
|
252
|
+
uuid: { type: 'string', pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' },
|
|
253
|
+
name: { type: 'string' },
|
|
254
|
+
description: { type: 'string' },
|
|
255
|
+
type: {
|
|
256
|
+
type: 'string',
|
|
257
|
+
enum: Object.values(AttributeType)
|
|
258
|
+
},
|
|
259
|
+
required: { type: 'boolean' },
|
|
260
|
+
unique: { type: 'boolean' },
|
|
261
|
+
primaryKey: { type: 'boolean' },
|
|
262
|
+
defaultValue: { },
|
|
263
|
+
examples: { type: 'array' },
|
|
264
|
+
constraints: {
|
|
265
|
+
type: 'object',
|
|
266
|
+
properties: {
|
|
267
|
+
minLength: { type: 'integer', minimum: 0 },
|
|
268
|
+
maxLength: { type: 'integer', minimum: 0 },
|
|
269
|
+
pattern: { type: 'string' },
|
|
270
|
+
format: { type: 'string' },
|
|
271
|
+
minimum: { type: 'number' },
|
|
272
|
+
maximum: { type: 'number' },
|
|
273
|
+
precision: { type: 'integer', minimum: 0 },
|
|
274
|
+
scale: { type: 'integer', minimum: 0 },
|
|
275
|
+
enumValues: {
|
|
276
|
+
type: 'array',
|
|
277
|
+
items: { type: 'string' }
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
items: { type: 'object' },
|
|
282
|
+
properties: { type: 'array' },
|
|
283
|
+
metadata: { type: 'array' }
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
metadata: { type: 'array' },
|
|
288
|
+
createdAt: { type: 'string', format: 'date-time' },
|
|
289
|
+
updatedAt: { type: 'string', format: 'date-time' }
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* JSON Schema for validating relationship definitions
|
|
295
|
+
*/
|
|
296
|
+
export const relationshipSchema: Schema = {
|
|
297
|
+
type: 'object',
|
|
298
|
+
required: ['uuid', 'source', 'target'],
|
|
299
|
+
properties: {
|
|
300
|
+
uuid: { type: 'string', pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' },
|
|
301
|
+
description: { type: 'string' },
|
|
302
|
+
source: {
|
|
303
|
+
type: 'object',
|
|
304
|
+
required: ['entity', 'cardinality'],
|
|
305
|
+
properties: {
|
|
306
|
+
entity: { type: 'string' },
|
|
307
|
+
cardinality: { type: 'string', enum: Object.values(Cardinality) },
|
|
308
|
+
name: { type: 'string' },
|
|
309
|
+
referenceAttributes: { type: 'array', items: { type: 'string' } }
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
target: {
|
|
313
|
+
type: 'object',
|
|
314
|
+
required: ['entity', 'cardinality'],
|
|
315
|
+
properties: {
|
|
316
|
+
entity: { type: 'string' },
|
|
317
|
+
cardinality: { type: 'string', enum: Object.values(Cardinality) },
|
|
318
|
+
name: { type: 'string' },
|
|
319
|
+
referenceAttributes: { type: 'array', items: { type: 'string' } }
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
metadata: { type: 'array' }
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Validates an entity definition against the schema
|
|
328
|
+
*/
|
|
329
|
+
export function validateEntity(entity: Entity): { valid: boolean; errors: string[] } {
|
|
330
|
+
const validator = new Validator();
|
|
331
|
+
const result = validator.validate(entity, entitySchema);
|
|
332
|
+
|
|
333
|
+
const errors: string[] = result.errors.map((error: any) => error.stack);
|
|
334
|
+
|
|
335
|
+
// Additional UUID validation
|
|
336
|
+
if (!isValidUUID(entity.uuid)) {
|
|
337
|
+
errors.push('Entity UUID is invalid');
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Validate attribute UUIDs
|
|
341
|
+
if (entity.attributes) {
|
|
342
|
+
entity.attributes.forEach((attr, index) => {
|
|
343
|
+
if (!isValidUUID(attr.uuid)) {
|
|
344
|
+
errors.push(`Attribute ${index} UUID is invalid`);
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return {
|
|
350
|
+
valid: result.valid && errors.length === 0,
|
|
351
|
+
errors
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Validates a relationship definition against the schema
|
|
357
|
+
*/
|
|
358
|
+
export function validateRelationship(relationship: Relationship): { valid: boolean; errors: string[] } {
|
|
359
|
+
const validator = new Validator();
|
|
360
|
+
const result = validator.validate(relationship, relationshipSchema);
|
|
361
|
+
|
|
362
|
+
const errors: string[] = result.errors.map((error: any) => error.stack);
|
|
363
|
+
|
|
364
|
+
if (!isValidUUID(relationship.uuid)) {
|
|
365
|
+
errors.push('Relationship UUID is invalid');
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return {
|
|
369
|
+
valid: result.valid && errors.length === 0,
|
|
370
|
+
errors
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Creates a new entity with UUIDs for all components
|
|
376
|
+
*/
|
|
377
|
+
export function createEntityWithUUIDs(entityData: Omit<Entity, 'uuid' | 'attributes'> & {
|
|
378
|
+
attributes: Omit<Attribute, 'uuid'>[];
|
|
379
|
+
}): Entity {
|
|
380
|
+
return {
|
|
381
|
+
...entityData,
|
|
382
|
+
uuid: generateUUID(),
|
|
383
|
+
attributes: entityData.attributes.map(attr => ({
|
|
384
|
+
...attr,
|
|
385
|
+
uuid: generateUUID(),
|
|
386
|
+
properties: attr.properties ? attr.properties.map(prop => ({
|
|
387
|
+
...prop,
|
|
388
|
+
uuid: prop.uuid || generateUUID()
|
|
389
|
+
})) : attr.properties,
|
|
390
|
+
items: attr.items ? { ...attr.items, uuid: attr.items.uuid || generateUUID() } : attr.items
|
|
391
|
+
}))
|
|
392
|
+
};
|
|
393
|
+
}
|