Files
cmdb-insight/backend/src/services/schemaConfigurationService.ts
Bert Hausmans c331540369 Fix TypeScript type errors in schemaConfigurationService
- Type all db variables as DatabaseAdapter to enable generic method calls
- Add explicit type annotations for row parameters in map callbacks
- Cast ensureInitialized calls to any (not part of DatabaseAdapter interface)

Resolves all 9 TypeScript linter errors in the file
2026-01-21 03:31:55 +01:00

479 lines
15 KiB
TypeScript

/**
* 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<Array<{
schemaId: string;
schemaName: string;
objectTypes: ConfiguredObjectType[];
}>> {
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<string, ConfiguredObjectType[]>();
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<void> {
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<void> {
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<Array<{
schemaId: string;
objectTypeId: number;
objectTypeName: string;
displayName: string;
}>> {
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<boolean> {
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<Array<{
schemaId: string;
schemaName: string;
searchEnabled: boolean;
}>> {
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<void> {
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();