Improve Team-indeling dashboard UI and cache invalidation

- Replace 'TEAM' label with Type attribute (Business/Enabling/Staf) in team blocks
- Make Type labels larger (text-sm) and brighter colors
- Make SUBTEAM label less bright (indigo-300) and smaller (text-[10px])
- Add 'FTE' suffix to bandbreedte values in header and application blocks
- Add Platform and Connected Device labels to application blocks
- Show Platform FTE and Workloads FTE separately in Platform blocks
- Add spacing between Regiemodel letter and count value
- Add cache invalidation for Team Dashboard when applications are updated
- Enrich team references with Type attribute in getSubteamToTeamMapping
This commit is contained in:
2026-01-10 02:16:55 +01:00
parent ea1c84262c
commit ca21b9538d
54 changed files with 13444 additions and 1789 deletions

View File

@@ -0,0 +1,982 @@
#!/usr/bin/env npx tsx
/**
* Schema Generator - Fetches Jira Assets schema DYNAMICALLY and generates:
* - TypeScript types (jira-types.ts)
* - Schema metadata (jira-schema.ts)
* - Database schema (db-schema.sql)
*
* This script connects to the Jira Assets API to discover ALL object types
* and their attributes, ensuring the data model is always in sync with the
* actual CMDB configuration.
*
* Usage: npm run generate-schema
*/
import * as fs from 'fs';
import * as path from 'path';
import { fileURLToPath } from 'url';
import dotenv from 'dotenv';
// Load environment variables
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Try multiple possible .env locations
const envPaths = [
path.resolve(__dirname, '../../.env'), // backend/.env
path.resolve(__dirname, '../../../.env'), // project root .env
];
let envLoaded = '';
for (const envPath of envPaths) {
if (fs.existsSync(envPath)) {
dotenv.config({ path: envPath });
envLoaded = envPath;
break;
}
}
// Configuration
const JIRA_HOST = process.env.JIRA_HOST || '';
const JIRA_PAT = process.env.JIRA_PAT || '';
const JIRA_SCHEMA_ID = process.env.JIRA_SCHEMA_ID || '';
const OUTPUT_DIR = path.resolve(__dirname, '../src/generated');
// =============================================================================
// Interfaces
// =============================================================================
interface JiraObjectSchema {
id: number;
name: string;
objectSchemaKey: string;
description?: string;
objectCount?: number;
}
interface JiraObjectType {
id: number;
name: string;
description?: string;
iconId?: number;
objectCount?: number;
parentObjectTypeId?: number;
objectSchemaId: number;
inherited?: boolean;
abstractObjectType?: boolean;
parentObjectTypeInherited?: boolean;
}
interface JiraAttribute {
id: number;
name: string;
label?: string;
type: number;
typeValue?: string;
defaultType?: { id: number; name: string };
referenceObjectTypeId?: number;
referenceObjectType?: { id: number; name: string };
referenceType?: { id: number; name: string };
minimumCardinality?: number;
maximumCardinality?: number;
editable?: boolean;
hidden?: boolean;
system?: boolean;
options?: string;
description?: string;
}
interface GeneratedAttribute {
jiraId: number;
name: string;
fieldName: string;
type: 'text' | 'integer' | 'float' | 'boolean' | 'date' | 'datetime' | 'select' | 'reference' | 'url' | 'email' | 'textarea' | 'user' | 'status' | 'unknown';
isMultiple: boolean;
isEditable: boolean;
isRequired: boolean;
isSystem: boolean;
referenceTypeId?: number;
referenceTypeName?: string;
description?: string;
}
interface GeneratedObjectType {
jiraTypeId: number;
name: string;
typeName: string;
syncPriority: number;
objectCount: number;
attributes: GeneratedAttribute[];
}
// =============================================================================
// Jira Type Mapping
// =============================================================================
// Jira attribute type mappings (based on Jira Insight/Assets API)
// See: https://docs.atlassian.com/jira-servicemanagement-docs/REST/5.x/insight/1.0/objecttypeattribute
const JIRA_TYPE_MAP: Record<number, GeneratedAttribute['type']> = {
0: 'text', // Default/Text
1: 'integer', // Integer
2: 'boolean', // Boolean
3: 'float', // Double/Float
4: 'date', // Date
5: 'datetime', // DateTime
6: 'url', // URL
7: 'email', // Email
8: 'textarea', // Textarea
9: 'select', // Select
10: 'reference', // Reference (Object)
11: 'user', // User
12: 'reference', // Confluence (treated as reference)
13: 'reference', // Group (treated as reference)
14: 'reference', // Version (treated as reference)
15: 'reference', // Project (treated as reference)
16: 'status', // Status
};
// Priority types - these sync first as they are reference data
const PRIORITY_TYPE_NAMES = new Set([
'Application Component',
'Server',
'Flows',
]);
// Reference data types - these sync with lower priority
const REFERENCE_TYPE_PATTERNS = [
/Factor$/,
/Model$/,
/Type$/,
/Category$/,
/Importance$/,
/Analyse$/,
/Organisation$/,
/Function$/,
];
// =============================================================================
// Jira Schema Fetcher
// =============================================================================
class JiraSchemaFetcher {
private baseUrl: string;
private headers: Record<string, string>;
constructor(host: string, pat: string) {
this.baseUrl = `${host}/rest/insight/1.0`;
this.headers = {
'Authorization': `Bearer ${pat}`,
'Content-Type': 'application/json',
'Accept': 'application/json',
};
}
/**
* Fetch schema info
*/
async fetchSchema(schemaId: string): Promise<JiraObjectSchema | null> {
try {
const response = await fetch(`${this.baseUrl}/objectschema/${schemaId}`, {
headers: this.headers,
});
if (!response.ok) {
console.error(`Failed to fetch schema ${schemaId}: ${response.status} ${response.statusText}`);
const text = await response.text();
console.error(`Response: ${text}`);
return null;
}
return await response.json();
} catch (error) {
console.error(`Error fetching schema ${schemaId}:`, error);
return null;
}
}
/**
* Fetch ALL object types in the schema
*/
async fetchAllObjectTypes(schemaId: string): Promise<JiraObjectType[]> {
try {
// Try the objecttypes/flat endpoint first (returns all types in flat structure)
let response = await fetch(`${this.baseUrl}/objectschema/${schemaId}/objecttypes/flat`, {
headers: this.headers,
});
if (!response.ok) {
// Fallback to regular objecttypes endpoint
response = await fetch(`${this.baseUrl}/objectschema/${schemaId}/objecttypes`, {
headers: this.headers,
});
}
if (!response.ok) {
console.error(`Failed to fetch object types: ${response.status} ${response.statusText}`);
const text = await response.text();
console.error(`Response: ${text}`);
return [];
}
const result = await response.json();
// Handle both array and object responses
if (Array.isArray(result)) {
return result;
} else if (result.objectTypes) {
return result.objectTypes;
}
return [];
} catch (error) {
console.error(`Error fetching object types:`, error);
return [];
}
}
/**
* Fetch attributes for a specific object type
*/
async fetchAttributes(typeId: number): Promise<JiraAttribute[]> {
try {
const response = await fetch(`${this.baseUrl}/objecttype/${typeId}/attributes`, {
headers: this.headers,
});
if (!response.ok) {
console.error(`Failed to fetch attributes for type ${typeId}: ${response.status}`);
return [];
}
return await response.json();
} catch (error) {
console.error(`Error fetching attributes for type ${typeId}:`, error);
return [];
}
}
/**
* Test the connection
*/
async testConnection(): Promise<boolean> {
try {
const response = await fetch(`${this.baseUrl}/objectschema/list`, {
headers: this.headers,
});
return response.ok;
} catch {
return false;
}
}
}
// =============================================================================
// Utility Functions
// =============================================================================
/**
* Convert a string to camelCase while preserving existing casing patterns
* E.g., "Application Function" -> "applicationFunction"
* "ICT Governance Model" -> "ictGovernanceModel"
* "ApplicationFunction" -> "applicationFunction"
*/
function toCamelCase(str: string): string {
// First split on spaces and special chars
const words = str
.replace(/[^a-zA-Z0-9\s]/g, ' ')
.split(/\s+/)
.filter(w => w.length > 0);
if (words.length === 0) return '';
// If it's a single word that's already camelCase or PascalCase, just lowercase first char
if (words.length === 1) {
const word = words[0];
return word.charAt(0).toLowerCase() + word.slice(1);
}
// Multiple words - first word lowercase, rest capitalize first letter
return words
.map((word, index) => {
if (index === 0) {
// First word: if all uppercase (acronym), lowercase it, otherwise just lowercase first char
if (word === word.toUpperCase() && word.length > 1) {
return word.toLowerCase();
}
return word.charAt(0).toLowerCase() + word.slice(1);
}
// Other words: capitalize first letter, keep rest as-is
return word.charAt(0).toUpperCase() + word.slice(1);
})
.join('');
}
/**
* Convert a string to PascalCase while preserving existing casing patterns
* E.g., "Application Function" -> "ApplicationFunction"
* "ICT Governance Model" -> "IctGovernanceModel"
* "applicationFunction" -> "ApplicationFunction"
*/
function toPascalCase(str: string): string {
// First split on spaces and special chars
const words = str
.replace(/[^a-zA-Z0-9\s]/g, ' ')
.split(/\s+/)
.filter(w => w.length > 0);
if (words.length === 0) return '';
// If it's a single word, just capitalize first letter
if (words.length === 1) {
const word = words[0];
return word.charAt(0).toUpperCase() + word.slice(1);
}
// Multiple words - capitalize first letter of each
return words
.map(word => {
// If all uppercase (acronym) and first word, just capitalize first letter
if (word === word.toUpperCase() && word.length > 1) {
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
}
return word.charAt(0).toUpperCase() + word.slice(1);
})
.join('');
}
function mapJiraType(typeId: number): GeneratedAttribute['type'] {
return JIRA_TYPE_MAP[typeId] || 'unknown';
}
function determineSyncPriority(typeName: string, objectCount: number): number {
// Application Component and related main types first
if (PRIORITY_TYPE_NAMES.has(typeName)) {
return 1;
}
// Reference data types last
for (const pattern of REFERENCE_TYPE_PATTERNS) {
if (pattern.test(typeName)) {
return 10;
}
}
// Medium priority for types with more objects
if (objectCount > 100) {
return 2;
}
if (objectCount > 10) {
return 5;
}
return 8;
}
function parseAttribute(
attr: JiraAttribute,
allTypeConfigs: Map<number, { name: string; typeName: string }>
): GeneratedAttribute {
const typeId = attr.type || attr.defaultType?.id || 0;
let type = mapJiraType(typeId);
const isMultiple = (attr.maximumCardinality ?? 1) > 1 || attr.maximumCardinality === -1;
const isEditable = attr.editable !== false && !attr.hidden;
const isRequired = (attr.minimumCardinality ?? 0) > 0;
const isSystem = attr.system === true;
// CRITICAL: Jira sometimes returns type=1 (integer) for reference attributes!
// The presence of referenceObjectTypeId is the true indicator of a reference type.
const refTypeId = attr.referenceObjectTypeId || attr.referenceObjectType?.id || attr.referenceType?.id;
if (refTypeId) {
type = 'reference';
}
const result: GeneratedAttribute = {
jiraId: attr.id,
name: attr.name,
fieldName: toCamelCase(attr.name),
type,
isMultiple,
isEditable,
isRequired,
isSystem,
description: attr.description,
};
// Handle reference types - add reference metadata
if (type === 'reference' && refTypeId) {
result.referenceTypeId = refTypeId;
const refConfig = allTypeConfigs.get(refTypeId);
result.referenceTypeName = refConfig?.typeName ||
attr.referenceObjectType?.name ||
attr.referenceType?.name ||
`Type${refTypeId}`;
}
return result;
}
// =============================================================================
// Code Generation Functions
// =============================================================================
function generateTypeScriptType(attrType: GeneratedAttribute['type'], isMultiple: boolean, isReference: boolean): string {
let tsType: string;
if (isReference) {
tsType = 'ObjectReference';
} else {
switch (attrType) {
case 'text':
case 'textarea':
case 'url':
case 'email':
case 'select':
case 'user':
case 'status':
tsType = 'string';
break;
case 'integer':
case 'float':
tsType = 'number';
break;
case 'boolean':
tsType = 'boolean';
break;
case 'date':
case 'datetime':
tsType = 'string'; // ISO date string
break;
default:
tsType = 'unknown';
}
}
if (isMultiple) {
return `${tsType}[]`;
}
return `${tsType} | null`;
}
function generateTypesFile(objectTypes: GeneratedObjectType[], generatedAt: Date): string {
const lines: string[] = [
'// AUTO-GENERATED FILE - DO NOT EDIT MANUALLY',
'// Generated from Jira Assets Schema via REST API',
`// Generated at: ${generatedAt.toISOString()}`,
'//',
'// Re-generate with: npm run generate-schema',
'',
'// =============================================================================',
'// Base Types',
'// =============================================================================',
'',
'/** Reference to another CMDB object */',
'export interface ObjectReference {',
' objectId: string;',
' objectKey: string;',
' label: string;',
' // Optional enriched data from referenced object',
' factor?: number;',
'}',
'',
'/** Base interface for all CMDB objects */',
'export interface BaseCMDBObject {',
' id: string;',
' objectKey: string;',
' label: string;',
' _objectType: string;',
' _jiraUpdatedAt: string;',
' _jiraCreatedAt: string;',
'}',
'',
'// =============================================================================',
'// Object Type Interfaces',
'// =============================================================================',
'',
];
for (const objType of objectTypes) {
lines.push(`/** ${objType.name} (Jira Type ID: ${objType.jiraTypeId}, ${objType.objectCount} objects) */`);
lines.push(`export interface ${objType.typeName} extends BaseCMDBObject {`);
lines.push(` _objectType: '${objType.typeName}';`);
lines.push('');
// Group attributes by type
const scalarAttrs = objType.attributes.filter(a => a.type !== 'reference');
const refAttrs = objType.attributes.filter(a => a.type === 'reference');
if (scalarAttrs.length > 0) {
lines.push(' // Scalar attributes');
for (const attr of scalarAttrs) {
const tsType = generateTypeScriptType(attr.type, attr.isMultiple, false);
const comment = attr.description ? ` // ${attr.description}` : '';
lines.push(` ${attr.fieldName}: ${tsType};${comment}`);
}
lines.push('');
}
if (refAttrs.length > 0) {
lines.push(' // Reference attributes');
for (const attr of refAttrs) {
const tsType = generateTypeScriptType(attr.type, attr.isMultiple, true);
const comment = attr.referenceTypeName ? ` // -> ${attr.referenceTypeName}` : '';
lines.push(` ${attr.fieldName}: ${tsType};${comment}`);
}
lines.push('');
}
lines.push('}');
lines.push('');
}
// Generate union type
lines.push('// =============================================================================');
lines.push('// Union Types');
lines.push('// =============================================================================');
lines.push('');
lines.push('/** Union of all CMDB object types */');
lines.push('export type CMDBObject =');
for (let i = 0; i < objectTypes.length; i++) {
const suffix = i < objectTypes.length - 1 ? '' : ';';
lines.push(` | ${objectTypes[i].typeName}${suffix}`);
}
lines.push('');
// Generate type name literal union
lines.push('/** All valid object type names */');
lines.push('export type CMDBObjectTypeName =');
for (let i = 0; i < objectTypes.length; i++) {
const suffix = i < objectTypes.length - 1 ? '' : ';';
lines.push(` | '${objectTypes[i].typeName}'${suffix}`);
}
lines.push('');
// Generate type guards
lines.push('// =============================================================================');
lines.push('// Type Guards');
lines.push('// =============================================================================');
lines.push('');
for (const objType of objectTypes) {
lines.push(`export function is${objType.typeName}(obj: CMDBObject): obj is ${objType.typeName} {`);
lines.push(` return obj._objectType === '${objType.typeName}';`);
lines.push('}');
lines.push('');
}
return lines.join('\n');
}
function generateSchemaFile(objectTypes: GeneratedObjectType[], generatedAt: Date): string {
const lines: string[] = [
'// AUTO-GENERATED FILE - DO NOT EDIT MANUALLY',
'// Generated from Jira Assets Schema via REST API',
`// Generated at: ${generatedAt.toISOString()}`,
'//',
'// Re-generate with: npm run generate-schema',
'',
'// =============================================================================',
'// Schema Type Definitions',
'// =============================================================================',
'',
'export interface AttributeDefinition {',
' jiraId: number;',
' name: string;',
' fieldName: string;',
" type: 'text' | 'integer' | 'float' | 'boolean' | 'date' | 'datetime' | 'select' | 'reference' | 'url' | 'email' | 'textarea' | 'user' | 'status' | 'unknown';",
' isMultiple: boolean;',
' isEditable: boolean;',
' isRequired: boolean;',
' isSystem: boolean;',
' referenceTypeId?: number;',
' referenceTypeName?: string;',
' description?: string;',
'}',
'',
'export interface ObjectTypeDefinition {',
' jiraTypeId: number;',
' name: string;',
' typeName: string;',
' syncPriority: number;',
' objectCount: number;',
' attributes: AttributeDefinition[];',
'}',
'',
'// =============================================================================',
'// Schema Metadata',
'// =============================================================================',
'',
`export const SCHEMA_GENERATED_AT = '${generatedAt.toISOString()}';`,
`export const SCHEMA_OBJECT_TYPE_COUNT = ${objectTypes.length};`,
`export const SCHEMA_TOTAL_ATTRIBUTES = ${objectTypes.reduce((sum, ot) => sum + ot.attributes.length, 0)};`,
'',
'// =============================================================================',
'// Object Type Definitions',
'// =============================================================================',
'',
'export const OBJECT_TYPES: Record<string, ObjectTypeDefinition> = {',
];
for (let i = 0; i < objectTypes.length; i++) {
const objType = objectTypes[i];
const comma = i < objectTypes.length - 1 ? ',' : '';
lines.push(` '${objType.typeName}': {`);
lines.push(` jiraTypeId: ${objType.jiraTypeId},`);
lines.push(` name: '${objType.name.replace(/'/g, "\\'")}',`);
lines.push(` typeName: '${objType.typeName}',`);
lines.push(` syncPriority: ${objType.syncPriority},`);
lines.push(` objectCount: ${objType.objectCount},`);
lines.push(' attributes: [');
for (let j = 0; j < objType.attributes.length; j++) {
const attr = objType.attributes[j];
const attrComma = j < objType.attributes.length - 1 ? ',' : '';
let attrLine = ` { jiraId: ${attr.jiraId}, name: '${attr.name.replace(/'/g, "\\'")}', fieldName: '${attr.fieldName}', type: '${attr.type}', isMultiple: ${attr.isMultiple}, isEditable: ${attr.isEditable}, isRequired: ${attr.isRequired}, isSystem: ${attr.isSystem}`;
if (attr.referenceTypeId) {
attrLine += `, referenceTypeId: ${attr.referenceTypeId}, referenceTypeName: '${attr.referenceTypeName}'`;
}
if (attr.description) {
attrLine += `, description: '${attr.description.replace(/'/g, "\\'").replace(/\n/g, ' ')}'`;
}
attrLine += ` }${attrComma}`;
lines.push(attrLine);
}
lines.push(' ],');
lines.push(` }${comma}`);
}
lines.push('};');
lines.push('');
// Generate lookup maps
lines.push('// =============================================================================');
lines.push('// Lookup Maps');
lines.push('// =============================================================================');
lines.push('');
// Type ID to name map
lines.push('/** Map from Jira Type ID to TypeScript type name */');
lines.push('export const TYPE_ID_TO_NAME: Record<number, string> = {');
for (const objType of objectTypes) {
lines.push(` ${objType.jiraTypeId}: '${objType.typeName}',`);
}
lines.push('};');
lines.push('');
// Type name to ID map
lines.push('/** Map from TypeScript type name to Jira Type ID */');
lines.push('export const TYPE_NAME_TO_ID: Record<string, number> = {');
for (const objType of objectTypes) {
lines.push(` '${objType.typeName}': ${objType.jiraTypeId},`);
}
lines.push('};');
lines.push('');
// Jira name to TypeScript name map
lines.push('/** Map from Jira object type name to TypeScript type name */');
lines.push('export const JIRA_NAME_TO_TYPE: Record<string, string> = {');
for (const objType of objectTypes) {
lines.push(` '${objType.name.replace(/'/g, "\\'")}': '${objType.typeName}',`);
}
lines.push('};');
lines.push('');
// Helper functions
lines.push('// =============================================================================');
lines.push('// Helper Functions');
lines.push('// =============================================================================');
lines.push('');
lines.push('/** Get attribute definition by type and field name */');
lines.push('export function getAttributeDefinition(typeName: string, fieldName: string): AttributeDefinition | undefined {');
lines.push(' const objectType = OBJECT_TYPES[typeName];');
lines.push(' if (!objectType) return undefined;');
lines.push(' return objectType.attributes.find(a => a.fieldName === fieldName);');
lines.push('}');
lines.push('');
lines.push('/** Get attribute definition by type and Jira attribute ID */');
lines.push('export function getAttributeById(typeName: string, jiraId: number): AttributeDefinition | undefined {');
lines.push(' const objectType = OBJECT_TYPES[typeName];');
lines.push(' if (!objectType) return undefined;');
lines.push(' return objectType.attributes.find(a => a.jiraId === jiraId);');
lines.push('}');
lines.push('');
lines.push('/** Get attribute definition by type and Jira attribute name */');
lines.push('export function getAttributeByName(typeName: string, attrName: string): AttributeDefinition | undefined {');
lines.push(' const objectType = OBJECT_TYPES[typeName];');
lines.push(' if (!objectType) return undefined;');
lines.push(' return objectType.attributes.find(a => a.name === attrName);');
lines.push('}');
lines.push('');
lines.push('/** Get attribute Jira ID by type and attribute name - throws if not found */');
lines.push('export function getAttributeId(typeName: string, attrName: string): number {');
lines.push(' const attr = getAttributeByName(typeName, attrName);');
lines.push(' if (!attr) {');
lines.push(' throw new Error(`Attribute "${attrName}" not found on type "${typeName}"`);');
lines.push(' }');
lines.push(' return attr.jiraId;');
lines.push('}');
lines.push('');
lines.push('/** Get all reference attributes for a type */');
lines.push('export function getReferenceAttributes(typeName: string): AttributeDefinition[] {');
lines.push(' const objectType = OBJECT_TYPES[typeName];');
lines.push(' if (!objectType) return [];');
lines.push(" return objectType.attributes.filter(a => a.type === 'reference');");
lines.push('}');
lines.push('');
lines.push('/** Get all object types sorted by sync priority */');
lines.push('export function getObjectTypesBySyncPriority(): ObjectTypeDefinition[] {');
lines.push(' return Object.values(OBJECT_TYPES).sort((a, b) => a.syncPriority - b.syncPriority);');
lines.push('}');
lines.push('');
return lines.join('\n');
}
function generateDatabaseSchema(generatedAt: Date): string {
const lines: string[] = [
'-- AUTO-GENERATED FILE - DO NOT EDIT MANUALLY',
'-- Generated from Jira Assets Schema via REST API',
`-- Generated at: ${generatedAt.toISOString()}`,
'--',
'-- Re-generate with: npm run generate-schema',
'',
'-- =============================================================================',
'-- Core Tables',
'-- =============================================================================',
'',
'-- Cached CMDB objects (all types stored in single table with JSON data)',
'CREATE TABLE IF NOT EXISTS cached_objects (',
' id TEXT PRIMARY KEY,',
' object_key TEXT NOT NULL UNIQUE,',
' object_type TEXT NOT NULL,',
' label TEXT NOT NULL,',
' data JSON NOT NULL,',
' jira_updated_at TEXT,',
' jira_created_at TEXT,',
' cached_at TEXT NOT NULL',
');',
'',
'-- Object relations (references between objects)',
'CREATE TABLE IF NOT EXISTS object_relations (',
' id INTEGER PRIMARY KEY AUTOINCREMENT,',
' source_id TEXT NOT NULL,',
' target_id TEXT NOT NULL,',
' attribute_name TEXT NOT NULL,',
' source_type TEXT NOT NULL,',
' target_type TEXT NOT NULL,',
' UNIQUE(source_id, target_id, attribute_name)',
');',
'',
'-- Sync metadata (tracks sync state)',
'CREATE TABLE IF NOT EXISTS sync_metadata (',
' key TEXT PRIMARY KEY,',
' value TEXT NOT NULL,',
' updated_at TEXT NOT NULL',
');',
'',
'-- =============================================================================',
'-- Indices for Performance',
'-- =============================================================================',
'',
'CREATE INDEX IF NOT EXISTS idx_objects_type ON cached_objects(object_type);',
'CREATE INDEX IF NOT EXISTS idx_objects_key ON cached_objects(object_key);',
'CREATE INDEX IF NOT EXISTS idx_objects_updated ON cached_objects(jira_updated_at);',
'CREATE INDEX IF NOT EXISTS idx_objects_label ON cached_objects(label);',
'',
'CREATE INDEX IF NOT EXISTS idx_relations_source ON object_relations(source_id);',
'CREATE INDEX IF NOT EXISTS idx_relations_target ON object_relations(target_id);',
'CREATE INDEX IF NOT EXISTS idx_relations_source_type ON object_relations(source_type);',
'CREATE INDEX IF NOT EXISTS idx_relations_target_type ON object_relations(target_type);',
'CREATE INDEX IF NOT EXISTS idx_relations_attr ON object_relations(attribute_name);',
'',
];
return lines.join('\n');
}
// =============================================================================
// Main
// =============================================================================
async function main() {
const generatedAt = new Date();
console.log('');
console.log('╔════════════════════════════════════════════════════════════════╗');
console.log('║ CMDB Schema Generator - Jira Assets API ║');
console.log('╚════════════════════════════════════════════════════════════════╝');
console.log('');
// Validate configuration
if (!JIRA_HOST) {
console.error('❌ ERROR: JIRA_HOST environment variable is required');
console.error(' Set this in your .env file: JIRA_HOST=https://jira.your-domain.com');
process.exit(1);
}
if (!JIRA_PAT) {
console.error('❌ ERROR: JIRA_PAT environment variable is required');
console.error(' Set this in your .env file: JIRA_PAT=your-personal-access-token');
process.exit(1);
}
if (!JIRA_SCHEMA_ID) {
console.error('❌ ERROR: JIRA_SCHEMA_ID environment variable is required');
console.error(' Set this in your .env file: JIRA_SCHEMA_ID=6');
process.exit(1);
}
if (envLoaded) {
console.log(`🔧 Environment: ${envLoaded}`);
}
console.log(`📡 Jira Host: ${JIRA_HOST}`);
console.log(`📋 Schema ID: ${JIRA_SCHEMA_ID}`);
console.log(`📁 Output Dir: ${OUTPUT_DIR}`);
console.log('');
// Ensure output directory exists
if (!fs.existsSync(OUTPUT_DIR)) {
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
console.log(`📁 Created output directory: ${OUTPUT_DIR}`);
}
const fetcher = new JiraSchemaFetcher(JIRA_HOST, JIRA_PAT);
// Test connection
console.log('🔌 Testing connection to Jira Assets API...');
const connected = await fetcher.testConnection();
if (!connected) {
console.error('❌ Failed to connect to Jira Assets API');
console.error(' Please check your JIRA_HOST and JIRA_PAT settings');
process.exit(1);
}
console.log('✅ Connection successful');
console.log('');
// Fetch schema info
console.log('📋 Fetching schema information...');
const schema = await fetcher.fetchSchema(JIRA_SCHEMA_ID);
if (!schema) {
console.error(`❌ Failed to fetch schema ${JIRA_SCHEMA_ID}`);
process.exit(1);
}
console.log(` Schema: ${schema.name} (${schema.objectSchemaKey})`);
console.log(` Total objects: ${schema.objectCount || 'unknown'}`);
console.log('');
// Fetch ALL object types from the schema
console.log('📦 Fetching all object types from schema...');
const allObjectTypes = await fetcher.fetchAllObjectTypes(JIRA_SCHEMA_ID);
if (allObjectTypes.length === 0) {
console.error('❌ No object types found in schema');
process.exit(1);
}
console.log(` Found ${allObjectTypes.length} object types`);
console.log('');
// Build a map of all type IDs to names for reference resolution
const typeConfigs = new Map<number, { name: string; typeName: string }>();
for (const ot of allObjectTypes) {
typeConfigs.set(ot.id, {
name: ot.name,
typeName: toPascalCase(ot.name),
});
}
// Fetch attributes for each object type
console.log('🔍 Fetching attributes for each object type:');
console.log('');
const generatedTypes: GeneratedObjectType[] = [];
let totalAttributes = 0;
for (const objType of allObjectTypes) {
const typeName = toPascalCase(objType.name);
process.stdout.write(` ${objType.name.padEnd(50)} `);
const attributes = await fetcher.fetchAttributes(objType.id);
if (attributes.length === 0) {
console.log('⚠️ (no attributes)');
continue;
}
const parsedAttributes = attributes.map(attr => parseAttribute(attr, typeConfigs));
const syncPriority = determineSyncPriority(objType.name, objType.objectCount || 0);
generatedTypes.push({
jiraTypeId: objType.id,
name: objType.name,
typeName,
syncPriority,
objectCount: objType.objectCount || 0,
attributes: parsedAttributes,
});
totalAttributes += attributes.length;
console.log(`${attributes.length} attributes`);
}
console.log('');
console.log(`📊 Summary: ${generatedTypes.length} types, ${totalAttributes} attributes total`);
console.log('');
// Sort by sync priority
generatedTypes.sort((a, b) => a.syncPriority - b.syncPriority);
// Generate files
console.log('📝 Generating output files:');
console.log('');
// 1. TypeScript types
const typesContent = generateTypesFile(generatedTypes, generatedAt);
const typesPath = path.join(OUTPUT_DIR, 'jira-types.ts');
fs.writeFileSync(typesPath, typesContent, 'utf-8');
console.log(`${typesPath}`);
// 2. Schema metadata
const schemaContent = generateSchemaFile(generatedTypes, generatedAt);
const schemaPath = path.join(OUTPUT_DIR, 'jira-schema.ts');
fs.writeFileSync(schemaPath, schemaContent, 'utf-8');
console.log(`${schemaPath}`);
// 3. Database DDL
const dbContent = generateDatabaseSchema(generatedAt);
const dbPath = path.join(OUTPUT_DIR, 'db-schema.sql');
fs.writeFileSync(dbPath, dbContent, 'utf-8');
console.log(`${dbPath}`);
console.log('');
console.log('╔════════════════════════════════════════════════════════════════╗');
console.log('║ Schema generation complete! ║');
console.log('╚════════════════════════════════════════════════════════════════╝');
console.log('');
console.log(`Generated at: ${generatedAt.toISOString()}`);
console.log(`Object types: ${generatedTypes.length}`);
console.log(`Attributes: ${totalAttributes}`);
console.log('');
console.log('Next steps:');
console.log(' 1. Review the generated files in src/generated/');
console.log(' 2. Rebuild the application: npm run build');
console.log(' 3. Restart the server to pick up the new schema');
console.log('');
}
// Run
main().catch(error => {
console.error('');
console.error('❌ Schema generation failed:', error);
console.error('');
process.exit(1);
});