/** * SyncController - API handlers for sync operations */ import { Request, Response } from 'express'; import { logger } from '../../services/logger.js'; import { getServices } from '../../services/ServiceFactory.js'; export class SyncController { /** * Sync all schemas * POST /api/v2/sync/schemas */ async syncSchemas(req: Request, res: Response): Promise { try { const services = getServices(); const result = await services.schemaSyncService.syncAll(); res.json({ ...result, success: result.success !== undefined ? result.success : true, }); } catch (error) { logger.error('SyncController: Failed to sync schemas', error); res.status(500).json({ success: false, error: error instanceof Error ? error.message : 'Unknown error', }); } } /** * Sync all enabled object types * POST /api/v2/sync/objects */ async syncAllObjects(req: Request, res: Response): Promise { try { const services = getServices(); // Get enabled types const rawTypes = await services.schemaRepo.getEnabledObjectTypes(); if (rawTypes.length === 0) { res.status(400).json({ success: false, error: 'No object types enabled for syncing. Please configure object types in Schema Configuration.', }); return; } const results = []; let totalObjectsProcessed = 0; let totalObjectsCached = 0; let totalRelations = 0; // Sync each enabled type for (const type of rawTypes) { const result = await services.objectSyncService.syncObjectType( type.schemaId.toString(), type.id, type.typeName, type.displayName ); results.push({ typeName: type.typeName, displayName: type.displayName, ...result, }); totalObjectsProcessed += result.objectsProcessed; totalObjectsCached += result.objectsCached; totalRelations += result.relationsExtracted; } res.json({ success: true, stats: results, totalObjectsProcessed, totalObjectsCached, totalRelations, }); } catch (error) { logger.error('SyncController: Failed to sync objects', error); res.status(500).json({ success: false, error: error instanceof Error ? error.message : 'Unknown error', }); } } /** * Sync a specific object type * POST /api/v2/sync/objects/:typeName */ async syncObjectType(req: Request, res: Response): Promise { try { const typeName = Array.isArray(req.params.typeName) ? req.params.typeName[0] : req.params.typeName; if (!typeName) { res.status(400).json({ error: 'typeName parameter required' }); return; } const services = getServices(); // Get enabled types let rawTypes = await services.schemaRepo.getEnabledObjectTypes(); let enabledTypes = rawTypes.map(t => ({ typeName: t.typeName, displayName: t.displayName, schemaId: t.schemaId.toString(), objectTypeId: t.id, })); // Filter out any entries with missing typeName enabledTypes = enabledTypes.filter((t: { typeName?: string }) => t && t.typeName); // Debug logging - also check database directly logger.info(`SyncController: Looking for type "${typeName}" in ${enabledTypes.length} enabled types`); logger.debug(`SyncController: Enabled types: ${JSON.stringify(enabledTypes.map((t: { typeName?: string; displayName?: string }) => ({ typeName: t?.typeName, displayName: t?.displayName })))}`); // Additional debug: Check database directly for enabled types (including those with missing type_name) const db = services.schemaRepo.getDatabaseAdapter(); const isPostgres = db.isPostgres === true; const enabledCondition = isPostgres ? 'enabled IS true' : 'enabled = 1'; const dbCheck = await db.query<{ type_name: string | null; display_name: string; enabled: boolean | number; id: number; jira_type_id: number }>( `SELECT id, jira_type_id, type_name, display_name, enabled FROM object_types WHERE ${enabledCondition}` ); logger.info(`SyncController: Found ${dbCheck.length} enabled types in database (raw check)`); logger.debug(`SyncController: Database enabled types (raw): ${JSON.stringify(dbCheck.map(t => ({ id: t.id, displayName: t.display_name, typeName: t.type_name, hasTypeName: !!(t.type_name && t.type_name.trim() !== '') })))}`); // Check if AzureSubscription or similar is enabled but missing type_name const typeNameLower = typeName.toLowerCase(); const matchingByDisplayName = dbCheck.filter((t: { display_name: string }) => t.display_name.toLowerCase().includes(typeNameLower) || typeNameLower.includes(t.display_name.toLowerCase()) ); if (matchingByDisplayName.length > 0) { logger.warn(`SyncController: Found enabled type(s) matching "${typeName}" by display_name but not in enabled list:`, { matches: matchingByDisplayName.map(t => ({ id: t.id, displayName: t.display_name, typeName: t.type_name, hasTypeName: !!(t.type_name && t.type_name.trim() !== ''), enabled: t.enabled, })), }); } const type = enabledTypes.find((t: { typeName?: string }) => t && t.typeName === typeName); if (!type) { // Check if type exists but is not enabled or has missing type_name const allType = await services.schemaRepo.getObjectTypeByTypeName(typeName); if (allType) { // Debug: Check the actual enabled value and query const enabledValue = allType.enabled; const enabledType = typeof enabledValue; logger.warn(`SyncController: Type "${typeName}" found but not in enabled list. enabled=${enabledValue} (type: ${enabledType}), enabledTypes.length=${enabledTypes.length}`); logger.debug(`SyncController: Enabled types details: ${JSON.stringify(enabledTypes)}`); // Try to find it with different case (handle undefined typeName) const typeNameLower = typeName.toLowerCase(); const caseInsensitiveMatch = enabledTypes.find((t: { typeName?: string }) => t && t.typeName && t.typeName.toLowerCase() === typeNameLower); if (caseInsensitiveMatch) { logger.warn(`SyncController: Found type with different case: "${caseInsensitiveMatch.typeName}" vs "${typeName}"`); // Use the found type with correct case const result = await services.objectSyncService.syncObjectType( caseInsensitiveMatch.schemaId.toString(), caseInsensitiveMatch.objectTypeId, caseInsensitiveMatch.typeName, caseInsensitiveMatch.displayName ); res.json({ success: true, ...result, hasErrors: result.errors.length > 0, note: `Type name case corrected: "${typeName}" -> "${caseInsensitiveMatch.typeName}"`, }); return; } // Direct SQL query to verify enabled status and type_name const db = services.schemaRepo.getDatabaseAdapter(); const isPostgres = db.isPostgres === true; const rawCheck = await db.queryOne<{ enabled: boolean | number; type_name: string | null; display_name: string }>( `SELECT enabled, type_name, display_name FROM object_types WHERE type_name = ?`, [typeName] ); // Check if type is enabled but missing type_name in enabled list (might be filtered out) const enabledCondition = isPostgres ? 'enabled IS true' : 'enabled = 1'; const enabledWithMissingTypeName = await db.query<{ display_name: string; type_name: string | null; enabled: boolean | number }>( `SELECT display_name, type_name, enabled FROM object_types WHERE display_name ILIKE ? AND ${enabledCondition}`, [`%${typeName}%`] ); // Get list of all enabled type names for better error message const enabledTypeNames = enabledTypes.map((t: { typeName?: string }) => t.typeName).filter(Boolean) as string[]; // Check if the issue is that the type is enabled but has a missing type_name if (rawCheck && (rawCheck.enabled === true || rawCheck.enabled === 1)) { if (!rawCheck.type_name || rawCheck.type_name.trim() === '') { res.status(400).json({ success: false, error: `Object type "${typeName}" is enabled in the database but has a missing or empty type_name. This prevents it from being synced. Please run schema sync again to fix the type_name, or use the "Fix Missing Type Names" debug tool (Settings → Debug).`, details: { requestedType: typeName, displayName: rawCheck.display_name, enabledInDatabase: rawCheck.enabled, typeNameInDatabase: rawCheck.type_name, enabledTypesCount: enabledTypes.length, enabledTypeNames: enabledTypeNames, hint: 'Run schema sync to ensure all object types have a valid type_name, or use the Debug page to fix missing type names.', }, }); return; } } res.status(400).json({ success: false, error: `Object type "${typeName}" is not enabled for syncing. Currently enabled types: ${enabledTypeNames.length > 0 ? enabledTypeNames.join(', ') : 'none'}. Please enable "${typeName}" in Schema Configuration settings (Settings → Schema Configuratie).`, details: { requestedType: typeName, enabledInDatabase: rawCheck?.enabled, typeNameInDatabase: rawCheck?.type_name, enabledTypesCount: enabledTypes.length, enabledTypeNames: enabledTypeNames, hint: enabledTypeNames.length === 0 ? 'No object types are currently enabled. Please enable at least one object type in Schema Configuration.' : `You enabled: ${enabledTypeNames.join(', ')}. Please enable "${typeName}" if you want to sync it.`, }, }); } else { // Type not found by type_name - check by display_name (case-insensitive) const db = services.schemaRepo.getDatabaseAdapter(); const byDisplayName = await db.queryOne<{ enabled: boolean | number; type_name: string | null; display_name: string }>( `SELECT enabled, type_name, display_name FROM object_types WHERE display_name ILIKE ? LIMIT 1`, [`%${typeName}%`] ); if (byDisplayName && (byDisplayName.enabled === true || byDisplayName.enabled === 1)) { // Type is enabled but type_name might be missing or different res.status(400).json({ success: false, error: `Found enabled type "${byDisplayName.display_name}" but it has ${byDisplayName.type_name ? `type_name="${byDisplayName.type_name}"` : 'missing type_name'}. ${!byDisplayName.type_name ? 'Please run schema sync to fix the type_name, or use the "Fix Missing Type Names" debug tool.' : `Please use the correct type_name: "${byDisplayName.type_name}"`}`, details: { requestedType: typeName, foundDisplayName: byDisplayName.display_name, foundTypeName: byDisplayName.type_name, enabledInDatabase: byDisplayName.enabled, hint: !byDisplayName.type_name ? 'Run schema sync to ensure all object types have a valid type_name.' : `Use type_name "${byDisplayName.type_name}" instead of "${typeName}"`, }, }); return; } res.status(400).json({ success: false, error: `Object type ${typeName} not found. Available enabled types: ${enabledTypes.map((t: { typeName?: string }) => t.typeName).filter(Boolean).join(', ') || 'none'}. Please run schema sync first.`, }); } return; } const result = await services.objectSyncService.syncObjectType( type.schemaId.toString(), type.objectTypeId, type.typeName, type.displayName ); // Return success even if there are errors (errors are in result.errors array) res.json({ success: true, ...result, hasErrors: result.errors.length > 0, }); } catch (error) { logger.error(`SyncController: Failed to sync object type ${req.params.typeName}`, error); res.status(500).json({ success: false, error: error instanceof Error ? error.message : 'Unknown error', }); } } }