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:
2026-01-21 03:24:56 +01:00
parent e276e77fbc
commit cdee0e8819
138 changed files with 24551 additions and 3352 deletions

View 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();

View File

@@ -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);',

View 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();

View 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();

View File

@@ -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...`);

View 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);
});