- Restore blue PageHeader on Dashboard (/app-components) - Update homepage (/) with subtle header design without blue bar - Add uniform PageHeader styling to application edit page - Fix Rapporten link on homepage to point to /reports overview - Improve header descriptions spacing for better readability
485 lines
18 KiB
TypeScript
485 lines
18 KiB
TypeScript
#!/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<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: '${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<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(` '${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<DatabaseObjectType>(`
|
|
SELECT * FROM object_types
|
|
ORDER BY sync_priority, type_name
|
|
`);
|
|
console.log(`✓ Fetched ${objectTypeRows.length} object types`);
|
|
|
|
// Fetch attributes
|
|
const attributeRows = await db.query<DatabaseAttribute>(`
|
|
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();
|