#!/usr/bin/env npx tsx /** * Type Generation Script - Database to TypeScript * * Generates TypeScript types from database schema. * This script reads the schema from the database (object_types, attributes) * and generates: * - TypeScript types (jira-types.ts) * - Schema metadata (jira-schema.ts) * * Usage: npm run generate-types */ import * as fs from 'fs'; import * as path from 'path'; import { fileURLToPath } from 'url'; import { createDatabaseAdapter } from '../src/services/database/factory.js'; import type { AttributeDefinition } from '../src/generated/jira-schema.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const OUTPUT_DIR = path.resolve(__dirname, '../src/generated'); interface DatabaseObjectType { jira_type_id: number; type_name: string; display_name: string; description: string | null; sync_priority: number; object_count: number; } interface DatabaseAttribute { jira_attr_id: number; object_type_name: string; attr_name: string; field_name: string; attr_type: string; is_multiple: boolean | number; is_editable: boolean | number; is_required: boolean | number; is_system: boolean | number; reference_type_name: string | null; description: string | null; } function generateTypeScriptType(attrType: string, 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 escapeString(str: string): string { return str.replace(/'/g, "\\'").replace(/\n/g, ' '); } function generateTypesFile(objectTypes: Array<{ jiraTypeId: number; name: string; typeName: string; objectCount: number; attributes: AttributeDefinition[]; }>, generatedAt: Date): string { const lines: string[] = [ '// AUTO-GENERATED FILE - DO NOT EDIT MANUALLY', '// Generated from database schema', `// Generated at: ${generatedAt.toISOString()}`, '//', '// Re-generate with: npm run generate-types', '', '// =============================================================================', '// 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: Array<{ jiraTypeId: number; name: string; typeName: string; syncPriority: number; objectCount: number; attributes: AttributeDefinition[]; }>, generatedAt: Date): string { const lines: string[] = [ '// AUTO-GENERATED FILE - DO NOT EDIT MANUALLY', '// Generated from database schema', `// Generated at: ${generatedAt.toISOString()}`, '//', '// Re-generate with: npm run generate-types', '', '// =============================================================================', '// 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: '${escapeString(objType.name)}',`); 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: '${escapeString(attr.name)}', fieldName: '${attr.fieldName}', type: '${attr.type}', isMultiple: ${attr.isMultiple}, isEditable: ${attr.isEditable}, isRequired: ${attr.isRequired}, isSystem: ${attr.isSystem}`; if (attr.referenceTypeName) { attrLine += `, referenceTypeName: '${attr.referenceTypeName}'`; } if (attr.description) { attrLine += `, description: '${escapeString(attr.description)}'`; } 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(` '${escapeString(objType.name)}': '${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'); } async function main() { const generatedAt = new Date(); console.log(''); console.log('╔════════════════════════════════════════════════════════════════╗'); console.log('║ Type Generation - Database to TypeScript ║'); console.log('╚════════════════════════════════════════════════════════════════╝'); console.log(''); try { // Connect to database const db = createDatabaseAdapter(); console.log('✓ Connected to database'); // Ensure schema is discovered first const { schemaDiscoveryService } = await import('../src/services/schemaDiscoveryService.js'); await schemaDiscoveryService.discoverAndStoreSchema(); console.log('✓ Schema discovered from database'); // Fetch object types const objectTypeRows = await db.query(` SELECT * FROM object_types ORDER BY sync_priority, type_name `); console.log(`✓ Fetched ${objectTypeRows.length} object types`); // Fetch attributes const attributeRows = await db.query(` SELECT * FROM attributes ORDER BY object_type_name, jira_attr_id `); console.log(`✓ Fetched ${attributeRows.length} attributes`); // Build object types with attributes const objectTypes = objectTypeRows.map(typeRow => { const attributes = attributeRows .filter(a => a.object_type_name === typeRow.type_name) .map(attrRow => { // Convert boolean/number for SQLite compatibility const isMultiple = typeof attrRow.is_multiple === 'boolean' ? attrRow.is_multiple : attrRow.is_multiple === 1; const isEditable = typeof attrRow.is_editable === 'boolean' ? attrRow.is_editable : attrRow.is_editable === 1; const isRequired = typeof attrRow.is_required === 'boolean' ? attrRow.is_required : attrRow.is_required === 1; const isSystem = typeof attrRow.is_system === 'boolean' ? attrRow.is_system : attrRow.is_system === 1; return { jiraId: attrRow.jira_attr_id, name: attrRow.attr_name, fieldName: attrRow.field_name, type: attrRow.attr_type as AttributeDefinition['type'], isMultiple, isEditable, isRequired, isSystem, referenceTypeName: attrRow.reference_type_name || undefined, description: attrRow.description || undefined, } as AttributeDefinition; }); return { jiraTypeId: typeRow.jira_type_id, name: typeRow.display_name, typeName: typeRow.type_name, syncPriority: typeRow.sync_priority, objectCount: typeRow.object_count, attributes, }; }); // Ensure output directory exists if (!fs.existsSync(OUTPUT_DIR)) { fs.mkdirSync(OUTPUT_DIR, { recursive: true }); } // Generate TypeScript types file const typesContent = generateTypesFile(objectTypes, generatedAt); const typesPath = path.join(OUTPUT_DIR, 'jira-types.ts'); fs.writeFileSync(typesPath, typesContent, 'utf-8'); console.log(`✓ Generated ${typesPath}`); // Generate schema file const schemaContent = generateSchemaFile(objectTypes, generatedAt); const schemaPath = path.join(OUTPUT_DIR, 'jira-schema.ts'); fs.writeFileSync(schemaPath, schemaContent, 'utf-8'); console.log(`✓ Generated ${schemaPath}`); console.log(''); console.log('✅ Type generation completed successfully!'); console.log(` Generated ${objectTypes.length} object types with ${objectTypes.reduce((sum, ot) => sum + ot.attributes.length, 0)} attributes`); console.log(''); } catch (error) { console.error(''); console.error('❌ Type generation failed:', error); process.exit(1); } } main();