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:
468
backend/src/services/schemaConfigurationService.ts
Normal file
468
backend/src/services/schemaConfigurationService.ts
Normal file
@@ -0,0 +1,468 @@
|
||||
/**
|
||||
* 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';
|
||||
|
||||
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 = (normalizedCacheStore as any).db;
|
||||
if (!db) {
|
||||
throw new Error('Database not available');
|
||||
}
|
||||
|
||||
await db.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 = (normalizedCacheStore as any).db;
|
||||
if (!db) {
|
||||
throw new Error('Database not available');
|
||||
}
|
||||
|
||||
await db.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 = (normalizedCacheStore as any).db;
|
||||
if (!db) {
|
||||
throw new Error('Database not available');
|
||||
}
|
||||
|
||||
await db.ensureInitialized?.();
|
||||
|
||||
const now = new Date().toISOString();
|
||||
|
||||
await db.transaction(async (txDb) => {
|
||||
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 = (normalizedCacheStore as any).db;
|
||||
if (!db) {
|
||||
throw new Error('Database not available');
|
||||
}
|
||||
|
||||
await db.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 => ({
|
||||
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 = (normalizedCacheStore as any).db;
|
||||
if (!db) {
|
||||
throw new Error('Database not available');
|
||||
}
|
||||
|
||||
await db.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 = (normalizedCacheStore as any).db;
|
||||
if (!db) {
|
||||
throw new Error('Database not available');
|
||||
}
|
||||
|
||||
await db.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 => ({
|
||||
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 = (normalizedCacheStore as any).db;
|
||||
if (!db) {
|
||||
throw new Error('Database not available');
|
||||
}
|
||||
|
||||
await db.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();
|
||||
Reference in New Issue
Block a user