- Fix query parameter type issues (string | string[] to string) in controllers - Add public getDatabaseAdapter() method to SchemaRepository for db access - Fix SchemaSyncService export and import issues - Fix referenceObject vs referenceObjectType property name - Add missing jiraAssetsClient import in normalizedCacheStore - Fix duplicate properties in object literals - Add type annotations for implicit any types - Fix type predicate issues with generics - Fix method calls (getEnabledObjectTypes, syncAllSchemas) - Fix type mismatches (ObjectTypeRecord vs expected types) - Fix Buffer type issue in biaMatchingService - Export SchemaSyncService class for ServiceFactory
290 lines
13 KiB
TypeScript
290 lines
13 KiB
TypeScript
/**
|
|
* 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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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',
|
|
});
|
|
}
|
|
}
|
|
}
|