- 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
479 lines
15 KiB
TypeScript
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();
|