#!/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. * * Schema Discovery: * - Automatically discovers available schemas via /objectschema/list * - Selects the schema with the most objects (or the first one if counts unavailable) * - The runtime application also discovers schemas dynamically * * 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 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 = { 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; 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 { 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 { 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 { 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 []; } } /** * List all available schemas */ async listSchemas(): Promise { try { const response = await fetch(`${this.baseUrl}/objectschema/list`, { headers: this.headers, }); if (!response.ok) { console.error(`Failed to list schemas: ${response.status} ${response.statusText}`); return []; } const result = await response.json(); // Handle both array and object responses if (Array.isArray(result)) { return result; } else if (result && typeof result === 'object' && 'objectschemas' in result) { return result.objectschemas || []; } return []; } catch (error) { console.error(`Error listing schemas:`, error); return []; } } /** * Test the connection */ async testConnection(): Promise { 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 ): 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 = {', ]; 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 = {'); 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 = {'); 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 = {'); 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', '-- =============================================================================', '--', '-- NOTE: This schema is LEGACY and deprecated.', '-- The current system uses the normalized schema defined in', '-- backend/src/services/database/normalized-schema.ts', '--', '-- This file is kept for reference and migration purposes only.', '', '-- 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_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 (envLoaded) { console.log(`🔧 Environment: ${envLoaded}`); } console.log(`📡 Jira Host: ${JIRA_HOST}`); 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(''); // Discover schema automatically console.log('📋 Discovering available schemas...'); const schemas = await fetcher.listSchemas(); if (schemas.length === 0) { console.error('❌ No schemas found'); console.error(' Please ensure Jira Assets is configured and accessible'); process.exit(1); } // Select the schema with the most objects (or the first one if counts unavailable) const schema = schemas.reduce((prev, current) => { const prevCount = prev.objectCount || 0; const currentCount = current.objectCount || 0; return currentCount > prevCount ? current : prev; }); const selectedSchemaId = schema.id.toString(); console.log(` Found ${schemas.length} schema(s)`); if (schemas.length > 1) { console.log(' Available schemas:'); schemas.forEach(s => { const marker = s.id === schema.id ? ' → ' : ' '; console.log(`${marker}${s.id}: ${s.name} (${s.objectSchemaKey}) - ${s.objectCount || 0} objects`); }); console.log(` Using schema: ${schema.name} (ID: ${selectedSchemaId})`); } 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(selectedSchemaId); 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(); 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); });