/** * Schema Configuration Service * * Manages schema and object type configuration for syncing. * Discovers schemas and object types from Jira Assets API and allows * enabling/disabling specific object types for synchronization. */ import { logger } from './logger.js'; import { normalizedCacheStore } from './normalizedCacheStore.js'; import { config } from '../config/env.js'; import { toPascalCase } from './schemaUtils.js'; import type { DatabaseAdapter } from './database/interface.js'; export interface JiraSchema { id: number; name: string; description?: string; objectTypeCount?: number; } export interface JiraObjectType { id: number; name: string; description?: string; objectCount?: number; objectSchemaId: number; parentObjectTypeId?: number; inherited?: boolean; abstractObjectType?: boolean; } export interface ConfiguredObjectType { id: string; // "schemaId:objectTypeId" schemaId: string; schemaName: string; objectTypeId: number; objectTypeName: string; displayName: string; description: string | null; objectCount: number; enabled: boolean; discoveredAt: string; updatedAt: string; } class SchemaConfigurationService { constructor() { // Configuration service - no API calls needed, uses database only } /** * NOTE: Schema discovery is now handled by SchemaSyncService. * This service only manages configuration (enabling/disabling object types). * Use schemaSyncService.syncAll() to discover and sync schemas, object types, and attributes. */ /** * Get all configured object types grouped by schema */ async getConfiguredObjectTypes(): Promise> { const db: DatabaseAdapter = (normalizedCacheStore as any).db; if (!db) { throw new Error('Database not available'); } await (db as any).ensureInitialized?.(); const rows = await db.query<{ id: number; schema_id: number; jira_schema_id: string; schema_name: string; jira_type_id: number; type_name: string; display_name: string; description: string | null; object_count: number; enabled: boolean | number; discovered_at: string; updated_at: string; }>(` SELECT ot.id, ot.schema_id, s.jira_schema_id, s.name as schema_name, ot.jira_type_id, ot.type_name, ot.display_name, ot.description, ot.object_count, ot.enabled, ot.discovered_at, ot.updated_at FROM object_types ot JOIN schemas s ON ot.schema_id = s.id ORDER BY s.name ASC, ot.display_name ASC `); // Group by schema const schemaMap = new Map(); for (const row of rows) { const objectType: ConfiguredObjectType = { id: `${row.jira_schema_id}:${row.jira_type_id}`, // Keep same format for compatibility schemaId: row.jira_schema_id, schemaName: row.schema_name, objectTypeId: row.jira_type_id, objectTypeName: row.type_name, displayName: row.display_name, description: row.description, objectCount: row.object_count, enabled: typeof row.enabled === 'boolean' ? row.enabled : row.enabled === 1, discoveredAt: row.discovered_at, updatedAt: row.updated_at, }; if (!schemaMap.has(row.jira_schema_id)) { schemaMap.set(row.jira_schema_id, []); } schemaMap.get(row.jira_schema_id)!.push(objectType); } // Convert to array return Array.from(schemaMap.entries()).map(([schemaId, objectTypes]) => { const firstType = objectTypes[0]; return { schemaId, schemaName: firstType.schemaName, objectTypes, }; }); } /** * Set enabled status for an object type * id format: "schemaId:objectTypeId" (e.g., "6:123") */ async setObjectTypeEnabled(id: string, enabled: boolean): Promise { const db: DatabaseAdapter = (normalizedCacheStore as any).db; if (!db) { throw new Error('Database not available'); } await (db as any).ensureInitialized?.(); // Parse id: "schemaId:objectTypeId" const [schemaIdStr, objectTypeIdStr] = id.split(':'); if (!schemaIdStr || !objectTypeIdStr) { throw new Error(`Invalid object type id format: ${id}. Expected format: "schemaId:objectTypeId"`); } const objectTypeId = parseInt(objectTypeIdStr, 10); if (isNaN(objectTypeId)) { throw new Error(`Invalid object type id: ${objectTypeIdStr}`); } // Get schema_id (FK) from schemas table const schemaRow = await db.queryOne<{ id: number }>( `SELECT id FROM schemas WHERE jira_schema_id = ?`, [schemaIdStr] ); if (!schemaRow) { throw new Error(`Schema ${schemaIdStr} not found`); } // Check if type_name is missing and try to fix it if enabling const currentType = await db.queryOne<{ type_name: string | null; display_name: string }>( `SELECT type_name, display_name FROM object_types WHERE schema_id = ? AND jira_type_id = ?`, [schemaRow.id, objectTypeId] ); let typeNameToSet = currentType?.type_name; const needsTypeNameFix = enabled && (!typeNameToSet || typeNameToSet.trim() === ''); if (needsTypeNameFix && currentType?.display_name) { // Try to generate type_name from display_name (PascalCase) const { toPascalCase } = await import('./schemaUtils.js'); typeNameToSet = toPascalCase(currentType.display_name); logger.warn(`SchemaConfiguration: Type ${id} has missing type_name. Auto-generating "${typeNameToSet}" from display_name "${currentType.display_name}"`); } const now = new Date().toISOString(); if (db.isPostgres) { if (needsTypeNameFix && typeNameToSet) { await db.execute(` UPDATE object_types SET enabled = ?, type_name = ?, updated_at = ? WHERE schema_id = ? AND jira_type_id = ? `, [enabled, typeNameToSet, now, schemaRow.id, objectTypeId]); logger.info(`SchemaConfiguration: Set object type ${id} enabled=${enabled} and fixed missing type_name to "${typeNameToSet}"`); } else { await db.execute(` UPDATE object_types SET enabled = ?, updated_at = ? WHERE schema_id = ? AND jira_type_id = ? `, [enabled, now, schemaRow.id, objectTypeId]); logger.info(`SchemaConfiguration: Set object type ${id} enabled=${enabled}`); } } else { if (needsTypeNameFix && typeNameToSet) { await db.execute(` UPDATE object_types SET enabled = ?, type_name = ?, updated_at = ? WHERE schema_id = ? AND jira_type_id = ? `, [enabled ? 1 : 0, typeNameToSet, now, schemaRow.id, objectTypeId]); logger.info(`SchemaConfiguration: Set object type ${id} enabled=${enabled} and fixed missing type_name to "${typeNameToSet}"`); } else { await db.execute(` UPDATE object_types SET enabled = ?, updated_at = ? WHERE schema_id = ? AND jira_type_id = ? `, [enabled ? 1 : 0, now, schemaRow.id, objectTypeId]); logger.info(`SchemaConfiguration: Set object type ${id} enabled=${enabled}`); } } } /** * Bulk update enabled status for multiple object types */ async bulkSetObjectTypesEnabled(updates: Array<{ id: string; enabled: boolean }>): Promise { const db: DatabaseAdapter = (normalizedCacheStore as any).db; if (!db) { throw new Error('Database not available'); } await (db as any).ensureInitialized?.(); const now = new Date().toISOString(); await db.transaction(async (txDb: DatabaseAdapter) => { for (const update of updates) { // Parse id: "schemaId:objectTypeId" const [schemaIdStr, objectTypeIdStr] = update.id.split(':'); if (!schemaIdStr || !objectTypeIdStr) { logger.warn(`SchemaConfiguration: Invalid object type id format: ${update.id}`); continue; } const objectTypeId = parseInt(objectTypeIdStr, 10); if (isNaN(objectTypeId)) { logger.warn(`SchemaConfiguration: Invalid object type id: ${objectTypeIdStr}`); continue; } // Get schema_id (FK) from schemas table const schemaRow = await txDb.queryOne<{ id: number }>( `SELECT id FROM schemas WHERE jira_schema_id = ?`, [schemaIdStr] ); if (!schemaRow) { logger.warn(`SchemaConfiguration: Schema ${schemaIdStr} not found`); continue; } // Check if type_name is missing and try to fix it if enabling const currentType = await txDb.queryOne<{ type_name: string | null; display_name: string }>( `SELECT type_name, display_name FROM object_types WHERE schema_id = ? AND jira_type_id = ?`, [schemaRow.id, objectTypeId] ); let typeNameToSet = currentType?.type_name; const needsTypeNameFix = update.enabled && (!typeNameToSet || typeNameToSet.trim() === ''); if (needsTypeNameFix && currentType?.display_name) { // Try to generate type_name from display_name (PascalCase) const { toPascalCase } = await import('./schemaUtils.js'); typeNameToSet = toPascalCase(currentType.display_name); logger.warn(`SchemaConfiguration: Type ${update.id} has missing type_name. Auto-generating "${typeNameToSet}" from display_name "${currentType.display_name}"`); } if (txDb.isPostgres) { if (needsTypeNameFix && typeNameToSet) { await txDb.execute(` UPDATE object_types SET enabled = ?, type_name = ?, updated_at = ? WHERE schema_id = ? AND jira_type_id = ? `, [update.enabled, typeNameToSet, now, schemaRow.id, objectTypeId]); } else { await txDb.execute(` UPDATE object_types SET enabled = ?, updated_at = ? WHERE schema_id = ? AND jira_type_id = ? `, [update.enabled, now, schemaRow.id, objectTypeId]); } } else { if (needsTypeNameFix && typeNameToSet) { await txDb.execute(` UPDATE object_types SET enabled = ?, type_name = ?, updated_at = ? WHERE schema_id = ? AND jira_type_id = ? `, [update.enabled ? 1 : 0, typeNameToSet, now, schemaRow.id, objectTypeId]); } else { await txDb.execute(` UPDATE object_types SET enabled = ?, updated_at = ? WHERE schema_id = ? AND jira_type_id = ? `, [update.enabled ? 1 : 0, now, schemaRow.id, objectTypeId]); } } } }); logger.info(`SchemaConfiguration: Bulk updated ${updates.length} object types`); } /** * Get enabled object types (for sync engine) */ async getEnabledObjectTypes(): Promise> { const db: DatabaseAdapter = (normalizedCacheStore as any).db; if (!db) { throw new Error('Database not available'); } await (db as any).ensureInitialized?.(); // Use parameterized query to avoid boolean/integer comparison issues const rows = await db.query<{ jira_schema_id: string; jira_type_id: number; type_name: string; display_name: string; }>( `SELECT s.jira_schema_id, ot.jira_type_id, ot.type_name, ot.display_name FROM object_types ot JOIN schemas s ON ot.schema_id = s.id WHERE ot.enabled = ?`, [db.isPostgres ? true : 1] ); return rows.map((row: { jira_schema_id: string; jira_type_id: number; type_name: string; display_name: string; }) => ({ schemaId: row.jira_schema_id, objectTypeId: row.jira_type_id, objectTypeName: row.type_name, displayName: row.display_name, })); } /** * Check if configuration is complete (at least one object type enabled) */ async isConfigurationComplete(): Promise { const enabledTypes = await this.getEnabledObjectTypes(); return enabledTypes.length > 0; } /** * Get configuration statistics */ async getConfigurationStats(): Promise<{ totalSchemas: number; totalObjectTypes: number; enabledObjectTypes: number; disabledObjectTypes: number; isConfigured: boolean; }> { const db: DatabaseAdapter = (normalizedCacheStore as any).db; if (!db) { throw new Error('Database not available'); } await (db as any).ensureInitialized?.(); const totalRow = await db.queryOne<{ count: number }>(` SELECT COUNT(*) as count FROM object_types `); // Use parameterized query to avoid boolean/integer comparison issues const enabledRow = await db.queryOne<{ count: number }>( `SELECT COUNT(*) as count FROM object_types WHERE enabled = ?`, [db.isPostgres ? true : 1] ); const schemaRow = await db.queryOne<{ count: number }>(` SELECT COUNT(*) as count FROM schemas `); const total = totalRow?.count || 0; const enabled = enabledRow?.count || 0; const schemas = schemaRow?.count || 0; return { totalSchemas: schemas, totalObjectTypes: total, enabledObjectTypes: enabled, disabledObjectTypes: total - enabled, isConfigured: enabled > 0, }; } /** * Get all schemas with their search enabled status */ async getSchemas(): Promise> { const db: DatabaseAdapter = (normalizedCacheStore as any).db; if (!db) { throw new Error('Database not available'); } await (db as any).ensureInitialized?.(); const rows = await db.query<{ jira_schema_id: string; name: string; search_enabled: boolean | number; }>(` SELECT jira_schema_id, name, search_enabled FROM schemas ORDER BY name ASC `); return rows.map((row: { jira_schema_id: string; name: string; search_enabled: boolean | number; }) => ({ schemaId: row.jira_schema_id, schemaName: row.name, searchEnabled: typeof row.search_enabled === 'boolean' ? row.search_enabled : row.search_enabled === 1, })); } /** * Set search enabled status for a schema */ async setSchemaSearchEnabled(schemaId: string, searchEnabled: boolean): Promise { const db: DatabaseAdapter = (normalizedCacheStore as any).db; if (!db) { throw new Error('Database not available'); } await (db as any).ensureInitialized?.(); const now = new Date().toISOString(); if (db.isPostgres) { await db.execute(` UPDATE schemas SET search_enabled = ?, updated_at = ? WHERE jira_schema_id = ? `, [searchEnabled, now, schemaId]); } else { await db.execute(` UPDATE schemas SET search_enabled = ?, updated_at = ? WHERE jira_schema_id = ? `, [searchEnabled ? 1 : 0, now, schemaId]); } logger.info(`SchemaConfiguration: Set schema ${schemaId} search_enabled=${searchEnabled}`); } } export const schemaConfigurationService = new SchemaConfigurationService();