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:
256
backend/src/services/schemaCacheService.ts
Normal file
256
backend/src/services/schemaCacheService.ts
Normal file
@@ -0,0 +1,256 @@
|
||||
/**
|
||||
* Schema Cache Service
|
||||
*
|
||||
* In-memory cache for schema data with TTL support.
|
||||
* Provides fast access to schema information without hitting the database on every request.
|
||||
*/
|
||||
|
||||
import { logger } from './logger.js';
|
||||
import { schemaDiscoveryService } from './schemaDiscoveryService.js';
|
||||
import type { ObjectTypeDefinition, AttributeDefinition } from '../generated/jira-schema.js';
|
||||
import { getDatabaseAdapter } from './database/singleton.js';
|
||||
|
||||
interface SchemaResponse {
|
||||
metadata: {
|
||||
generatedAt: string;
|
||||
objectTypeCount: number;
|
||||
totalAttributes: number;
|
||||
enabledObjectTypeCount?: number;
|
||||
};
|
||||
objectTypes: Record<string, ObjectTypeWithLinks>;
|
||||
cacheCounts?: Record<string, number>;
|
||||
jiraCounts?: Record<string, number>;
|
||||
}
|
||||
|
||||
interface ObjectTypeWithLinks extends ObjectTypeDefinition {
|
||||
enabled: boolean; // Whether this object type is enabled for syncing
|
||||
incomingLinks: Array<{
|
||||
fromType: string;
|
||||
fromTypeName: string;
|
||||
attributeName: string;
|
||||
isMultiple: boolean;
|
||||
}>;
|
||||
outgoingLinks: Array<{
|
||||
toType: string;
|
||||
toTypeName: string;
|
||||
attributeName: string;
|
||||
isMultiple: boolean;
|
||||
}>;
|
||||
}
|
||||
|
||||
class SchemaCacheService {
|
||||
private cache: SchemaResponse | null = null;
|
||||
private cacheTimestamp: number = 0;
|
||||
private readonly CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
||||
private db = getDatabaseAdapter(); // Use shared database adapter singleton
|
||||
|
||||
/**
|
||||
* Get schema from cache or fetch from database
|
||||
*/
|
||||
async getSchema(): Promise<SchemaResponse> {
|
||||
// Check cache validity
|
||||
const now = Date.now();
|
||||
if (this.cache && (now - this.cacheTimestamp) < this.CACHE_TTL_MS) {
|
||||
logger.debug('SchemaCache: Returning cached schema');
|
||||
return this.cache;
|
||||
}
|
||||
|
||||
// Cache expired or doesn't exist - fetch from database
|
||||
logger.debug('SchemaCache: Cache expired or missing, fetching from database');
|
||||
const schema = await this.fetchFromDatabase();
|
||||
|
||||
// Update cache
|
||||
this.cache = schema;
|
||||
this.cacheTimestamp = now;
|
||||
|
||||
return schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate cache (force refresh on next request)
|
||||
*/
|
||||
invalidate(): void {
|
||||
logger.debug('SchemaCache: Invalidating cache');
|
||||
this.cache = null;
|
||||
this.cacheTimestamp = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch schema from database and build response
|
||||
* Returns ALL object types (enabled and disabled) with their sync status
|
||||
*/
|
||||
private async fetchFromDatabase(): Promise<SchemaResponse> {
|
||||
// Schema discovery must be manually triggered via API endpoints
|
||||
// No automatic discovery on first run
|
||||
|
||||
// Fetch ALL object types (enabled and disabled) with their schema info
|
||||
const objectTypeRows = await this.db.query<{
|
||||
id: number;
|
||||
schema_id: number;
|
||||
jira_type_id: number;
|
||||
type_name: string;
|
||||
display_name: string;
|
||||
description: string | null;
|
||||
sync_priority: number;
|
||||
object_count: number;
|
||||
enabled: boolean | number;
|
||||
}>(
|
||||
`SELECT ot.id, ot.schema_id, ot.jira_type_id, ot.type_name, ot.display_name, ot.description, ot.sync_priority, ot.object_count, ot.enabled
|
||||
FROM object_types ot
|
||||
ORDER BY ot.sync_priority, ot.type_name`
|
||||
);
|
||||
|
||||
if (objectTypeRows.length === 0) {
|
||||
// No types found, return empty schema
|
||||
return {
|
||||
metadata: {
|
||||
generatedAt: new Date().toISOString(),
|
||||
objectTypeCount: 0,
|
||||
totalAttributes: 0,
|
||||
},
|
||||
objectTypes: {},
|
||||
};
|
||||
}
|
||||
|
||||
// Fetch attributes for ALL object types using JOIN
|
||||
const attributeRows = await this.db.query<{
|
||||
id: number;
|
||||
jira_attr_id: number;
|
||||
object_type_name: string;
|
||||
attr_name: string;
|
||||
field_name: string;
|
||||
attr_type: string;
|
||||
is_multiple: boolean | number;
|
||||
is_editable: boolean | number;
|
||||
is_required: boolean | number;
|
||||
is_system: boolean | number;
|
||||
reference_type_name: string | null;
|
||||
description: string | null;
|
||||
position: number | null;
|
||||
schema_id: number;
|
||||
type_name: string;
|
||||
}>(
|
||||
`SELECT a.*, ot.schema_id, ot.type_name
|
||||
FROM attributes a
|
||||
INNER JOIN object_types ot ON a.object_type_name = ot.type_name
|
||||
ORDER BY ot.type_name, COALESCE(a.position, 0), a.jira_attr_id`
|
||||
);
|
||||
|
||||
logger.debug(`SchemaCache: Found ${objectTypeRows.length} object types (enabled and disabled) and ${attributeRows.length} attributes`);
|
||||
|
||||
// Build object types with attributes
|
||||
// Use type_name as key (even if same type exists in multiple schemas, we'll show the first enabled one)
|
||||
// In practice, if same type_name exists in multiple schemas, attributes should be the same
|
||||
const objectTypesWithLinks: Record<string, ObjectTypeWithLinks> = {};
|
||||
|
||||
for (const typeRow of objectTypeRows) {
|
||||
const typeName = typeRow.type_name;
|
||||
|
||||
// Skip if we already have this type_name (first enabled one wins)
|
||||
if (objectTypesWithLinks[typeName]) {
|
||||
logger.debug(`SchemaCache: Skipping duplicate type_name ${typeName} from schema ${typeRow.schema_id}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Match attributes by both schema_id and type_name to ensure correct mapping
|
||||
const matchingAttributes = attributeRows.filter(a => a.schema_id === typeRow.schema_id && a.type_name === typeName);
|
||||
logger.debug(`SchemaCache: Found ${matchingAttributes.length} attributes for ${typeName} (schema_id: ${typeRow.schema_id})`);
|
||||
|
||||
const attributes = matchingAttributes.map(attrRow => {
|
||||
// Convert boolean/number for SQLite compatibility
|
||||
const isMultiple = typeof attrRow.is_multiple === 'boolean' ? attrRow.is_multiple : attrRow.is_multiple === 1;
|
||||
const isEditable = typeof attrRow.is_editable === 'boolean' ? attrRow.is_editable : attrRow.is_editable === 1;
|
||||
const isRequired = typeof attrRow.is_required === 'boolean' ? attrRow.is_required : attrRow.is_required === 1;
|
||||
const isSystem = typeof attrRow.is_system === 'boolean' ? attrRow.is_system : attrRow.is_system === 1;
|
||||
|
||||
return {
|
||||
jiraId: attrRow.jira_attr_id,
|
||||
name: attrRow.attr_name,
|
||||
fieldName: attrRow.field_name,
|
||||
type: attrRow.attr_type as AttributeDefinition['type'],
|
||||
isMultiple,
|
||||
isEditable,
|
||||
isRequired,
|
||||
isSystem,
|
||||
referenceTypeName: attrRow.reference_type_name || undefined,
|
||||
description: attrRow.description || undefined,
|
||||
position: attrRow.position ?? 0,
|
||||
} as AttributeDefinition;
|
||||
});
|
||||
|
||||
// Convert enabled boolean/number to boolean
|
||||
const isEnabled = typeof typeRow.enabled === 'boolean' ? typeRow.enabled : typeRow.enabled === 1;
|
||||
|
||||
objectTypesWithLinks[typeName] = {
|
||||
jiraTypeId: typeRow.jira_type_id,
|
||||
name: typeRow.display_name,
|
||||
typeName: typeName,
|
||||
syncPriority: typeRow.sync_priority,
|
||||
objectCount: typeRow.object_count,
|
||||
enabled: isEnabled,
|
||||
attributes,
|
||||
incomingLinks: [],
|
||||
outgoingLinks: [],
|
||||
};
|
||||
}
|
||||
|
||||
// Build link relationships
|
||||
for (const [typeName, typeDef] of Object.entries(objectTypesWithLinks)) {
|
||||
for (const attr of typeDef.attributes) {
|
||||
if (attr.type === 'reference' && attr.referenceTypeName) {
|
||||
// Add outgoing link from this type
|
||||
typeDef.outgoingLinks.push({
|
||||
toType: attr.referenceTypeName,
|
||||
toTypeName: objectTypesWithLinks[attr.referenceTypeName]?.name || attr.referenceTypeName,
|
||||
attributeName: attr.name,
|
||||
isMultiple: attr.isMultiple,
|
||||
});
|
||||
|
||||
// Add incoming link to the referenced type
|
||||
if (objectTypesWithLinks[attr.referenceTypeName]) {
|
||||
objectTypesWithLinks[attr.referenceTypeName].incomingLinks.push({
|
||||
fromType: typeName,
|
||||
fromTypeName: typeDef.name,
|
||||
attributeName: attr.name,
|
||||
isMultiple: attr.isMultiple,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get cache counts (objectsByType) if available
|
||||
let cacheCounts: Record<string, number> | undefined;
|
||||
try {
|
||||
const { dataService } = await import('./dataService.js');
|
||||
const cacheStatus = await dataService.getCacheStatus();
|
||||
cacheCounts = cacheStatus.objectsByType;
|
||||
} catch (err) {
|
||||
logger.debug('SchemaCache: Could not fetch cache counts', err);
|
||||
// Continue without cache counts - not critical
|
||||
}
|
||||
|
||||
// Calculate metadata (include enabled count)
|
||||
const totalAttributes = Object.values(objectTypesWithLinks).reduce(
|
||||
(sum, t) => sum + t.attributes.length,
|
||||
0
|
||||
);
|
||||
const enabledCount = Object.values(objectTypesWithLinks).filter(t => t.enabled).length;
|
||||
|
||||
const response: SchemaResponse = {
|
||||
metadata: {
|
||||
generatedAt: new Date().toISOString(),
|
||||
objectTypeCount: objectTypeRows.length,
|
||||
totalAttributes,
|
||||
enabledObjectTypeCount: enabledCount,
|
||||
},
|
||||
objectTypes: objectTypesWithLinks,
|
||||
cacheCounts,
|
||||
};
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const schemaCacheService = new SchemaCacheService();
|
||||
Reference in New Issue
Block a user