UI styling improvements: dashboard headers and navigation
- 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
This commit is contained in:
38
backend/scripts/discover-schema.ts
Normal file
38
backend/scripts/discover-schema.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
|
||||
/**
|
||||
* Schema Discovery CLI
|
||||
*
|
||||
* Manually trigger schema discovery from Jira Assets API.
|
||||
* This script fetches the schema and stores it in the database.
|
||||
*
|
||||
* Usage: npm run discover-schema
|
||||
*/
|
||||
|
||||
import { schemaDiscoveryService } from '../src/services/schemaDiscoveryService.js';
|
||||
import { schemaCacheService } from '../src/services/schemaCacheService.js';
|
||||
import { logger } from '../src/services/logger.js';
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
console.log('Starting schema discovery...');
|
||||
logger.info('Schema Discovery CLI: Starting manual schema discovery');
|
||||
|
||||
// Force discovery (ignore cache)
|
||||
await schemaDiscoveryService.discoverAndStoreSchema(true);
|
||||
|
||||
// Invalidate cache so next request gets fresh data
|
||||
schemaCacheService.invalidate();
|
||||
|
||||
console.log('✅ Schema discovery completed successfully!');
|
||||
logger.info('Schema Discovery CLI: Schema discovery completed successfully');
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('❌ Schema discovery failed:', error);
|
||||
logger.error('Schema Discovery CLI: Schema discovery failed', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -752,18 +752,12 @@ function generateDatabaseSchema(generatedAt: Date): string {
|
||||
'-- =============================================================================',
|
||||
'-- 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',
|
||||
');',
|
||||
'--',
|
||||
'-- 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 (',
|
||||
@@ -787,10 +781,6 @@ function generateDatabaseSchema(generatedAt: Date): string {
|
||||
'-- 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);',
|
||||
|
||||
484
backend/scripts/generate-types-from-db.ts
Normal file
484
backend/scripts/generate-types-from-db.ts
Normal file
@@ -0,0 +1,484 @@
|
||||
#!/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();
|
||||
90
backend/scripts/migrate-search-enabled.ts
Normal file
90
backend/scripts/migrate-search-enabled.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Migration script: Add search_enabled column to schemas table
|
||||
*
|
||||
* This script adds the search_enabled column to the schemas table if it doesn't exist.
|
||||
*
|
||||
* Usage:
|
||||
* npm run migrate:search-enabled
|
||||
* or
|
||||
* tsx scripts/migrate-search-enabled.ts
|
||||
*/
|
||||
|
||||
import { getDatabaseAdapter } from '../src/services/database/singleton.js';
|
||||
import { logger } from '../src/services/logger.js';
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
console.log('Starting migration: Adding search_enabled column to schemas table...');
|
||||
|
||||
const db = getDatabaseAdapter();
|
||||
await db.ensureInitialized?.();
|
||||
|
||||
const isPostgres = db.isPostgres === true;
|
||||
|
||||
// Check if column exists and add it if it doesn't
|
||||
if (isPostgres) {
|
||||
// PostgreSQL: Check if column exists
|
||||
const columnExists = await db.queryOne<{ exists: boolean }>(`
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'schemas' AND column_name = 'search_enabled'
|
||||
) as exists
|
||||
`);
|
||||
|
||||
if (!columnExists?.exists) {
|
||||
console.log('Adding search_enabled column to schemas table...');
|
||||
await db.execute(`
|
||||
ALTER TABLE schemas ADD COLUMN search_enabled BOOLEAN NOT NULL DEFAULT TRUE;
|
||||
`);
|
||||
console.log('✓ Column added successfully');
|
||||
} else {
|
||||
console.log('✓ Column already exists');
|
||||
}
|
||||
|
||||
// Create index if it doesn't exist
|
||||
try {
|
||||
await db.execute(`
|
||||
CREATE INDEX IF NOT EXISTS idx_schemas_search_enabled ON schemas(search_enabled);
|
||||
`);
|
||||
console.log('✓ Index created/verified');
|
||||
} catch (error) {
|
||||
console.log('Index may already exist, continuing...');
|
||||
}
|
||||
} else {
|
||||
// SQLite: Try to query the column to see if it exists
|
||||
try {
|
||||
await db.queryOne('SELECT search_enabled FROM schemas LIMIT 1');
|
||||
console.log('✓ Column already exists');
|
||||
} catch {
|
||||
// Column doesn't exist, add it
|
||||
console.log('Adding search_enabled column to schemas table...');
|
||||
await db.execute('ALTER TABLE schemas ADD COLUMN search_enabled INTEGER NOT NULL DEFAULT 1');
|
||||
console.log('✓ Column added successfully');
|
||||
}
|
||||
|
||||
// Create index if it doesn't exist
|
||||
try {
|
||||
await db.execute('CREATE INDEX IF NOT EXISTS idx_schemas_search_enabled ON schemas(search_enabled)');
|
||||
console.log('✓ Index created/verified');
|
||||
} catch (error) {
|
||||
console.log('Index may already exist, continuing...');
|
||||
}
|
||||
}
|
||||
|
||||
// Verify the column exists
|
||||
try {
|
||||
await db.queryOne('SELECT search_enabled FROM schemas LIMIT 1');
|
||||
console.log('✓ Migration completed successfully - search_enabled column verified');
|
||||
} catch (error) {
|
||||
console.error('✗ Migration verification failed:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('✗ Migration failed:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -66,7 +66,8 @@ async function migrateCacheDatabase(pg: Pool) {
|
||||
const sqlite = new Database(SQLITE_CACHE_DB, { readonly: true });
|
||||
|
||||
try {
|
||||
// Migrate cached_objects
|
||||
// Migrate cached_objects (LEGACY - only for migrating old data from deprecated schema)
|
||||
// Note: New databases use the normalized schema (objects + attribute_values tables)
|
||||
const objects = sqlite.prepare('SELECT * FROM cached_objects').all() as any[];
|
||||
console.log(` Migrating ${objects.length} cached objects...`);
|
||||
|
||||
|
||||
178
backend/scripts/setup-schema-mappings.ts
Normal file
178
backend/scripts/setup-schema-mappings.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
/**
|
||||
* Setup Schema Mappings Script
|
||||
*
|
||||
* Configures schema mappings for object types based on the provided configuration.
|
||||
* Run with: npm run setup-schema-mappings
|
||||
*/
|
||||
|
||||
import { schemaMappingService } from '../src/services/schemaMappingService.js';
|
||||
import { logger } from '../src/services/logger.js';
|
||||
import { JIRA_NAME_TO_TYPE } from '../src/generated/jira-schema.js';
|
||||
|
||||
// Configuration: Schema ID -> Array of object type display names
|
||||
const SCHEMA_MAPPINGS: Record<string, string[]> = {
|
||||
'8': ['User'],
|
||||
'6': [
|
||||
'Application Component',
|
||||
'Flows',
|
||||
'Server',
|
||||
'AzureSubscription',
|
||||
'Certificate',
|
||||
'Domain',
|
||||
'Package',
|
||||
'PackageBuild',
|
||||
'Privileged User',
|
||||
'Software',
|
||||
'SoftwarePatch',
|
||||
'Supplier',
|
||||
'Application Management - Subteam',
|
||||
'Application Management - Team',
|
||||
'Measures',
|
||||
'Rebootgroups',
|
||||
'Application Management - Hosting',
|
||||
'Application Management - Number of Users',
|
||||
'Application Management - TAM',
|
||||
'Application Management - Application Type',
|
||||
'Application Management - Complexity Factor',
|
||||
'Application Management - Dynamics Factor',
|
||||
'ApplicationFunction',
|
||||
'ApplicationFunctionCategory',
|
||||
'Business Impact Analyse',
|
||||
'Business Importance',
|
||||
'Certificate ClassificationType',
|
||||
'Certificate Type',
|
||||
'Hosting Type',
|
||||
'ICT Governance Model',
|
||||
'Organisation',
|
||||
],
|
||||
};
|
||||
|
||||
async function setupSchemaMappings() {
|
||||
logger.info('Setting up schema mappings...');
|
||||
|
||||
try {
|
||||
let totalMappings = 0;
|
||||
let skippedMappings = 0;
|
||||
let errors = 0;
|
||||
|
||||
for (const [schemaId, objectTypeNames] of Object.entries(SCHEMA_MAPPINGS)) {
|
||||
logger.info(`\nConfiguring schema ${schemaId} with ${objectTypeNames.length} object types...`);
|
||||
|
||||
for (const displayName of objectTypeNames) {
|
||||
try {
|
||||
// Convert display name to typeName
|
||||
let typeName: string;
|
||||
|
||||
if (displayName === 'User') {
|
||||
// User might not be in the generated schema, use 'User' directly
|
||||
typeName = 'User';
|
||||
|
||||
// First, ensure User exists in object_types table
|
||||
const { normalizedCacheStore } = await import('../src/services/normalizedCacheStore.js');
|
||||
const db = (normalizedCacheStore as any).db;
|
||||
await db.ensureInitialized?.();
|
||||
|
||||
// Check if User exists in object_types
|
||||
const existing = await db.queryOne<{ type_name: string }>(`
|
||||
SELECT type_name FROM object_types WHERE type_name = ?
|
||||
`, [typeName]);
|
||||
|
||||
if (!existing) {
|
||||
// Insert User into object_types (we'll use a placeholder jira_type_id)
|
||||
// The actual jira_type_id will be discovered during schema discovery
|
||||
logger.info(` ℹ️ Adding "User" to object_types table...`);
|
||||
try {
|
||||
await db.execute(`
|
||||
INSERT INTO object_types (jira_type_id, type_name, display_name, description, sync_priority, object_count, discovered_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(jira_type_id) DO NOTHING
|
||||
`, [
|
||||
999999, // Placeholder ID - will be updated during schema discovery
|
||||
'User',
|
||||
'User',
|
||||
'User object type from schema 8',
|
||||
0,
|
||||
0,
|
||||
new Date().toISOString(),
|
||||
new Date().toISOString()
|
||||
]);
|
||||
|
||||
// Also try with type_name as unique constraint
|
||||
await db.execute(`
|
||||
INSERT INTO object_types (jira_type_id, type_name, display_name, description, sync_priority, object_count, discovered_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(type_name) DO UPDATE SET
|
||||
display_name = excluded.display_name,
|
||||
updated_at = excluded.updated_at
|
||||
`, [
|
||||
999999,
|
||||
'User',
|
||||
'User',
|
||||
'User object type from schema 8',
|
||||
0,
|
||||
0,
|
||||
new Date().toISOString(),
|
||||
new Date().toISOString()
|
||||
]);
|
||||
logger.info(` ✓ Added "User" to object_types table`);
|
||||
} catch (error: any) {
|
||||
// If it already exists, that's fine
|
||||
if (error.message?.includes('UNIQUE constraint') || error.message?.includes('duplicate key')) {
|
||||
logger.info(` ℹ️ "User" already exists in object_types table`);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Look up typeName from JIRA_NAME_TO_TYPE mapping
|
||||
typeName = JIRA_NAME_TO_TYPE[displayName];
|
||||
|
||||
if (!typeName) {
|
||||
logger.warn(` ⚠️ Skipping "${displayName}" - typeName not found in schema`);
|
||||
skippedMappings++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Set the mapping
|
||||
await schemaMappingService.setMapping(typeName, schemaId, true);
|
||||
logger.info(` ✓ Mapped ${typeName} (${displayName}) -> Schema ${schemaId}`);
|
||||
totalMappings++;
|
||||
|
||||
} catch (error) {
|
||||
logger.error(` ✗ Failed to map "${displayName}" to schema ${schemaId}:`, error);
|
||||
errors++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`\n✅ Schema mappings setup complete!`);
|
||||
logger.info(` - Total mappings created: ${totalMappings}`);
|
||||
if (skippedMappings > 0) {
|
||||
logger.info(` - Skipped (not found in schema): ${skippedMappings}`);
|
||||
}
|
||||
if (errors > 0) {
|
||||
logger.info(` - Errors: ${errors}`);
|
||||
}
|
||||
|
||||
// Clear cache to ensure fresh lookups
|
||||
schemaMappingService.clearCache();
|
||||
logger.info(`\n💾 Cache cleared - mappings are now active`);
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Failed to setup schema mappings:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run the script
|
||||
setupSchemaMappings()
|
||||
.then(() => {
|
||||
logger.info('\n✨ Done!');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error('Script failed:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user