Improve Team-indeling dashboard UI and cache invalidation

- Replace 'TEAM' label with Type attribute (Business/Enabling/Staf) in team blocks
- Make Type labels larger (text-sm) and brighter colors
- Make SUBTEAM label less bright (indigo-300) and smaller (text-[10px])
- Add 'FTE' suffix to bandbreedte values in header and application blocks
- Add Platform and Connected Device labels to application blocks
- Show Platform FTE and Workloads FTE separately in Platform blocks
- Add spacing between Regiemodel letter and count value
- Add cache invalidation for Team Dashboard when applications are updated
- Enrich team references with Type attribute in getSubteamToTeamMapping
This commit is contained in:
2026-01-10 02:16:55 +01:00
parent ea1c84262c
commit ca21b9538d
54 changed files with 13444 additions and 1789 deletions

View File

@@ -6,7 +6,8 @@
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
"start": "node dist/index.js",
"generate-schema": "tsx scripts/generate-schema.ts"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.32.1",

View File

@@ -0,0 +1,982 @@
#!/usr/bin/env npx tsx
/**
* Schema Generator - Fetches Jira Assets schema DYNAMICALLY and generates:
* - TypeScript types (jira-types.ts)
* - Schema metadata (jira-schema.ts)
* - Database schema (db-schema.sql)
*
* This script connects to the Jira Assets API to discover ALL object types
* and their attributes, ensuring the data model is always in sync with the
* actual CMDB configuration.
*
* Usage: npm run generate-schema
*/
import * as fs from 'fs';
import * as path from 'path';
import { fileURLToPath } from 'url';
import dotenv from 'dotenv';
// Load environment variables
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Try multiple possible .env locations
const envPaths = [
path.resolve(__dirname, '../../.env'), // backend/.env
path.resolve(__dirname, '../../../.env'), // project root .env
];
let envLoaded = '';
for (const envPath of envPaths) {
if (fs.existsSync(envPath)) {
dotenv.config({ path: envPath });
envLoaded = envPath;
break;
}
}
// Configuration
const JIRA_HOST = process.env.JIRA_HOST || '';
const JIRA_PAT = process.env.JIRA_PAT || '';
const JIRA_SCHEMA_ID = process.env.JIRA_SCHEMA_ID || '';
const OUTPUT_DIR = path.resolve(__dirname, '../src/generated');
// =============================================================================
// Interfaces
// =============================================================================
interface JiraObjectSchema {
id: number;
name: string;
objectSchemaKey: string;
description?: string;
objectCount?: number;
}
interface JiraObjectType {
id: number;
name: string;
description?: string;
iconId?: number;
objectCount?: number;
parentObjectTypeId?: number;
objectSchemaId: number;
inherited?: boolean;
abstractObjectType?: boolean;
parentObjectTypeInherited?: boolean;
}
interface JiraAttribute {
id: number;
name: string;
label?: string;
type: number;
typeValue?: string;
defaultType?: { id: number; name: string };
referenceObjectTypeId?: number;
referenceObjectType?: { id: number; name: string };
referenceType?: { id: number; name: string };
minimumCardinality?: number;
maximumCardinality?: number;
editable?: boolean;
hidden?: boolean;
system?: boolean;
options?: string;
description?: string;
}
interface GeneratedAttribute {
jiraId: number;
name: string;
fieldName: string;
type: 'text' | 'integer' | 'float' | 'boolean' | 'date' | 'datetime' | 'select' | 'reference' | 'url' | 'email' | 'textarea' | 'user' | 'status' | 'unknown';
isMultiple: boolean;
isEditable: boolean;
isRequired: boolean;
isSystem: boolean;
referenceTypeId?: number;
referenceTypeName?: string;
description?: string;
}
interface GeneratedObjectType {
jiraTypeId: number;
name: string;
typeName: string;
syncPriority: number;
objectCount: number;
attributes: GeneratedAttribute[];
}
// =============================================================================
// Jira Type Mapping
// =============================================================================
// Jira attribute type mappings (based on Jira Insight/Assets API)
// See: https://docs.atlassian.com/jira-servicemanagement-docs/REST/5.x/insight/1.0/objecttypeattribute
const JIRA_TYPE_MAP: Record<number, GeneratedAttribute['type']> = {
0: 'text', // Default/Text
1: 'integer', // Integer
2: 'boolean', // Boolean
3: 'float', // Double/Float
4: 'date', // Date
5: 'datetime', // DateTime
6: 'url', // URL
7: 'email', // Email
8: 'textarea', // Textarea
9: 'select', // Select
10: 'reference', // Reference (Object)
11: 'user', // User
12: 'reference', // Confluence (treated as reference)
13: 'reference', // Group (treated as reference)
14: 'reference', // Version (treated as reference)
15: 'reference', // Project (treated as reference)
16: 'status', // Status
};
// Priority types - these sync first as they are reference data
const PRIORITY_TYPE_NAMES = new Set([
'Application Component',
'Server',
'Flows',
]);
// Reference data types - these sync with lower priority
const REFERENCE_TYPE_PATTERNS = [
/Factor$/,
/Model$/,
/Type$/,
/Category$/,
/Importance$/,
/Analyse$/,
/Organisation$/,
/Function$/,
];
// =============================================================================
// Jira Schema Fetcher
// =============================================================================
class JiraSchemaFetcher {
private baseUrl: string;
private headers: Record<string, string>;
constructor(host: string, pat: string) {
this.baseUrl = `${host}/rest/insight/1.0`;
this.headers = {
'Authorization': `Bearer ${pat}`,
'Content-Type': 'application/json',
'Accept': 'application/json',
};
}
/**
* Fetch schema info
*/
async fetchSchema(schemaId: string): Promise<JiraObjectSchema | null> {
try {
const response = await fetch(`${this.baseUrl}/objectschema/${schemaId}`, {
headers: this.headers,
});
if (!response.ok) {
console.error(`Failed to fetch schema ${schemaId}: ${response.status} ${response.statusText}`);
const text = await response.text();
console.error(`Response: ${text}`);
return null;
}
return await response.json();
} catch (error) {
console.error(`Error fetching schema ${schemaId}:`, error);
return null;
}
}
/**
* Fetch ALL object types in the schema
*/
async fetchAllObjectTypes(schemaId: string): Promise<JiraObjectType[]> {
try {
// Try the objecttypes/flat endpoint first (returns all types in flat structure)
let response = await fetch(`${this.baseUrl}/objectschema/${schemaId}/objecttypes/flat`, {
headers: this.headers,
});
if (!response.ok) {
// Fallback to regular objecttypes endpoint
response = await fetch(`${this.baseUrl}/objectschema/${schemaId}/objecttypes`, {
headers: this.headers,
});
}
if (!response.ok) {
console.error(`Failed to fetch object types: ${response.status} ${response.statusText}`);
const text = await response.text();
console.error(`Response: ${text}`);
return [];
}
const result = await response.json();
// Handle both array and object responses
if (Array.isArray(result)) {
return result;
} else if (result.objectTypes) {
return result.objectTypes;
}
return [];
} catch (error) {
console.error(`Error fetching object types:`, error);
return [];
}
}
/**
* Fetch attributes for a specific object type
*/
async fetchAttributes(typeId: number): Promise<JiraAttribute[]> {
try {
const response = await fetch(`${this.baseUrl}/objecttype/${typeId}/attributes`, {
headers: this.headers,
});
if (!response.ok) {
console.error(`Failed to fetch attributes for type ${typeId}: ${response.status}`);
return [];
}
return await response.json();
} catch (error) {
console.error(`Error fetching attributes for type ${typeId}:`, error);
return [];
}
}
/**
* Test the connection
*/
async testConnection(): Promise<boolean> {
try {
const response = await fetch(`${this.baseUrl}/objectschema/list`, {
headers: this.headers,
});
return response.ok;
} catch {
return false;
}
}
}
// =============================================================================
// Utility Functions
// =============================================================================
/**
* Convert a string to camelCase while preserving existing casing patterns
* E.g., "Application Function" -> "applicationFunction"
* "ICT Governance Model" -> "ictGovernanceModel"
* "ApplicationFunction" -> "applicationFunction"
*/
function toCamelCase(str: string): string {
// First split on spaces and special chars
const words = str
.replace(/[^a-zA-Z0-9\s]/g, ' ')
.split(/\s+/)
.filter(w => w.length > 0);
if (words.length === 0) return '';
// If it's a single word that's already camelCase or PascalCase, just lowercase first char
if (words.length === 1) {
const word = words[0];
return word.charAt(0).toLowerCase() + word.slice(1);
}
// Multiple words - first word lowercase, rest capitalize first letter
return words
.map((word, index) => {
if (index === 0) {
// First word: if all uppercase (acronym), lowercase it, otherwise just lowercase first char
if (word === word.toUpperCase() && word.length > 1) {
return word.toLowerCase();
}
return word.charAt(0).toLowerCase() + word.slice(1);
}
// Other words: capitalize first letter, keep rest as-is
return word.charAt(0).toUpperCase() + word.slice(1);
})
.join('');
}
/**
* Convert a string to PascalCase while preserving existing casing patterns
* E.g., "Application Function" -> "ApplicationFunction"
* "ICT Governance Model" -> "IctGovernanceModel"
* "applicationFunction" -> "ApplicationFunction"
*/
function toPascalCase(str: string): string {
// First split on spaces and special chars
const words = str
.replace(/[^a-zA-Z0-9\s]/g, ' ')
.split(/\s+/)
.filter(w => w.length > 0);
if (words.length === 0) return '';
// If it's a single word, just capitalize first letter
if (words.length === 1) {
const word = words[0];
return word.charAt(0).toUpperCase() + word.slice(1);
}
// Multiple words - capitalize first letter of each
return words
.map(word => {
// If all uppercase (acronym) and first word, just capitalize first letter
if (word === word.toUpperCase() && word.length > 1) {
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
}
return word.charAt(0).toUpperCase() + word.slice(1);
})
.join('');
}
function mapJiraType(typeId: number): GeneratedAttribute['type'] {
return JIRA_TYPE_MAP[typeId] || 'unknown';
}
function determineSyncPriority(typeName: string, objectCount: number): number {
// Application Component and related main types first
if (PRIORITY_TYPE_NAMES.has(typeName)) {
return 1;
}
// Reference data types last
for (const pattern of REFERENCE_TYPE_PATTERNS) {
if (pattern.test(typeName)) {
return 10;
}
}
// Medium priority for types with more objects
if (objectCount > 100) {
return 2;
}
if (objectCount > 10) {
return 5;
}
return 8;
}
function parseAttribute(
attr: JiraAttribute,
allTypeConfigs: Map<number, { name: string; typeName: string }>
): GeneratedAttribute {
const typeId = attr.type || attr.defaultType?.id || 0;
let type = mapJiraType(typeId);
const isMultiple = (attr.maximumCardinality ?? 1) > 1 || attr.maximumCardinality === -1;
const isEditable = attr.editable !== false && !attr.hidden;
const isRequired = (attr.minimumCardinality ?? 0) > 0;
const isSystem = attr.system === true;
// CRITICAL: Jira sometimes returns type=1 (integer) for reference attributes!
// The presence of referenceObjectTypeId is the true indicator of a reference type.
const refTypeId = attr.referenceObjectTypeId || attr.referenceObjectType?.id || attr.referenceType?.id;
if (refTypeId) {
type = 'reference';
}
const result: GeneratedAttribute = {
jiraId: attr.id,
name: attr.name,
fieldName: toCamelCase(attr.name),
type,
isMultiple,
isEditable,
isRequired,
isSystem,
description: attr.description,
};
// Handle reference types - add reference metadata
if (type === 'reference' && refTypeId) {
result.referenceTypeId = refTypeId;
const refConfig = allTypeConfigs.get(refTypeId);
result.referenceTypeName = refConfig?.typeName ||
attr.referenceObjectType?.name ||
attr.referenceType?.name ||
`Type${refTypeId}`;
}
return result;
}
// =============================================================================
// Code Generation Functions
// =============================================================================
function generateTypeScriptType(attrType: GeneratedAttribute['type'], isMultiple: boolean, isReference: boolean): string {
let tsType: string;
if (isReference) {
tsType = 'ObjectReference';
} else {
switch (attrType) {
case 'text':
case 'textarea':
case 'url':
case 'email':
case 'select':
case 'user':
case 'status':
tsType = 'string';
break;
case 'integer':
case 'float':
tsType = 'number';
break;
case 'boolean':
tsType = 'boolean';
break;
case 'date':
case 'datetime':
tsType = 'string'; // ISO date string
break;
default:
tsType = 'unknown';
}
}
if (isMultiple) {
return `${tsType}[]`;
}
return `${tsType} | null`;
}
function generateTypesFile(objectTypes: GeneratedObjectType[], generatedAt: Date): string {
const lines: string[] = [
'// AUTO-GENERATED FILE - DO NOT EDIT MANUALLY',
'// Generated from Jira Assets Schema via REST API',
`// Generated at: ${generatedAt.toISOString()}`,
'//',
'// Re-generate with: npm run generate-schema',
'',
'// =============================================================================',
'// Base Types',
'// =============================================================================',
'',
'/** Reference to another CMDB object */',
'export interface ObjectReference {',
' objectId: string;',
' objectKey: string;',
' label: string;',
' // Optional enriched data from referenced object',
' factor?: number;',
'}',
'',
'/** Base interface for all CMDB objects */',
'export interface BaseCMDBObject {',
' id: string;',
' objectKey: string;',
' label: string;',
' _objectType: string;',
' _jiraUpdatedAt: string;',
' _jiraCreatedAt: string;',
'}',
'',
'// =============================================================================',
'// Object Type Interfaces',
'// =============================================================================',
'',
];
for (const objType of objectTypes) {
lines.push(`/** ${objType.name} (Jira Type ID: ${objType.jiraTypeId}, ${objType.objectCount} objects) */`);
lines.push(`export interface ${objType.typeName} extends BaseCMDBObject {`);
lines.push(` _objectType: '${objType.typeName}';`);
lines.push('');
// Group attributes by type
const scalarAttrs = objType.attributes.filter(a => a.type !== 'reference');
const refAttrs = objType.attributes.filter(a => a.type === 'reference');
if (scalarAttrs.length > 0) {
lines.push(' // Scalar attributes');
for (const attr of scalarAttrs) {
const tsType = generateTypeScriptType(attr.type, attr.isMultiple, false);
const comment = attr.description ? ` // ${attr.description}` : '';
lines.push(` ${attr.fieldName}: ${tsType};${comment}`);
}
lines.push('');
}
if (refAttrs.length > 0) {
lines.push(' // Reference attributes');
for (const attr of refAttrs) {
const tsType = generateTypeScriptType(attr.type, attr.isMultiple, true);
const comment = attr.referenceTypeName ? ` // -> ${attr.referenceTypeName}` : '';
lines.push(` ${attr.fieldName}: ${tsType};${comment}`);
}
lines.push('');
}
lines.push('}');
lines.push('');
}
// Generate union type
lines.push('// =============================================================================');
lines.push('// Union Types');
lines.push('// =============================================================================');
lines.push('');
lines.push('/** Union of all CMDB object types */');
lines.push('export type CMDBObject =');
for (let i = 0; i < objectTypes.length; i++) {
const suffix = i < objectTypes.length - 1 ? '' : ';';
lines.push(` | ${objectTypes[i].typeName}${suffix}`);
}
lines.push('');
// Generate type name literal union
lines.push('/** All valid object type names */');
lines.push('export type CMDBObjectTypeName =');
for (let i = 0; i < objectTypes.length; i++) {
const suffix = i < objectTypes.length - 1 ? '' : ';';
lines.push(` | '${objectTypes[i].typeName}'${suffix}`);
}
lines.push('');
// Generate type guards
lines.push('// =============================================================================');
lines.push('// Type Guards');
lines.push('// =============================================================================');
lines.push('');
for (const objType of objectTypes) {
lines.push(`export function is${objType.typeName}(obj: CMDBObject): obj is ${objType.typeName} {`);
lines.push(` return obj._objectType === '${objType.typeName}';`);
lines.push('}');
lines.push('');
}
return lines.join('\n');
}
function generateSchemaFile(objectTypes: GeneratedObjectType[], generatedAt: Date): string {
const lines: string[] = [
'// AUTO-GENERATED FILE - DO NOT EDIT MANUALLY',
'// Generated from Jira Assets Schema via REST API',
`// Generated at: ${generatedAt.toISOString()}`,
'//',
'// Re-generate with: npm run generate-schema',
'',
'// =============================================================================',
'// Schema Type Definitions',
'// =============================================================================',
'',
'export interface AttributeDefinition {',
' jiraId: number;',
' name: string;',
' fieldName: string;',
" type: 'text' | 'integer' | 'float' | 'boolean' | 'date' | 'datetime' | 'select' | 'reference' | 'url' | 'email' | 'textarea' | 'user' | 'status' | 'unknown';",
' isMultiple: boolean;',
' isEditable: boolean;',
' isRequired: boolean;',
' isSystem: boolean;',
' referenceTypeId?: number;',
' referenceTypeName?: string;',
' description?: string;',
'}',
'',
'export interface ObjectTypeDefinition {',
' jiraTypeId: number;',
' name: string;',
' typeName: string;',
' syncPriority: number;',
' objectCount: number;',
' attributes: AttributeDefinition[];',
'}',
'',
'// =============================================================================',
'// Schema Metadata',
'// =============================================================================',
'',
`export const SCHEMA_GENERATED_AT = '${generatedAt.toISOString()}';`,
`export const SCHEMA_OBJECT_TYPE_COUNT = ${objectTypes.length};`,
`export const SCHEMA_TOTAL_ATTRIBUTES = ${objectTypes.reduce((sum, ot) => sum + ot.attributes.length, 0)};`,
'',
'// =============================================================================',
'// Object Type Definitions',
'// =============================================================================',
'',
'export const OBJECT_TYPES: Record<string, ObjectTypeDefinition> = {',
];
for (let i = 0; i < objectTypes.length; i++) {
const objType = objectTypes[i];
const comma = i < objectTypes.length - 1 ? ',' : '';
lines.push(` '${objType.typeName}': {`);
lines.push(` jiraTypeId: ${objType.jiraTypeId},`);
lines.push(` name: '${objType.name.replace(/'/g, "\\'")}',`);
lines.push(` typeName: '${objType.typeName}',`);
lines.push(` syncPriority: ${objType.syncPriority},`);
lines.push(` objectCount: ${objType.objectCount},`);
lines.push(' attributes: [');
for (let j = 0; j < objType.attributes.length; j++) {
const attr = objType.attributes[j];
const attrComma = j < objType.attributes.length - 1 ? ',' : '';
let attrLine = ` { jiraId: ${attr.jiraId}, name: '${attr.name.replace(/'/g, "\\'")}', fieldName: '${attr.fieldName}', type: '${attr.type}', isMultiple: ${attr.isMultiple}, isEditable: ${attr.isEditable}, isRequired: ${attr.isRequired}, isSystem: ${attr.isSystem}`;
if (attr.referenceTypeId) {
attrLine += `, referenceTypeId: ${attr.referenceTypeId}, referenceTypeName: '${attr.referenceTypeName}'`;
}
if (attr.description) {
attrLine += `, description: '${attr.description.replace(/'/g, "\\'").replace(/\n/g, ' ')}'`;
}
attrLine += ` }${attrComma}`;
lines.push(attrLine);
}
lines.push(' ],');
lines.push(` }${comma}`);
}
lines.push('};');
lines.push('');
// Generate lookup maps
lines.push('// =============================================================================');
lines.push('// Lookup Maps');
lines.push('// =============================================================================');
lines.push('');
// Type ID to name map
lines.push('/** Map from Jira Type ID to TypeScript type name */');
lines.push('export const TYPE_ID_TO_NAME: Record<number, string> = {');
for (const objType of objectTypes) {
lines.push(` ${objType.jiraTypeId}: '${objType.typeName}',`);
}
lines.push('};');
lines.push('');
// Type name to ID map
lines.push('/** Map from TypeScript type name to Jira Type ID */');
lines.push('export const TYPE_NAME_TO_ID: Record<string, number> = {');
for (const objType of objectTypes) {
lines.push(` '${objType.typeName}': ${objType.jiraTypeId},`);
}
lines.push('};');
lines.push('');
// Jira name to TypeScript name map
lines.push('/** Map from Jira object type name to TypeScript type name */');
lines.push('export const JIRA_NAME_TO_TYPE: Record<string, string> = {');
for (const objType of objectTypes) {
lines.push(` '${objType.name.replace(/'/g, "\\'")}': '${objType.typeName}',`);
}
lines.push('};');
lines.push('');
// Helper functions
lines.push('// =============================================================================');
lines.push('// Helper Functions');
lines.push('// =============================================================================');
lines.push('');
lines.push('/** Get attribute definition by type and field name */');
lines.push('export function getAttributeDefinition(typeName: string, fieldName: string): AttributeDefinition | undefined {');
lines.push(' const objectType = OBJECT_TYPES[typeName];');
lines.push(' if (!objectType) return undefined;');
lines.push(' return objectType.attributes.find(a => a.fieldName === fieldName);');
lines.push('}');
lines.push('');
lines.push('/** Get attribute definition by type and Jira attribute ID */');
lines.push('export function getAttributeById(typeName: string, jiraId: number): AttributeDefinition | undefined {');
lines.push(' const objectType = OBJECT_TYPES[typeName];');
lines.push(' if (!objectType) return undefined;');
lines.push(' return objectType.attributes.find(a => a.jiraId === jiraId);');
lines.push('}');
lines.push('');
lines.push('/** Get attribute definition by type and Jira attribute name */');
lines.push('export function getAttributeByName(typeName: string, attrName: string): AttributeDefinition | undefined {');
lines.push(' const objectType = OBJECT_TYPES[typeName];');
lines.push(' if (!objectType) return undefined;');
lines.push(' return objectType.attributes.find(a => a.name === attrName);');
lines.push('}');
lines.push('');
lines.push('/** Get attribute Jira ID by type and attribute name - throws if not found */');
lines.push('export function getAttributeId(typeName: string, attrName: string): number {');
lines.push(' const attr = getAttributeByName(typeName, attrName);');
lines.push(' if (!attr) {');
lines.push(' throw new Error(`Attribute "${attrName}" not found on type "${typeName}"`);');
lines.push(' }');
lines.push(' return attr.jiraId;');
lines.push('}');
lines.push('');
lines.push('/** Get all reference attributes for a type */');
lines.push('export function getReferenceAttributes(typeName: string): AttributeDefinition[] {');
lines.push(' const objectType = OBJECT_TYPES[typeName];');
lines.push(' if (!objectType) return [];');
lines.push(" return objectType.attributes.filter(a => a.type === 'reference');");
lines.push('}');
lines.push('');
lines.push('/** Get all object types sorted by sync priority */');
lines.push('export function getObjectTypesBySyncPriority(): ObjectTypeDefinition[] {');
lines.push(' return Object.values(OBJECT_TYPES).sort((a, b) => a.syncPriority - b.syncPriority);');
lines.push('}');
lines.push('');
return lines.join('\n');
}
function generateDatabaseSchema(generatedAt: Date): string {
const lines: string[] = [
'-- AUTO-GENERATED FILE - DO NOT EDIT MANUALLY',
'-- Generated from Jira Assets Schema via REST API',
`-- Generated at: ${generatedAt.toISOString()}`,
'--',
'-- Re-generate with: npm run generate-schema',
'',
'-- =============================================================================',
'-- Core Tables',
'-- =============================================================================',
'',
'-- Cached CMDB objects (all types stored in single table with JSON data)',
'CREATE TABLE IF NOT EXISTS cached_objects (',
' id TEXT PRIMARY KEY,',
' object_key TEXT NOT NULL UNIQUE,',
' object_type TEXT NOT NULL,',
' label TEXT NOT NULL,',
' data JSON NOT NULL,',
' jira_updated_at TEXT,',
' jira_created_at TEXT,',
' cached_at TEXT NOT NULL',
');',
'',
'-- Object relations (references between objects)',
'CREATE TABLE IF NOT EXISTS object_relations (',
' id INTEGER PRIMARY KEY AUTOINCREMENT,',
' source_id TEXT NOT NULL,',
' target_id TEXT NOT NULL,',
' attribute_name TEXT NOT NULL,',
' source_type TEXT NOT NULL,',
' target_type TEXT NOT NULL,',
' UNIQUE(source_id, target_id, attribute_name)',
');',
'',
'-- Sync metadata (tracks sync state)',
'CREATE TABLE IF NOT EXISTS sync_metadata (',
' key TEXT PRIMARY KEY,',
' value TEXT NOT NULL,',
' updated_at TEXT NOT NULL',
');',
'',
'-- =============================================================================',
'-- Indices for Performance',
'-- =============================================================================',
'',
'CREATE INDEX IF NOT EXISTS idx_objects_type ON cached_objects(object_type);',
'CREATE INDEX IF NOT EXISTS idx_objects_key ON cached_objects(object_key);',
'CREATE INDEX IF NOT EXISTS idx_objects_updated ON cached_objects(jira_updated_at);',
'CREATE INDEX IF NOT EXISTS idx_objects_label ON cached_objects(label);',
'',
'CREATE INDEX IF NOT EXISTS idx_relations_source ON object_relations(source_id);',
'CREATE INDEX IF NOT EXISTS idx_relations_target ON object_relations(target_id);',
'CREATE INDEX IF NOT EXISTS idx_relations_source_type ON object_relations(source_type);',
'CREATE INDEX IF NOT EXISTS idx_relations_target_type ON object_relations(target_type);',
'CREATE INDEX IF NOT EXISTS idx_relations_attr ON object_relations(attribute_name);',
'',
];
return lines.join('\n');
}
// =============================================================================
// Main
// =============================================================================
async function main() {
const generatedAt = new Date();
console.log('');
console.log('╔════════════════════════════════════════════════════════════════╗');
console.log('║ CMDB Schema Generator - Jira Assets API ║');
console.log('╚════════════════════════════════════════════════════════════════╝');
console.log('');
// Validate configuration
if (!JIRA_HOST) {
console.error('❌ ERROR: JIRA_HOST environment variable is required');
console.error(' Set this in your .env file: JIRA_HOST=https://jira.your-domain.com');
process.exit(1);
}
if (!JIRA_PAT) {
console.error('❌ ERROR: JIRA_PAT environment variable is required');
console.error(' Set this in your .env file: JIRA_PAT=your-personal-access-token');
process.exit(1);
}
if (!JIRA_SCHEMA_ID) {
console.error('❌ ERROR: JIRA_SCHEMA_ID environment variable is required');
console.error(' Set this in your .env file: JIRA_SCHEMA_ID=6');
process.exit(1);
}
if (envLoaded) {
console.log(`🔧 Environment: ${envLoaded}`);
}
console.log(`📡 Jira Host: ${JIRA_HOST}`);
console.log(`📋 Schema ID: ${JIRA_SCHEMA_ID}`);
console.log(`📁 Output Dir: ${OUTPUT_DIR}`);
console.log('');
// Ensure output directory exists
if (!fs.existsSync(OUTPUT_DIR)) {
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
console.log(`📁 Created output directory: ${OUTPUT_DIR}`);
}
const fetcher = new JiraSchemaFetcher(JIRA_HOST, JIRA_PAT);
// Test connection
console.log('🔌 Testing connection to Jira Assets API...');
const connected = await fetcher.testConnection();
if (!connected) {
console.error('❌ Failed to connect to Jira Assets API');
console.error(' Please check your JIRA_HOST and JIRA_PAT settings');
process.exit(1);
}
console.log('✅ Connection successful');
console.log('');
// Fetch schema info
console.log('📋 Fetching schema information...');
const schema = await fetcher.fetchSchema(JIRA_SCHEMA_ID);
if (!schema) {
console.error(`❌ Failed to fetch schema ${JIRA_SCHEMA_ID}`);
process.exit(1);
}
console.log(` Schema: ${schema.name} (${schema.objectSchemaKey})`);
console.log(` Total objects: ${schema.objectCount || 'unknown'}`);
console.log('');
// Fetch ALL object types from the schema
console.log('📦 Fetching all object types from schema...');
const allObjectTypes = await fetcher.fetchAllObjectTypes(JIRA_SCHEMA_ID);
if (allObjectTypes.length === 0) {
console.error('❌ No object types found in schema');
process.exit(1);
}
console.log(` Found ${allObjectTypes.length} object types`);
console.log('');
// Build a map of all type IDs to names for reference resolution
const typeConfigs = new Map<number, { name: string; typeName: string }>();
for (const ot of allObjectTypes) {
typeConfigs.set(ot.id, {
name: ot.name,
typeName: toPascalCase(ot.name),
});
}
// Fetch attributes for each object type
console.log('🔍 Fetching attributes for each object type:');
console.log('');
const generatedTypes: GeneratedObjectType[] = [];
let totalAttributes = 0;
for (const objType of allObjectTypes) {
const typeName = toPascalCase(objType.name);
process.stdout.write(` ${objType.name.padEnd(50)} `);
const attributes = await fetcher.fetchAttributes(objType.id);
if (attributes.length === 0) {
console.log('⚠️ (no attributes)');
continue;
}
const parsedAttributes = attributes.map(attr => parseAttribute(attr, typeConfigs));
const syncPriority = determineSyncPriority(objType.name, objType.objectCount || 0);
generatedTypes.push({
jiraTypeId: objType.id,
name: objType.name,
typeName,
syncPriority,
objectCount: objType.objectCount || 0,
attributes: parsedAttributes,
});
totalAttributes += attributes.length;
console.log(`${attributes.length} attributes`);
}
console.log('');
console.log(`📊 Summary: ${generatedTypes.length} types, ${totalAttributes} attributes total`);
console.log('');
// Sort by sync priority
generatedTypes.sort((a, b) => a.syncPriority - b.syncPriority);
// Generate files
console.log('📝 Generating output files:');
console.log('');
// 1. TypeScript types
const typesContent = generateTypesFile(generatedTypes, generatedAt);
const typesPath = path.join(OUTPUT_DIR, 'jira-types.ts');
fs.writeFileSync(typesPath, typesContent, 'utf-8');
console.log(`${typesPath}`);
// 2. Schema metadata
const schemaContent = generateSchemaFile(generatedTypes, generatedAt);
const schemaPath = path.join(OUTPUT_DIR, 'jira-schema.ts');
fs.writeFileSync(schemaPath, schemaContent, 'utf-8');
console.log(`${schemaPath}`);
// 3. Database DDL
const dbContent = generateDatabaseSchema(generatedAt);
const dbPath = path.join(OUTPUT_DIR, 'db-schema.sql');
fs.writeFileSync(dbPath, dbContent, 'utf-8');
console.log(`${dbPath}`);
console.log('');
console.log('╔════════════════════════════════════════════════════════════════╗');
console.log('║ Schema generation complete! ║');
console.log('╚════════════════════════════════════════════════════════════════╝');
console.log('');
console.log(`Generated at: ${generatedAt.toISOString()}`);
console.log(`Object types: ${generatedTypes.length}`);
console.log(`Attributes: ${totalAttributes}`);
console.log('');
console.log('Next steps:');
console.log(' 1. Review the generated files in src/generated/');
console.log(' 2. Rebuild the application: npm run build');
console.log(' 3. Restart the server to pick up the new schema');
console.log('');
}
// Run
main().catch(error => {
console.error('');
console.error('❌ Schema generation failed:', error);
console.error('');
process.exit(1);
});

View File

@@ -1,17 +1,42 @@
import dotenv from 'dotenv';
import path from 'path';
import fs from 'fs';
import { fileURLToPath } from 'url';
// Load .env from project root
dotenv.config({ path: path.resolve(__dirname, '../../../.env') });
// Get __dirname equivalent for ES modules
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Try multiple possible .env locations (handles both tsx watch and compiled dist)
const possibleEnvPaths = [
path.resolve(process.cwd(), '.env'), // Project root from cwd
path.resolve(__dirname, '../../../.env'), // From src/config/ to project root
path.resolve(__dirname, '../../../../.env'), // From dist/config/ to project root
];
for (const envPath of possibleEnvPaths) {
if (fs.existsSync(envPath)) {
dotenv.config({ path: envPath });
console.log(`Environment loaded from: ${envPath}`);
break;
}
}
// Authentication method type
export type JiraAuthMethod = 'pat' | 'oauth';
interface Config {
// Jira Assets
jiraHost: string;
jiraPat: string;
jiraSchemaId: string;
// Jira OAuth 2.0 Configuration
jiraOAuthEnabled: boolean;
// Jira Authentication Method ('pat' or 'oauth')
jiraAuthMethod: JiraAuthMethod;
// Jira Personal Access Token (used when jiraAuthMethod = 'pat')
jiraPat: string;
// Jira OAuth 2.0 Configuration (used when jiraAuthMethod = 'oauth')
jiraOAuthClientId: string;
jiraOAuthClientSecret: string;
jiraOAuthCallbackUrl: string;
@@ -20,38 +45,6 @@ interface Config {
// Session Configuration
sessionSecret: string;
// Object Type IDs
jiraApplicationComponentTypeId: string;
jiraApplicationFunctionTypeId: string;
jiraDynamicsFactorTypeId: string;
jiraComplexityFactorTypeId: string;
jiraNumberOfUsersTypeId: string;
jiraGovernanceModelTypeId: string;
jiraApplicationClusterTypeId: string;
jiraApplicationTypeTypeId: string;
jiraHostingTypeTypeId: string;
jiraBusinessImpactAnalyseTypeId: string;
jiraApplicationManagementHostingTypeId: string; // Object Type ID for "Application Management - Hosting"
jiraApplicationManagementTAMTypeId: string; // Object Type ID for "Application Management - TAM"
// Attribute IDs
jiraAttrApplicationFunction: string;
jiraAttrDynamicsFactor: string;
jiraAttrComplexityFactor: string;
jiraAttrNumberOfUsers: string;
jiraAttrGovernanceModel: string;
jiraAttrApplicationCluster: string;
jiraAttrApplicationType: string;
jiraAttrPlatform: string;
jiraAttrHostingType: string;
jiraAttrBusinessImpactAnalyse: string;
jiraAttrTechnischeArchitectuur: string; // Attribute ID for "Technische Architectuur (TA)"
jiraAttrTechnicalApplicationManagementPrimary: string; // Attribute ID for "Technical Application Management Primary"
jiraAttrTechnicalApplicationManagementSecondary: string; // Attribute ID for "Technical Application Management Secondary"
jiraAttrOverrideFTE: string; // Attribute ID for "Application Management - Override FTE"
jiraAttrApplicationManagementHosting: string; // Attribute ID for "Application Management - Hosting" (4939)
jiraAttrApplicationManagementTAM: string; // Attribute ID for "Application Management - TAM" (4945)
// AI API Keys
anthropicApiKey: string;
openaiApiKey: string;
@@ -72,26 +65,44 @@ interface Config {
jiraApiBatchSize: number;
}
function getEnvVar(name: string, defaultValue?: string): string {
const value = process.env[name] || defaultValue;
if (!value) {
throw new Error(`Environment variable ${name} is required but not set`);
}
return value;
}
function getOptionalEnvVar(name: string, defaultValue: string = ''): string {
return process.env[name] || defaultValue;
}
// Helper to determine auth method with backward compatibility
function getJiraAuthMethod(): JiraAuthMethod {
// Check new JIRA_AUTH_METHOD first
const authMethod = getOptionalEnvVar('JIRA_AUTH_METHOD', '').toLowerCase();
if (authMethod === 'oauth') return 'oauth';
if (authMethod === 'pat') return 'pat';
// Backward compatibility: check JIRA_OAUTH_ENABLED
const oauthEnabled = getOptionalEnvVar('JIRA_OAUTH_ENABLED', 'false').toLowerCase() === 'true';
if (oauthEnabled) return 'oauth';
// Default to 'pat' if JIRA_PAT is set, otherwise 'oauth' if OAuth credentials exist
const hasPat = !!getOptionalEnvVar('JIRA_PAT');
const hasOAuthCredentials = !!getOptionalEnvVar('JIRA_OAUTH_CLIENT_ID') && !!getOptionalEnvVar('JIRA_OAUTH_CLIENT_SECRET');
if (hasPat) return 'pat';
if (hasOAuthCredentials) return 'oauth';
// Default to 'pat' (will show warning during validation)
return 'pat';
}
export const config: Config = {
// Jira Assets
jiraHost: getOptionalEnvVar('JIRA_HOST', 'https://jira.zuyderland.nl'),
jiraPat: getOptionalEnvVar('JIRA_PAT'),
jiraSchemaId: getOptionalEnvVar('JIRA_SCHEMA_ID'),
// Jira OAuth 2.0 Configuration
jiraOAuthEnabled: getOptionalEnvVar('JIRA_OAUTH_ENABLED', 'false').toLowerCase() === 'true',
// Jira Authentication Method
jiraAuthMethod: getJiraAuthMethod(),
// Jira Personal Access Token (for PAT authentication)
jiraPat: getOptionalEnvVar('JIRA_PAT'),
// Jira OAuth 2.0 Configuration (for OAuth authentication)
jiraOAuthClientId: getOptionalEnvVar('JIRA_OAUTH_CLIENT_ID'),
jiraOAuthClientSecret: getOptionalEnvVar('JIRA_OAUTH_CLIENT_SECRET'),
jiraOAuthCallbackUrl: getOptionalEnvVar('JIRA_OAUTH_CALLBACK_URL', 'http://localhost:3001/api/auth/callback'),
@@ -100,38 +111,6 @@ export const config: Config = {
// Session Configuration
sessionSecret: getOptionalEnvVar('SESSION_SECRET', 'change-this-secret-in-production'),
// Object Type IDs
jiraApplicationComponentTypeId: getOptionalEnvVar('JIRA_APPLICATION_COMPONENT_TYPE_ID'),
jiraApplicationFunctionTypeId: getOptionalEnvVar('JIRA_APPLICATION_FUNCTION_TYPE_ID'),
jiraDynamicsFactorTypeId: getOptionalEnvVar('JIRA_DYNAMICS_FACTOR_TYPE_ID'),
jiraComplexityFactorTypeId: getOptionalEnvVar('JIRA_COMPLEXITY_FACTOR_TYPE_ID'),
jiraNumberOfUsersTypeId: getOptionalEnvVar('JIRA_NUMBER_OF_USERS_TYPE_ID'),
jiraGovernanceModelTypeId: getOptionalEnvVar('JIRA_GOVERNANCE_MODEL_TYPE_ID'),
jiraApplicationClusterTypeId: getOptionalEnvVar('JIRA_APPLICATION_CLUSTER_TYPE_ID'),
jiraApplicationTypeTypeId: getOptionalEnvVar('JIRA_APPLICATION_TYPE_TYPE_ID'),
jiraHostingTypeTypeId: getOptionalEnvVar('JIRA_HOSTING_TYPE_TYPE_ID', '39'),
jiraBusinessImpactAnalyseTypeId: getOptionalEnvVar('JIRA_BUSINESS_IMPACT_ANALYSE_TYPE_ID', '41'),
jiraApplicationManagementHostingTypeId: getOptionalEnvVar('JIRA_APPLICATION_MANAGEMENT_HOSTING_TYPE_ID', '438'),
jiraApplicationManagementTAMTypeId: getOptionalEnvVar('JIRA_APPLICATION_MANAGEMENT_TAM_TYPE_ID', '439'),
// Attribute IDs
jiraAttrApplicationFunction: getOptionalEnvVar('JIRA_ATTR_APPLICATION_FUNCTION'),
jiraAttrDynamicsFactor: getOptionalEnvVar('JIRA_ATTR_DYNAMICS_FACTOR'),
jiraAttrComplexityFactor: getOptionalEnvVar('JIRA_ATTR_COMPLEXITY_FACTOR'),
jiraAttrNumberOfUsers: getOptionalEnvVar('JIRA_ATTR_NUMBER_OF_USERS'),
jiraAttrGovernanceModel: getOptionalEnvVar('JIRA_ATTR_GOVERNANCE_MODEL'),
jiraAttrApplicationCluster: getOptionalEnvVar('JIRA_ATTR_APPLICATION_CLUSTER'),
jiraAttrApplicationType: getOptionalEnvVar('JIRA_ATTR_APPLICATION_TYPE'),
jiraAttrPlatform: getOptionalEnvVar('JIRA_ATTR_PLATFORM'),
jiraAttrHostingType: getOptionalEnvVar('JIRA_ATTR_HOSTING_TYPE', '355'),
jiraAttrBusinessImpactAnalyse: getOptionalEnvVar('JIRA_ATTR_BUSINESS_IMPACT_ANALYSE', '368'),
jiraAttrTechnischeArchitectuur: getOptionalEnvVar('JIRA_ATTR_TECHNISCHE_ARCHITECTUUR', '572'),
jiraAttrTechnicalApplicationManagementPrimary: getOptionalEnvVar('JIRA_ATTR_TECHNICAL_APPLICATION_MANAGEMENT_PRIMARY', '377'),
jiraAttrTechnicalApplicationManagementSecondary: getOptionalEnvVar('JIRA_ATTR_TECHNICAL_APPLICATION_MANAGEMENT_SECONDARY', '1330'),
jiraAttrOverrideFTE: getOptionalEnvVar('JIRA_ATTR_OVERRIDE_FTE', '4932'),
jiraAttrApplicationManagementHosting: getOptionalEnvVar('JIRA_ATTR_APPLICATION_MANAGEMENT_HOSTING', '4939'),
jiraAttrApplicationManagementTAM: getOptionalEnvVar('JIRA_ATTR_APPLICATION_MANAGEMENT_TAM', '4945'),
// AI API Keys
anthropicApiKey: getOptionalEnvVar('ANTHROPIC_API_KEY'),
openaiApiKey: getOptionalEnvVar('OPENAI_API_KEY'),
@@ -154,10 +133,34 @@ export const config: Config = {
export function validateConfig(): void {
const missingVars: string[] = [];
const warnings: string[] = [];
if (!config.jiraPat) missingVars.push('JIRA_PAT');
// Validate authentication configuration based on selected method
console.log(`Jira Authentication Method: ${config.jiraAuthMethod.toUpperCase()}`);
if (config.jiraAuthMethod === 'pat') {
if (!config.jiraPat) {
missingVars.push('JIRA_PAT (required for PAT authentication)');
}
} else if (config.jiraAuthMethod === 'oauth') {
if (!config.jiraOAuthClientId) {
missingVars.push('JIRA_OAUTH_CLIENT_ID (required for OAuth authentication)');
}
if (!config.jiraOAuthClientSecret) {
missingVars.push('JIRA_OAUTH_CLIENT_SECRET (required for OAuth authentication)');
}
if (!config.sessionSecret || config.sessionSecret === 'change-this-secret-in-production') {
warnings.push('SESSION_SECRET should be set to a secure random value for OAuth sessions');
}
}
// General required config
if (!config.jiraSchemaId) missingVars.push('JIRA_SCHEMA_ID');
if (!config.anthropicApiKey) missingVars.push('ANTHROPIC_API_KEY');
if (!config.anthropicApiKey) warnings.push('ANTHROPIC_API_KEY not set - AI classification disabled');
if (warnings.length > 0) {
warnings.forEach(w => console.warn(`Warning: ${w}`));
}
if (missingVars.length > 0) {
console.warn(`Warning: Missing environment variables: ${missingVars.join(', ')}`);

View File

@@ -0,0 +1,54 @@
-- AUTO-GENERATED FILE - DO NOT EDIT MANUALLY
-- Generated from Jira Assets Schema via REST API
-- Generated at: 2026-01-09T02:12:50.973Z
--
-- Re-generate with: npm run generate-schema
-- =============================================================================
-- Core Tables
-- =============================================================================
-- Cached CMDB objects (all types stored in single table with JSON data)
CREATE TABLE IF NOT EXISTS cached_objects (
id TEXT PRIMARY KEY,
object_key TEXT NOT NULL UNIQUE,
object_type TEXT NOT NULL,
label TEXT NOT NULL,
data JSON NOT NULL,
jira_updated_at TEXT,
jira_created_at TEXT,
cached_at TEXT NOT NULL
);
-- Object relations (references between objects)
CREATE TABLE IF NOT EXISTS object_relations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source_id TEXT NOT NULL,
target_id TEXT NOT NULL,
attribute_name TEXT NOT NULL,
source_type TEXT NOT NULL,
target_type TEXT NOT NULL,
UNIQUE(source_id, target_id, attribute_name)
);
-- Sync metadata (tracks sync state)
CREATE TABLE IF NOT EXISTS sync_metadata (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at TEXT NOT NULL
);
-- =============================================================================
-- Indices for Performance
-- =============================================================================
CREATE INDEX IF NOT EXISTS idx_objects_type ON cached_objects(object_type);
CREATE INDEX IF NOT EXISTS idx_objects_key ON cached_objects(object_key);
CREATE INDEX IF NOT EXISTS idx_objects_updated ON cached_objects(jira_updated_at);
CREATE INDEX IF NOT EXISTS idx_objects_label ON cached_objects(label);
CREATE INDEX IF NOT EXISTS idx_relations_source ON object_relations(source_id);
CREATE INDEX IF NOT EXISTS idx_relations_target ON object_relations(target_id);
CREATE INDEX IF NOT EXISTS idx_relations_source_type ON object_relations(source_type);
CREATE INDEX IF NOT EXISTS idx_relations_target_type ON object_relations(target_type);
CREATE INDEX IF NOT EXISTS idx_relations_attr ON object_relations(attribute_name);

View File

@@ -0,0 +1,894 @@
// AUTO-GENERATED FILE - DO NOT EDIT MANUALLY
// Generated from Jira Assets Schema via REST API
// Generated at: 2026-01-09T02:12:50.973Z
//
// Re-generate with: npm run generate-schema
// =============================================================================
// Schema Type Definitions
// =============================================================================
export interface AttributeDefinition {
jiraId: number;
name: string;
fieldName: string;
type: 'text' | 'integer' | 'float' | 'boolean' | 'date' | 'datetime' | 'select' | 'reference' | 'url' | 'email' | 'textarea' | 'user' | 'status' | 'unknown';
isMultiple: boolean;
isEditable: boolean;
isRequired: boolean;
isSystem: boolean;
referenceTypeId?: number;
referenceTypeName?: string;
description?: string;
}
export interface ObjectTypeDefinition {
jiraTypeId: number;
name: string;
typeName: string;
syncPriority: number;
objectCount: number;
attributes: AttributeDefinition[];
}
// =============================================================================
// Schema Metadata
// =============================================================================
export const SCHEMA_GENERATED_AT = '2026-01-09T02:12:50.973Z';
export const SCHEMA_OBJECT_TYPE_COUNT = 35;
export const SCHEMA_TOTAL_ATTRIBUTES = 365;
// =============================================================================
// Object Type Definitions
// =============================================================================
export const OBJECT_TYPES: Record<string, ObjectTypeDefinition> = {
'ApplicationComponent': {
jiraTypeId: 38,
name: 'Application Component',
typeName: 'ApplicationComponent',
syncPriority: 1,
objectCount: 596,
attributes: [
{ jiraId: 569, name: 'Reference', fieldName: 'reference', type: 'text', isMultiple: false, isEditable: false, isRequired: false, isSystem: false, description: 'Niet aanpassen. GUID - Enterprise Architect' },
{ jiraId: 341, name: 'Key', fieldName: 'key', type: 'text', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 425, name: 'SearchReference', fieldName: 'searchReference', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false, description: 'Additionele zoekwoorden t.b.v. search' },
{ jiraId: 342, name: 'Name', fieldName: 'name', type: 'text', isMultiple: false, isEditable: true, isRequired: true, isSystem: false, description: 'Unieke naam object' },
{ jiraId: 343, name: 'Created', fieldName: 'created', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 344, name: 'Updated', fieldName: 'updated', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 354, name: 'Description', fieldName: 'description', type: 'select', isMultiple: false, isEditable: true, isRequired: false, isSystem: false, description: '* Application description' },
{ jiraId: 4538, name: 'Organisation', fieldName: 'organisation', type: 'reference', isMultiple: false, isEditable: true, isRequired: false, isSystem: false, referenceTypeId: 390, referenceTypeName: 'Organisation' },
{ jiraId: 4666, name: 'ApplicationFunction', fieldName: 'applicationFunction', type: 'reference', isMultiple: true, isEditable: true, isRequired: false, isSystem: false, referenceTypeId: 403, referenceTypeName: 'ApplicationFunction' },
{ jiraId: 2416, name: 'Status', fieldName: 'status', type: 'email', isMultiple: false, isEditable: true, isRequired: false, isSystem: false, description: 'Application Lifecycle Management' },
{ jiraId: 416, name: 'Confluence Space', fieldName: 'confluenceSpace', type: 'float', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 399, name: 'Business Importance', fieldName: 'businessImportance', type: 'reference', isMultiple: false, isEditable: true, isRequired: false, isSystem: false, referenceTypeId: 44, referenceTypeName: 'BusinessImportance' },
{ jiraId: 4540, name: 'Zenya ID', fieldName: 'zenyaID', type: 'integer', isMultiple: false, isEditable: false, isRequired: false, isSystem: false },
{ jiraId: 4615, name: 'Zenya URL', fieldName: 'zenyaURL', type: 'email', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 368, name: 'Business Impact Analyse', fieldName: 'businessImpactAnalyse', type: 'reference', isMultiple: false, isEditable: true, isRequired: false, isSystem: false, referenceTypeId: 41, referenceTypeName: 'BusinessImpactAnalyse' },
{ jiraId: 355, name: 'Application Component Hosting Type', fieldName: 'applicationComponentHostingType', type: 'reference', isMultiple: false, isEditable: true, isRequired: false, isSystem: false, referenceTypeId: 39, referenceTypeName: 'HostingType' },
{ jiraId: 394, name: 'CustomDevelopment', fieldName: 'customDevelopment', type: 'boolean', isMultiple: false, isEditable: true, isRequired: false, isSystem: false, description: 'Is er sprake van eigen programmatuur?' },
{ jiraId: 4927, name: 'Platform', fieldName: 'platform', type: 'reference', isMultiple: false, isEditable: true, isRequired: false, isSystem: false, referenceTypeId: 38, referenceTypeName: 'ApplicationComponent' },
{ jiraId: 358, name: 'Referenced Application Component', fieldName: 'referencedApplicationComponent', type: 'reference', isMultiple: true, isEditable: true, isRequired: false, isSystem: false, referenceTypeId: 38, referenceTypeName: 'ApplicationComponent', description: 'Welk application component maakt onderdeel uit van een ander Application Component' },
{ jiraId: 359, name: 'Authentication Method', fieldName: 'authenticationMethod', type: 'reference', isMultiple: false, isEditable: false, isRequired: false, isSystem: false, referenceTypeId: 38, referenceTypeName: 'ApplicationComponent', description: '*HIDDEN* zie CMDB-488' },
{ jiraId: 362, name: 'Monitoring', fieldName: 'monitoring', type: 'reference', isMultiple: false, isEditable: true, isRequired: false, isSystem: false, referenceTypeId: 38, referenceTypeName: 'ApplicationComponent', description: 'Cross reference naar Application Component' },
{ jiraId: 373, name: 'PII Data', fieldName: 'piiData', type: 'boolean', isMultiple: false, isEditable: true, isRequired: false, isSystem: false, description: 'Maakt applicatie gebruik van Persoonlijk identificeerbare informatie?' },
{ jiraId: 374, name: 'Medical Data', fieldName: 'medicalData', type: 'boolean', isMultiple: false, isEditable: true, isRequired: false, isSystem: false, description: 'Maakt de Application Component gebruik van medische data?' },
{ jiraId: 363, name: 'Supplier Product', fieldName: 'supplierProduct', type: 'reference', isMultiple: false, isEditable: true, isRequired: false, isSystem: false, referenceTypeId: 37, referenceTypeName: 'Supplier', description: 'Wie is de leverancier van de Application Component?' },
{ jiraId: 364, name: 'Supplier Technical', fieldName: 'supplierTechnical', type: 'reference', isMultiple: false, isEditable: true, isRequired: false, isSystem: false, referenceTypeId: 37, referenceTypeName: 'Supplier' },
{ jiraId: 365, name: 'Supplier Implementation', fieldName: 'supplierImplementation', type: 'reference', isMultiple: false, isEditable: true, isRequired: false, isSystem: false, referenceTypeId: 37, referenceTypeName: 'Supplier', description: 'Wie is leverancier van de implementatie?' },
{ jiraId: 366, name: 'Supplier Consultancy', fieldName: 'supplierConsultancy', type: 'reference', isMultiple: false, isEditable: true, isRequired: false, isSystem: false, referenceTypeId: 37, referenceTypeName: 'Supplier' },
{ jiraId: 2365, name: 'Business Owner', fieldName: 'businessOwner', type: 'reference', isMultiple: false, isEditable: true, isRequired: false, isSystem: false, referenceTypeId: 243, referenceTypeName: 'Organization Unit', description: 'Verantwoordelijk voor waarde en bedrijfsvoering van het systeem' },
{ jiraId: 2366, name: 'System Owner', fieldName: 'systemOwner', type: 'reference', isMultiple: false, isEditable: true, isRequired: false, isSystem: false, referenceTypeId: 103, referenceTypeName: 'User', description: 'Verantwoordelijk voor technische werking en beheer van het systeem.' },
{ jiraId: 2371, name: 'Functional Application Management', fieldName: 'functionalApplicationManagement', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4613, name: 'Technical Application Management', fieldName: 'technicalApplicationManagement', type: 'reference', isMultiple: false, isEditable: true, isRequired: false, isSystem: false, referenceTypeId: 400, referenceTypeName: 'group' },
{ jiraId: 377, name: 'Technical Application Management Primary', fieldName: 'technicalApplicationManagementPrimary', type: 'boolean', isMultiple: true, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 1330, name: 'Technical Application Management Secondary', fieldName: 'technicalApplicationManagementSecondary', type: 'boolean', isMultiple: true, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 1331, name: 'Medische Techniek', fieldName: 'medischeTechniek', type: 'boolean', isMultiple: false, isEditable: true, isRequired: false, isSystem: false, description: 'Is er een link met medische techniek' },
{ jiraId: 572, name: 'Technische Architectuur (TA)', fieldName: 'technischeArchitectuurTA', type: 'email', isMultiple: false, isEditable: true, isRequired: false, isSystem: false, description: 'Komt uit Enterprise Architect mee' },
{ jiraId: 4497, name: 'Measures', fieldName: 'measures', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4749, name: 'GenerateConfluenceSpace', fieldName: 'generateConfluenceSpace', type: 'boolean', isMultiple: false, isEditable: false, isRequired: false, isSystem: false, description: 'Wordt gebruikt door script om space te genereren - niet verwijderen. Attribuut is hidden' },
{ jiraId: 4793, name: 'SourceStatus', fieldName: 'sourceStatus', type: 'reference', isMultiple: false, isEditable: false, isRequired: false, isSystem: false },
{ jiraId: 4799, name: 'ImportDate', fieldName: 'importDate', type: 'date', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4906, name: 'ICT Governance Model', fieldName: 'ictGovernanceModel', type: 'reference', isMultiple: false, isEditable: true, isRequired: false, isSystem: false, referenceTypeId: 431, referenceTypeName: 'IctGovernanceModel' },
{ jiraId: 4918, name: 'Application Management - Application Type', fieldName: 'applicationManagementApplicationType', type: 'reference', isMultiple: false, isEditable: true, isRequired: false, isSystem: false, referenceTypeId: 436, referenceTypeName: 'ApplicationManagementApplicationType', description: 'De Type Classificatie bepaalt de aard en scope van het IT-object.' },
{ jiraId: 4939, name: 'Application Management - Hosting', fieldName: 'applicationManagementHosting', type: 'reference', isMultiple: false, isEditable: true, isRequired: false, isSystem: false, referenceTypeId: 438, referenceTypeName: 'ApplicationManagementHosting', description: 'Het Hosting-veld geeft aan waar de infrastructuur draait. Dit bepaalt mede de technische verantwoordelijkheden en compliance-eisen.' },
{ jiraId: 4945, name: 'Application Management - TAM', fieldName: 'applicationManagementTAM', type: 'reference', isMultiple: false, isEditable: true, isRequired: false, isSystem: false, referenceTypeId: 439, referenceTypeName: 'ApplicationManagementTam', description: 'Dit veld geeft aan wie het technisch applicatiebeheer uitvoert. Dit is de primaire factor voor het bepalen van het regiemodel.' },
{ jiraId: 4903, name: 'Application Management - Dynamics Factor', fieldName: 'applicationManagementDynamicsFactor', type: 'reference', isMultiple: false, isEditable: true, isRequired: false, isSystem: false, referenceTypeId: 434, referenceTypeName: 'ApplicationManagementDynamicsFactor' },
{ jiraId: 4904, name: 'Application Management - Complexity Factor', fieldName: 'applicationManagementComplexityFactor', type: 'reference', isMultiple: false, isEditable: true, isRequired: false, isSystem: false, referenceTypeId: 432, referenceTypeName: 'ApplicationManagementComplexityFactor' },
{ jiraId: 4905, name: 'Application Management - Number of Users', fieldName: 'applicationManagementNumberOfUsers', type: 'reference', isMultiple: false, isEditable: true, isRequired: false, isSystem: false, referenceTypeId: 433, referenceTypeName: 'ApplicationManagementNumberOfUsers' },
{ jiraId: 4911, name: 'Application Management - Subteam', fieldName: 'applicationManagementSubteam', type: 'reference', isMultiple: false, isEditable: true, isRequired: false, isSystem: false, referenceTypeId: 435, referenceTypeName: 'ApplicationManagementSubteam' },
{ jiraId: 4932, name: 'Application Management - Override FTE', fieldName: 'applicationManagementOverrideFTE', type: 'float', isMultiple: false, isEditable: true, isRequired: false, isSystem: false }
],
},
'Flows': {
jiraTypeId: 59,
name: 'Flows',
typeName: 'Flows',
syncPriority: 1,
objectCount: 903,
attributes: [
{ jiraId: 1104, name: 'Reference', fieldName: 'reference', type: 'text', isMultiple: false, isEditable: false, isRequired: false, isSystem: false },
{ jiraId: 1105, name: 'Present In Import Enterprise Architect', fieldName: 'presentInImportEnterpriseArchitect', type: 'boolean', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 558, name: 'Key', fieldName: 'key', type: 'text', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 559, name: 'Name', fieldName: 'name', type: 'text', isMultiple: false, isEditable: true, isRequired: true, isSystem: false, description: 'The name of the object' },
{ jiraId: 567, name: 'Source', fieldName: 'source', type: 'reference', isMultiple: false, isEditable: true, isRequired: false, isSystem: false, referenceTypeId: 38, referenceTypeName: 'ApplicationComponent', description: 'Application Component - Source' },
{ jiraId: 4535, name: 'Search Reference Source', fieldName: 'searchReferenceSource', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 568, name: 'Target', fieldName: 'target', type: 'reference', isMultiple: false, isEditable: true, isRequired: false, isSystem: false, referenceTypeId: 38, referenceTypeName: 'ApplicationComponent', description: 'Application Component - Target' },
{ jiraId: 4536, name: 'Search Reference Target', fieldName: 'searchReferenceTarget', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 564, name: 'Type', fieldName: 'type', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 565, name: 'Protocol', fieldName: 'protocol', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 566, name: 'Details', fieldName: 'details', type: 'select', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 570, name: 'Broker', fieldName: 'broker', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 571, name: 'Status', fieldName: 'status', type: 'email', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 560, name: 'Created', fieldName: 'created', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 561, name: 'Updated', fieldName: 'updated', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4801, name: 'ImportDate', fieldName: 'importDate', type: 'date', isMultiple: false, isEditable: true, isRequired: false, isSystem: false }
],
},
'Server': {
jiraTypeId: 48,
name: 'Server',
typeName: 'Server',
syncPriority: 1,
objectCount: 909,
attributes: [
{ jiraId: 417, name: 'Key', fieldName: 'key', type: 'text', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 418, name: 'Name', fieldName: 'name', type: 'text', isMultiple: false, isEditable: true, isRequired: true, isSystem: false, description: 'The name of the object' },
{ jiraId: 450, name: 'Description', fieldName: 'description', type: 'select', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 441, name: 'Status', fieldName: 'status', type: 'email', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4500, name: 'VMLocation', fieldName: 'vMLocation', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4501, name: 'Application Component', fieldName: 'applicationComponent', type: 'reference', isMultiple: false, isEditable: true, isRequired: false, isSystem: false, referenceTypeId: 38, referenceTypeName: 'ApplicationComponent' },
{ jiraId: 4762, name: 'AzureSubscription', fieldName: 'azureSubscription', type: 'reference', isMultiple: true, isEditable: true, isRequired: false, isSystem: false, referenceTypeId: 418, referenceTypeName: 'AzureSubscription' },
{ jiraId: 455, name: 'VMOSType', fieldName: 'vMOSType', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 2406, name: 'Reboot group', fieldName: 'rebootGroup', type: 'reference', isMultiple: false, isEditable: true, isRequired: false, isSystem: false, referenceTypeId: 251, referenceTypeName: 'Rebootgroups' },
{ jiraId: 444, name: 'MemoryMB', fieldName: 'memoryMB', type: 'integer', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 2383, name: 'CPUCores', fieldName: 'cPUCores', type: 'integer', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 2384, name: 'NICCount', fieldName: 'nICCount', type: 'integer', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 2385, name: 'DataDisks', fieldName: 'dataDisks', type: 'integer', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4720, name: 'PrivateIPAddress', fieldName: 'privateIPAddress', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4764, name: 'Cluster', fieldName: 'cluster', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4765, name: 'VMSize', fieldName: 'vMSize', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 419, name: 'Created', fieldName: 'created', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 420, name: 'Updated', fieldName: 'updated', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4592, name: 'DBA', fieldName: 'dBA', type: 'boolean', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4792, name: 'SourceStatus', fieldName: 'sourceStatus', type: 'reference', isMultiple: false, isEditable: false, isRequired: false, isSystem: false },
{ jiraId: 4802, name: 'ImportDate', fieldName: 'importDate', type: 'date', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4873, name: 'State', fieldName: 'state', type: 'email', isMultiple: false, isEditable: true, isRequired: false, isSystem: false }
],
},
'AzureSubscription': {
jiraTypeId: 418,
name: 'AzureSubscription',
typeName: 'AzureSubscription',
syncPriority: 2,
objectCount: 151,
attributes: [
{ jiraId: 4755, name: 'Key', fieldName: 'key', type: 'text', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4756, name: 'Name', fieldName: 'name', type: 'text', isMultiple: false, isEditable: true, isRequired: true, isSystem: false, description: 'The name of the subscription' },
{ jiraId: 4761, name: 'Status', fieldName: 'status', type: 'email', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4760, name: 'Server', fieldName: 'server', type: 'reference', isMultiple: true, isEditable: true, isRequired: false, isSystem: false, referenceTypeId: 48, referenceTypeName: 'Server' },
{ jiraId: 4759, name: 'ApplicationComponent', fieldName: 'applicationComponent', type: 'reference', isMultiple: false, isEditable: true, isRequired: false, isSystem: false, referenceTypeId: 38, referenceTypeName: 'ApplicationComponent' },
{ jiraId: 4757, name: 'Created', fieldName: 'created', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4758, name: 'Updated', fieldName: 'updated', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4803, name: 'ImportDate', fieldName: 'importDate', type: 'date', isMultiple: false, isEditable: true, isRequired: false, isSystem: false }
],
},
'PackageBuild': {
jiraTypeId: 424,
name: 'PackageBuild',
typeName: 'PackageBuild',
syncPriority: 2,
objectCount: 496,
attributes: [
{ jiraId: 4816, name: 'Key', fieldName: 'key', type: 'text', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4817, name: 'Name', fieldName: 'name', type: 'text', isMultiple: false, isEditable: true, isRequired: true, isSystem: false, description: 'naamgeving: software-versie' },
{ jiraId: 4820, name: 'Software', fieldName: 'software', type: 'reference', isMultiple: false, isEditable: true, isRequired: true, isSystem: false, referenceTypeId: 421, referenceTypeName: 'Software' },
{ jiraId: 4821, name: 'Version', fieldName: 'version', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4835, name: 'Status', fieldName: 'status', type: 'reference', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4818, name: 'Created', fieldName: 'created', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4819, name: 'Updated', fieldName: 'updated', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4839, name: 'Description', fieldName: 'description', type: 'select', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4824, name: 'BuildNumber', fieldName: 'buildNumber', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4825, name: 'Architecture', fieldName: 'architecture', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4826, name: 'Environment', fieldName: 'environment', type: 'reference', isMultiple: true, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4827, name: 'Language', fieldName: 'language', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4828, name: 'AppDelivery', fieldName: 'appDelivery', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4829, name: 'ADlocalMemberships', fieldName: 'aDlocalMemberships', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4830, name: 'ADmemberships', fieldName: 'aDmemberships', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4831, name: 'EntraIDmemberships', fieldName: 'entraIDmemberships', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4832, name: 'Platform', fieldName: 'platform', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4833, name: 'InGoldenImage', fieldName: 'inGoldenImage', type: 'boolean', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4837, name: 'ImportDate', fieldName: 'importDate', type: 'date', isMultiple: false, isEditable: true, isRequired: false, isSystem: false }
],
},
'Package': {
jiraTypeId: 422,
name: 'Package',
typeName: 'Package',
syncPriority: 2,
objectCount: 299,
attributes: [
{ jiraId: 4806, name: 'Key', fieldName: 'key', type: 'text', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4807, name: 'Name', fieldName: 'name', type: 'text', isMultiple: false, isEditable: true, isRequired: true, isSystem: false, description: 'The name of the object' },
{ jiraId: 4808, name: 'Created', fieldName: 'created', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4809, name: 'Updated', fieldName: 'updated', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4836, name: 'Description', fieldName: 'description', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4810, name: 'ApplicationComponent', fieldName: 'applicationComponent', type: 'reference', isMultiple: false, isEditable: true, isRequired: false, isSystem: false, referenceTypeId: 38, referenceTypeName: 'ApplicationComponent' },
{ jiraId: 4811, name: 'Status', fieldName: 'status', type: 'email', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4812, name: 'SupplierProduct', fieldName: 'supplierProduct', type: 'reference', isMultiple: false, isEditable: true, isRequired: false, isSystem: false, referenceTypeId: 37, referenceTypeName: 'Supplier' },
{ jiraId: 4813, name: 'SupplierTechnical', fieldName: 'supplierTechnical', type: 'reference', isMultiple: false, isEditable: true, isRequired: false, isSystem: false, referenceTypeId: 37, referenceTypeName: 'Supplier' },
{ jiraId: 4814, name: 'BusinessImportance', fieldName: 'businessImportance', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4815, name: 'Authentication', fieldName: 'authentication', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4834, name: 'MaintenanceContract', fieldName: 'maintenanceContract', type: 'boolean', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4838, name: 'ImportDate', fieldName: 'importDate', type: 'date', isMultiple: false, isEditable: true, isRequired: false, isSystem: false }
],
},
'Certificate': {
jiraTypeId: 406,
name: 'Certificate',
typeName: 'Certificate',
syncPriority: 2,
objectCount: 508,
attributes: [
{ jiraId: 4675, name: 'Key', fieldName: 'key', type: 'text', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4676, name: 'Name', fieldName: 'name', type: 'text', isMultiple: false, isEditable: true, isRequired: true, isSystem: false, description: 'Naam van het certificaat' },
{ jiraId: 4686, name: 'Status', fieldName: 'status', type: 'email', isMultiple: false, isEditable: true, isRequired: true, isSystem: false, description: 'Status is Active, Closed, Unknown (date 1970-01-01) or Support Requested (ticket aangemaakt vanwege Expiry Date)' },
{ jiraId: 4695, name: 'Type', fieldName: 'type', type: 'reference', isMultiple: false, isEditable: true, isRequired: true, isSystem: false, referenceTypeId: 407, referenceTypeName: 'CertificateType', description: 'Type certificaat' },
{ jiraId: 4696, name: 'Classification Type', fieldName: 'classificationType', type: 'reference', isMultiple: false, isEditable: true, isRequired: true, isSystem: false, referenceTypeId: 408, referenceTypeName: 'CertificateClassificationType', description: 'Classificatie type' },
{ jiraId: 4698, name: 'Requester', fieldName: 'requester', type: 'boolean', isMultiple: false, isEditable: true, isRequired: true, isSystem: false, description: 'De aanvrager (medewerker) van het betreffende certificaat' },
{ jiraId: 4682, name: 'Issuing Authority', fieldName: 'issuingAuthority', type: 'reference', isMultiple: false, isEditable: true, isRequired: true, isSystem: false, referenceTypeId: 37, referenceTypeName: 'Supplier', description: 'Het bedrijf of de organisatie/instantie die verantwoordelijk is voor de uitgifte van het certificaat' },
{ jiraId: 4702, name: 'Issuing Supplier', fieldName: 'issuingSupplier', type: 'reference', isMultiple: false, isEditable: true, isRequired: true, isSystem: false, referenceTypeId: 37, referenceTypeName: 'Supplier', description: 'Het bedrijf of de organisatie/instantie waar het certificaat is besteld.' },
{ jiraId: 4697, name: 'Autorenew', fieldName: 'autorenew', type: 'boolean', isMultiple: false, isEditable: true, isRequired: false, isSystem: false, description: 'Indien een expiry date beschikbaar, dan op False' },
{ jiraId: 4721, name: 'Expiry Date', fieldName: 'expiryDate', type: 'date', isMultiple: false, isEditable: true, isRequired: false, isSystem: false, description: ' Als er geen Expiry Date bekend is, wordt de waarde ingesteld op 01-01-1970' },
{ jiraId: 4753, name: 'ReminderInDays', fieldName: 'reminderInDays', type: 'reference', isMultiple: false, isEditable: true, isRequired: true, isSystem: false, description: 'Aantal dagen vanaf welk moment er een reminder mechanisme wordt gestart' },
{ jiraId: 4797, name: 'ReminderMailbox', fieldName: 'reminderMailbox', type: 'boolean', isMultiple: true, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4683, name: 'Certificate Owner', fieldName: 'certificateOwner', type: 'reference', isMultiple: false, isEditable: true, isRequired: false, isSystem: false, referenceTypeId: 243, referenceTypeName: 'Organization Unit', description: 'De (eind)verantwoordelijke (medewerker) van het certificaat binnen Zuyderland' },
{ jiraId: 4699, name: 'IT Operations Team', fieldName: 'itOperationsTeam', type: 'reference', isMultiple: false, isEditable: true, isRequired: false, isSystem: false, referenceTypeId: 400, referenceTypeName: 'group', description: 'Het team dat verantwoordelijk is voor de installatie / configuratie certificaat' },
{ jiraId: 4700, name: 'Application Management', fieldName: 'applicationManagement', type: 'reference', isMultiple: false, isEditable: true, isRequired: false, isSystem: false, referenceTypeId: 400, referenceTypeName: 'group', description: 'Het team dat verantwoordelijk is voor het applicatiebeheer van de gekoppelde Application Component / Dienst' },
{ jiraId: 4701, name: 'Application Component', fieldName: 'applicationComponent', type: 'reference', isMultiple: true, isEditable: true, isRequired: false, isSystem: false, referenceTypeId: 38, referenceTypeName: 'ApplicationComponent', description: 'Verwijzing naar Application Component' },
{ jiraId: 4680, name: 'Domain', fieldName: 'domain', type: 'reference', isMultiple: true, isEditable: true, isRequired: false, isSystem: false, referenceTypeId: 252, referenceTypeName: 'Domain', description: 'Verwijzing naar Domain' },
{ jiraId: 4681, name: 'Server', fieldName: 'server', type: 'reference', isMultiple: true, isEditable: true, isRequired: false, isSystem: false, referenceTypeId: 48, referenceTypeName: 'Server', description: 'Verwijzing naar Server' },
{ jiraId: 4679, name: 'Description', fieldName: 'description', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false, description: 'Algemene omschrijving' },
{ jiraId: 4719, name: 'Extra Installatie', fieldName: 'extraInstallatie', type: 'reference', isMultiple: true, isEditable: true, isRequired: false, isSystem: false, referenceTypeId: 38, referenceTypeName: 'ApplicationComponent' },
{ jiraId: 4677, name: 'Created', fieldName: 'created', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4678, name: 'Updated', fieldName: 'updated', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true }
],
},
'PrivilegedUser': {
jiraTypeId: 401,
name: 'Privileged User',
typeName: 'PrivilegedUser',
syncPriority: 2,
objectCount: 728,
attributes: [
{ jiraId: 4616, name: 'Key', fieldName: 'key', type: 'text', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4617, name: 'Name', fieldName: 'name', type: 'text', isMultiple: false, isEditable: true, isRequired: true, isSystem: false, description: 'The name of the object' },
{ jiraId: 4620, name: 'UPN', fieldName: 'uPN', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false, description: 'User Principal Name' },
{ jiraId: 4621, name: 'Description', fieldName: 'description', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false, description: 'Description' },
{ jiraId: 4622, name: 'Topdesk ticket', fieldName: 'topdeskTicket', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4623, name: 'Application Component', fieldName: 'applicationComponent', type: 'reference', isMultiple: false, isEditable: true, isRequired: false, isSystem: false, referenceTypeId: 38, referenceTypeName: 'ApplicationComponent' },
{ jiraId: 4624, name: 'E-Mail', fieldName: 'eMail', type: 'textarea', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4625, name: 'Mobile', fieldName: 'mobile', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4626, name: 'MethodsRegistered', fieldName: 'methodsRegistered', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4627, name: 'Creation Date', fieldName: 'creationDate', type: 'url', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4628, name: 'Expiry Date', fieldName: 'expiryDate', type: 'url', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4629, name: 'Last Updated', fieldName: 'lastUpdated', type: 'url', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4630, name: 'Password Last Set', fieldName: 'passwordLastSet', type: 'url', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4631, name: 'Password never expires', fieldName: 'passwordNeverExpires', type: 'boolean', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4632, name: 'MFA Registered', fieldName: 'mfaRegistered', type: 'boolean', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4633, name: 'Self-Service Password Reset Used', fieldName: 'selfServicePasswordResetUsed', type: 'boolean', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4653, name: 'Self-Service Password Reset Enabled', fieldName: 'selfServicePasswordResetEnabled', type: 'boolean', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4634, name: 'Last Logon', fieldName: 'lastLogon', type: 'url', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4618, name: 'Created', fieldName: 'created', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4619, name: 'Updated', fieldName: 'updated', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4652, name: 'SID', fieldName: 'sID', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4800, name: 'ImportDate', fieldName: 'importDate', type: 'date', isMultiple: false, isEditable: true, isRequired: false, isSystem: false }
],
},
'Domain': {
jiraTypeId: 252,
name: 'Domain',
typeName: 'Domain',
syncPriority: 2,
objectCount: 796,
attributes: [
{ jiraId: 2398, name: 'Key', fieldName: 'key', type: 'text', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 2399, name: 'Name', fieldName: 'name', type: 'text', isMultiple: false, isEditable: true, isRequired: true, isSystem: false, description: 'The name of the object' },
{ jiraId: 2465, name: 'Status', fieldName: 'status', type: 'email', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 2466, name: 'Description', fieldName: 'description', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4672, name: 'Registration date', fieldName: 'registrationDate', type: 'date', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4668, name: 'Domain Type', fieldName: 'domainType', type: 'reference', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 2471, name: 'Domain Function', fieldName: 'domainFunction', type: 'reference', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4669, name: 'DNS Management', fieldName: 'dnsManagement', type: 'reference', isMultiple: false, isEditable: true, isRequired: false, isSystem: false, referenceTypeId: 37, referenceTypeName: 'Supplier' },
{ jiraId: 4673, name: 'Application Component', fieldName: 'applicationComponent', type: 'reference', isMultiple: true, isEditable: true, isRequired: false, isSystem: false, referenceTypeId: 38, referenceTypeName: 'ApplicationComponent' },
{ jiraId: 2467, name: 'Application Component Monitoring', fieldName: 'applicationComponentMonitoring', type: 'reference', isMultiple: true, isEditable: true, isRequired: false, isSystem: false, referenceTypeId: 38, referenceTypeName: 'ApplicationComponent' },
{ jiraId: 4543, name: 'Website availability', fieldName: 'websiteAvailability', type: 'reference', isMultiple: false, isEditable: true, isRequired: false, isSystem: false, description: 'Intern/extern/local' },
{ jiraId: 4685, name: 'Redirect URL', fieldName: 'redirectURL', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4674, name: 'Mail enabled', fieldName: 'mailEnabled', type: 'boolean', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 2472, name: 'Business Owner', fieldName: 'businessOwner', type: 'reference', isMultiple: false, isEditable: true, isRequired: false, isSystem: false, referenceTypeId: 243, referenceTypeName: 'Organization Unit' },
{ jiraId: 2474, name: 'Confluence', fieldName: 'confluence', type: 'float', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 2400, name: 'Created', fieldName: 'created', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 2401, name: 'Updated', fieldName: 'updated', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true }
],
},
'Supplier': {
jiraTypeId: 37,
name: 'Supplier',
typeName: 'Supplier',
syncPriority: 2,
objectCount: 471,
attributes: [
{ jiraId: 337, name: 'Key', fieldName: 'key', type: 'text', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 338, name: 'Name', fieldName: 'name', type: 'text', isMultiple: false, isEditable: true, isRequired: true, isSystem: false, description: 'The name of the object' },
{ jiraId: 339, name: 'Created', fieldName: 'created', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 340, name: 'Updated', fieldName: 'updated', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 1143, name: 'Address', fieldName: 'address', type: 'select', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4635, name: 'Street', fieldName: 'street', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4636, name: 'House number', fieldName: 'houseNumber', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4637, name: 'City', fieldName: 'city', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4638, name: 'Postal code', fieldName: 'postalCode', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4639, name: 'Country code', fieldName: 'countryCode', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 1144, name: 'Company Telephone', fieldName: 'companyTelephone', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 1145, name: 'Company Website', fieldName: 'companyWebsite', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 1146, name: 'Company email', fieldName: 'companyEmail', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 1147, name: 'Servicedesk Telephone', fieldName: 'servicedeskTelephone', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 1148, name: 'Servicedesk email', fieldName: 'servicedeskEmail', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 1149, name: 'Consultant 1', fieldName: 'consultant1', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 2412, name: 'Consultant 1 e-mail', fieldName: 'consultant1EMail', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 1150, name: 'Consultant 2', fieldName: 'consultant2', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 2413, name: 'Consultant 2 e-mail', fieldName: 'consultant2EMail', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 1151, name: 'Consultant 3', fieldName: 'consultant3', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 2414, name: 'Consultant 3 e-mail', fieldName: 'consultant3EMail', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false }
],
},
'Software': {
jiraTypeId: 421,
name: 'Software',
typeName: 'Software',
syncPriority: 2,
objectCount: 307,
attributes: [
{ jiraId: 4806, name: 'Key', fieldName: 'key', type: 'text', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4807, name: 'Name', fieldName: 'name', type: 'text', isMultiple: false, isEditable: true, isRequired: true, isSystem: false, description: 'The name of the object' },
{ jiraId: 4808, name: 'Created', fieldName: 'created', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4809, name: 'Updated', fieldName: 'updated', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4836, name: 'Description', fieldName: 'description', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4810, name: 'ApplicationComponent', fieldName: 'applicationComponent', type: 'reference', isMultiple: false, isEditable: true, isRequired: false, isSystem: false, referenceTypeId: 38, referenceTypeName: 'ApplicationComponent' },
{ jiraId: 4811, name: 'Status', fieldName: 'status', type: 'email', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4812, name: 'SupplierProduct', fieldName: 'supplierProduct', type: 'reference', isMultiple: false, isEditable: true, isRequired: false, isSystem: false, referenceTypeId: 37, referenceTypeName: 'Supplier' },
{ jiraId: 4813, name: 'SupplierTechnical', fieldName: 'supplierTechnical', type: 'reference', isMultiple: false, isEditable: true, isRequired: false, isSystem: false, referenceTypeId: 37, referenceTypeName: 'Supplier' }
],
},
'SoftwarePatch': {
jiraTypeId: 423,
name: 'SoftwarePatch',
typeName: 'SoftwarePatch',
syncPriority: 2,
objectCount: 555,
attributes: [
{ jiraId: 4816, name: 'Key', fieldName: 'key', type: 'text', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4817, name: 'Name', fieldName: 'name', type: 'text', isMultiple: false, isEditable: true, isRequired: true, isSystem: false, description: 'naamgeving: software-versie' },
{ jiraId: 4820, name: 'Software', fieldName: 'software', type: 'reference', isMultiple: false, isEditable: true, isRequired: true, isSystem: false, referenceTypeId: 421, referenceTypeName: 'Software' },
{ jiraId: 4821, name: 'Version', fieldName: 'version', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4835, name: 'Status', fieldName: 'status', type: 'reference', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4818, name: 'Created', fieldName: 'created', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4819, name: 'Updated', fieldName: 'updated', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4839, name: 'Description', fieldName: 'description', type: 'select', isMultiple: false, isEditable: true, isRequired: false, isSystem: false }
],
},
'Manual': {
jiraTypeId: 430,
name: 'Manual',
typeName: 'Manual',
syncPriority: 5,
objectCount: 23,
attributes: [
{ jiraId: 4865, name: 'Key', fieldName: 'key', type: 'text', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4866, name: 'Name', fieldName: 'name', type: 'text', isMultiple: false, isEditable: true, isRequired: true, isSystem: false, description: 'The name of the object' },
{ jiraId: 4869, name: 'Link', fieldName: 'link', type: 'float', isMultiple: true, isEditable: true, isRequired: false, isSystem: false, description: 'Confluence manual page' },
{ jiraId: 4870, name: 'Type', fieldName: 'type', type: 'reference', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4867, name: 'Created', fieldName: 'created', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4868, name: 'Updated', fieldName: 'updated', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4871, name: 'Target', fieldName: 'target', type: 'reference', isMultiple: true, isEditable: true, isRequired: false, isSystem: false, referenceTypeId: 430, referenceTypeName: 'Manual', description: 'If applicable' }
],
},
'Measures': {
jiraTypeId: 391,
name: 'Measures',
typeName: 'Measures',
syncPriority: 5,
objectCount: 11,
attributes: [
{ jiraId: 4512, name: 'Key', fieldName: 'key', type: 'text', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4513, name: 'Name', fieldName: 'name', type: 'text', isMultiple: false, isEditable: true, isRequired: true, isSystem: false, description: 'The name of the object' },
{ jiraId: 4516, name: 'Description', fieldName: 'description', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4522, name: 'Obliged', fieldName: 'obliged', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4521, name: 'Availability', fieldName: 'availability', type: 'reference', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4523, name: 'Optional', fieldName: 'optional', type: 'reference', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4514, name: 'Created', fieldName: 'created', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4515, name: 'Updated', fieldName: 'updated', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true }
],
},
'Rebootgroups': {
jiraTypeId: 251,
name: 'Rebootgroups',
typeName: 'Rebootgroups',
syncPriority: 5,
objectCount: 14,
attributes: [
{ jiraId: 2391, name: 'Key', fieldName: 'key', type: 'text', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 2392, name: 'Name', fieldName: 'name', type: 'text', isMultiple: false, isEditable: true, isRequired: true, isSystem: false, description: 'The name of the object' },
{ jiraId: 2397, name: 'Reboot day', fieldName: 'rebootDay', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 2395, name: 'Reboot time', fieldName: 'rebootTime', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 2396, name: 'Install information', fieldName: 'installInformation', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 2393, name: 'Created', fieldName: 'created', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 2394, name: 'Updated', fieldName: 'updated', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true }
],
},
'ApplicationManagementTeam': {
jiraTypeId: 440,
name: 'Application Management - Team',
typeName: 'ApplicationManagementTeam',
syncPriority: 5,
objectCount: 11,
attributes: [
{ jiraId: 4946, name: 'Key', fieldName: 'key', type: 'text', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4947, name: 'Name', fieldName: 'name', type: 'text', isMultiple: false, isEditable: true, isRequired: true, isSystem: false, description: 'The name of the object' },
{ jiraId: 4948, name: 'Created', fieldName: 'created', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4949, name: 'Updated', fieldName: 'updated', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4950, name: 'Type', fieldName: 'type', type: 'reference', isMultiple: false, isEditable: true, isRequired: false, isSystem: false }
],
},
'ApplicationManagementSubteam': {
jiraTypeId: 435,
name: 'Application Management - Subteam',
typeName: 'ApplicationManagementSubteam',
syncPriority: 5,
objectCount: 19,
attributes: [
{ jiraId: 4907, name: 'Key', fieldName: 'key', type: 'text', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4951, name: 'Application Management - Team', fieldName: 'applicationManagementTeam', type: 'reference', isMultiple: false, isEditable: true, isRequired: true, isSystem: false, referenceTypeId: 440, referenceTypeName: 'ApplicationManagementTeam' },
{ jiraId: 4908, name: 'Name', fieldName: 'name', type: 'text', isMultiple: false, isEditable: true, isRequired: true, isSystem: false, description: 'The name of the object' },
{ jiraId: 4909, name: 'Created', fieldName: 'created', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4910, name: 'Updated', fieldName: 'updated', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4912, name: 'Description', fieldName: 'description', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false }
],
},
'Assets': {
jiraTypeId: 42,
name: 'Assets',
typeName: 'Assets',
syncPriority: 8,
objectCount: 0,
attributes: [
{ jiraId: 384, name: 'Key', fieldName: 'key', type: 'text', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 385, name: 'Name', fieldName: 'name', type: 'text', isMultiple: false, isEditable: true, isRequired: true, isSystem: false, description: 'The name of the object' },
{ jiraId: 386, name: 'Created', fieldName: 'created', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 387, name: 'Updated', fieldName: 'updated', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true }
],
},
'CI': {
jiraTypeId: 392,
name: 'CI',
typeName: 'CI',
syncPriority: 8,
objectCount: 0,
attributes: [
{ jiraId: 4552, name: 'Key', fieldName: 'key', type: 'text', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4553, name: 'Name', fieldName: 'name', type: 'text', isMultiple: false, isEditable: true, isRequired: true, isSystem: false, description: 'The name of the object' },
{ jiraId: 4554, name: 'Created', fieldName: 'created', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4555, name: 'Updated', fieldName: 'updated', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true }
],
},
'Metadata': {
jiraTypeId: 57,
name: 'Metadata',
typeName: 'Metadata',
syncPriority: 8,
objectCount: 0,
attributes: [
{ jiraId: 499, name: 'Key', fieldName: 'key', type: 'text', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 500, name: 'Name', fieldName: 'name', type: 'text', isMultiple: false, isEditable: true, isRequired: true, isSystem: false, description: 'The name of the object' },
{ jiraId: 4705, name: 'Description', fieldName: 'description', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 501, name: 'Created', fieldName: 'created', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 502, name: 'Updated', fieldName: 'updated', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true }
],
},
'ApplicationManagementHosting': {
jiraTypeId: 438,
name: 'Application Management - Hosting',
typeName: 'ApplicationManagementHosting',
syncPriority: 8,
objectCount: 4,
attributes: [
{ jiraId: 4933, name: 'Key', fieldName: 'key', type: 'text', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4934, name: 'Name', fieldName: 'name', type: 'text', isMultiple: false, isEditable: true, isRequired: true, isSystem: false, description: 'The name of the object' },
{ jiraId: 4935, name: 'Created', fieldName: 'created', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4936, name: 'Updated', fieldName: 'updated', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4938, name: 'Description', fieldName: 'description', type: 'select', isMultiple: false, isEditable: true, isRequired: false, isSystem: false }
],
},
'ApplicationManagementTam': {
jiraTypeId: 439,
name: 'Application Management - TAM',
typeName: 'ApplicationManagementTam',
syncPriority: 8,
objectCount: 4,
attributes: [
{ jiraId: 4940, name: 'Key', fieldName: 'key', type: 'text', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4941, name: 'Name', fieldName: 'name', type: 'text', isMultiple: false, isEditable: true, isRequired: true, isSystem: false, description: 'The name of the object' },
{ jiraId: 4942, name: 'Created', fieldName: 'created', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4943, name: 'Updated', fieldName: 'updated', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4944, name: 'Description', fieldName: 'description', type: 'select', isMultiple: false, isEditable: true, isRequired: false, isSystem: false }
],
},
'ApplicationManagementNumberOfUsers': {
jiraTypeId: 433,
name: 'Application Management - Number of Users',
typeName: 'ApplicationManagementNumberOfUsers',
syncPriority: 8,
objectCount: 7,
attributes: [
{ jiraId: 4889, name: 'Key', fieldName: 'key', type: 'text', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4890, name: 'Name', fieldName: 'name', type: 'text', isMultiple: false, isEditable: true, isRequired: true, isSystem: false, description: 'The name of the object' },
{ jiraId: 4891, name: 'Created', fieldName: 'created', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4892, name: 'Updated', fieldName: 'updated', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4893, name: 'Examples', fieldName: 'examples', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4894, name: 'Factor', fieldName: 'factor', type: 'float', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4895, name: 'Order', fieldName: 'order', type: 'integer', isMultiple: false, isEditable: true, isRequired: false, isSystem: false }
],
},
'CertificateType': {
jiraTypeId: 407,
name: 'Certificate Type',
typeName: 'CertificateType',
syncPriority: 10,
objectCount: 3,
attributes: [
{ jiraId: 4687, name: 'Key', fieldName: 'key', type: 'text', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4688, name: 'Name', fieldName: 'name', type: 'text', isMultiple: false, isEditable: true, isRequired: true, isSystem: false, description: 'The name of the object' },
{ jiraId: 4689, name: 'Created', fieldName: 'created', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4690, name: 'Updated', fieldName: 'updated', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true }
],
},
'ApplicationFunctionCategory': {
jiraTypeId: 437,
name: 'ApplicationFunctionCategory',
typeName: 'ApplicationFunctionCategory',
syncPriority: 10,
objectCount: 13,
attributes: [
{ jiraId: 4921, name: 'Key', fieldName: 'key', type: 'text', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4922, name: 'Name', fieldName: 'name', type: 'text', isMultiple: false, isEditable: true, isRequired: true, isSystem: false, description: 'The name of the object' },
{ jiraId: 4923, name: 'Created', fieldName: 'created', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4924, name: 'Updated', fieldName: 'updated', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4925, name: 'Description', fieldName: 'description', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false }
],
},
'ApplicationFunction': {
jiraTypeId: 403,
name: 'ApplicationFunction',
typeName: 'ApplicationFunction',
syncPriority: 10,
objectCount: 93,
attributes: [
{ jiraId: 4650, name: 'AppFuncGUID', fieldName: 'appFuncGUID', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4646, name: 'Key', fieldName: 'key', type: 'text', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4926, name: 'Application Function Category', fieldName: 'applicationFunctionCategory', type: 'reference', isMultiple: false, isEditable: true, isRequired: false, isSystem: false, referenceTypeId: 437, referenceTypeName: 'ApplicationFunctionCategory' },
{ jiraId: 4647, name: 'Name', fieldName: 'name', type: 'text', isMultiple: false, isEditable: true, isRequired: true, isSystem: false, description: 'The name of the object' },
{ jiraId: 4651, name: 'Description', fieldName: 'description', type: 'select', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4920, name: 'Keywords', fieldName: 'keywords', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4648, name: 'Created', fieldName: 'created', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4649, name: 'Updated', fieldName: 'updated', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4919, name: 'Application Management - Application Cluster', fieldName: 'applicationManagementApplicationCluster', type: 'reference', isMultiple: false, isEditable: true, isRequired: false, isSystem: false, referenceTypeId: 435, referenceTypeName: 'ApplicationManagementSubteam' }
],
},
'CertificateClassificationType': {
jiraTypeId: 408,
name: 'Certificate ClassificationType',
typeName: 'CertificateClassificationType',
syncPriority: 10,
objectCount: 8,
attributes: [
{ jiraId: 4691, name: 'Key', fieldName: 'key', type: 'text', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4692, name: 'Name', fieldName: 'name', type: 'text', isMultiple: false, isEditable: true, isRequired: true, isSystem: false, description: 'The name of the object' },
{ jiraId: 4703, name: 'Description', fieldName: 'description', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false, description: 'https://jira.zuyderland.nl/browse/CMDB-430' },
{ jiraId: 4693, name: 'Created', fieldName: 'created', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4694, name: 'Updated', fieldName: 'updated', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true }
],
},
'BusinessImpactAnalyse': {
jiraTypeId: 41,
name: 'Business Impact Analyse',
typeName: 'BusinessImpactAnalyse',
syncPriority: 10,
objectCount: 6,
attributes: [
{ jiraId: 369, name: 'Key', fieldName: 'key', type: 'text', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 370, name: 'Name', fieldName: 'name', type: 'text', isMultiple: false, isEditable: true, isRequired: true, isSystem: false, description: 'The name of the object' },
{ jiraId: 1107, name: 'Service Window', fieldName: 'serviceWindow', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 1108, name: 'Availabilty', fieldName: 'availabilty', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 2411, name: 'BIA Check', fieldName: 'biaCheck', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 1109, name: 'Recovery Time Objective (RTO)', fieldName: 'recoveryTimeObjectiveRTO', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 1110, name: 'Recovery Point Objective (RPO)', fieldName: 'recoveryPointObjectiveRPO', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 1111, name: 'Environments', fieldName: 'environments', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4930, name: 'Description', fieldName: 'description', type: 'select', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4931, name: 'Indicators', fieldName: 'indicators', type: 'select', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 371, name: 'Created', fieldName: 'created', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 372, name: 'Updated', fieldName: 'updated', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true }
],
},
'BusinessImportance': {
jiraTypeId: 44,
name: 'Business Importance',
typeName: 'BusinessImportance',
syncPriority: 10,
objectCount: 8,
attributes: [
{ jiraId: 395, name: 'Key', fieldName: 'key', type: 'text', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 396, name: 'Name', fieldName: 'name', type: 'text', isMultiple: false, isEditable: true, isRequired: true, isSystem: false, description: 'The name of the object' },
{ jiraId: 4517, name: 'Description', fieldName: 'description', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 397, name: 'Created', fieldName: 'created', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 398, name: 'Updated', fieldName: 'updated', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true }
],
},
'HostingType': {
jiraTypeId: 39,
name: 'Hosting Type',
typeName: 'HostingType',
syncPriority: 10,
objectCount: 6,
attributes: [
{ jiraId: 345, name: 'Key', fieldName: 'key', type: 'text', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 346, name: 'Name', fieldName: 'name', type: 'text', isMultiple: false, isEditable: true, isRequired: true, isSystem: false, description: 'The name of the object' },
{ jiraId: 4520, name: 'Description', fieldName: 'description', type: 'select', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 347, name: 'Created', fieldName: 'created', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 348, name: 'Updated', fieldName: 'updated', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true }
],
},
'Organisation': {
jiraTypeId: 390,
name: 'Organisation',
typeName: 'Organisation',
syncPriority: 10,
objectCount: 6,
attributes: [
{ jiraId: 4507, name: 'Key', fieldName: 'key', type: 'text', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4508, name: 'Name', fieldName: 'name', type: 'text', isMultiple: false, isEditable: true, isRequired: true, isSystem: false, description: 'The name of the object' },
{ jiraId: 4511, name: 'Description', fieldName: 'description', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4509, name: 'Created', fieldName: 'created', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4510, name: 'Updated', fieldName: 'updated', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true }
],
},
'ApplicationManagementApplicationType': {
jiraTypeId: 436,
name: 'Application Management - Application Type',
typeName: 'ApplicationManagementApplicationType',
syncPriority: 10,
objectCount: 4,
attributes: [
{ jiraId: 4913, name: 'Key', fieldName: 'key', type: 'text', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4914, name: 'Name', fieldName: 'name', type: 'text', isMultiple: false, isEditable: true, isRequired: true, isSystem: false, description: 'The name of the object' },
{ jiraId: 4915, name: 'Created', fieldName: 'created', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4916, name: 'Updated', fieldName: 'updated', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4917, name: 'Description', fieldName: 'description', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false }
],
},
'ApplicationManagementDynamicsFactor': {
jiraTypeId: 434,
name: 'Application Management - Dynamics Factor',
typeName: 'ApplicationManagementDynamicsFactor',
syncPriority: 10,
objectCount: 4,
attributes: [
{ jiraId: 4896, name: 'Key', fieldName: 'key', type: 'text', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4897, name: 'Name', fieldName: 'name', type: 'text', isMultiple: false, isEditable: true, isRequired: true, isSystem: false, description: 'The name of the object' },
{ jiraId: 4898, name: 'Created', fieldName: 'created', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4899, name: 'Updated', fieldName: 'updated', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4900, name: 'Summary', fieldName: 'summary', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4928, name: 'Description', fieldName: 'description', type: 'select', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4901, name: 'Factor', fieldName: 'factor', type: 'float', isMultiple: false, isEditable: true, isRequired: false, isSystem: false }
],
},
'ApplicationManagementComplexityFactor': {
jiraTypeId: 432,
name: 'Application Management - Complexity Factor',
typeName: 'ApplicationManagementComplexityFactor',
syncPriority: 10,
objectCount: 4,
attributes: [
{ jiraId: 4883, name: 'Key', fieldName: 'key', type: 'text', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4884, name: 'Name', fieldName: 'name', type: 'text', isMultiple: false, isEditable: true, isRequired: true, isSystem: false, description: 'The name of the object' },
{ jiraId: 4885, name: 'Created', fieldName: 'created', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4886, name: 'Updated', fieldName: 'updated', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4887, name: 'Summary', fieldName: 'summary', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4929, name: 'Description', fieldName: 'description', type: 'select', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4888, name: 'Factor', fieldName: 'factor', type: 'float', isMultiple: false, isEditable: true, isRequired: false, isSystem: false }
],
},
'IctGovernanceModel': {
jiraTypeId: 431,
name: 'ICT Governance Model',
typeName: 'IctGovernanceModel',
syncPriority: 10,
objectCount: 6,
attributes: [
{ jiraId: 4874, name: 'Key', fieldName: 'key', type: 'text', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4875, name: 'Name', fieldName: 'name', type: 'text', isMultiple: false, isEditable: true, isRequired: true, isSystem: false, description: 'The name of the object' },
{ jiraId: 4876, name: 'Created', fieldName: 'created', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4877, name: 'Updated', fieldName: 'updated', type: 'url', isMultiple: false, isEditable: false, isRequired: true, isSystem: true },
{ jiraId: 4878, name: 'Summary', fieldName: 'summary', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4879, name: 'Description', fieldName: 'description', type: 'text', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4880, name: 'Remarks', fieldName: 'remarks', type: 'select', isMultiple: false, isEditable: true, isRequired: false, isSystem: false },
{ jiraId: 4881, name: 'Application', fieldName: 'application', type: 'select', isMultiple: false, isEditable: true, isRequired: false, isSystem: false }
],
}
};
// =============================================================================
// Lookup Maps
// =============================================================================
/** Map from Jira Type ID to TypeScript type name */
export const TYPE_ID_TO_NAME: Record<number, string> = {
38: 'ApplicationComponent',
59: 'Flows',
48: 'Server',
418: 'AzureSubscription',
424: 'PackageBuild',
422: 'Package',
406: 'Certificate',
401: 'PrivilegedUser',
252: 'Domain',
37: 'Supplier',
421: 'Software',
423: 'SoftwarePatch',
430: 'Manual',
391: 'Measures',
251: 'Rebootgroups',
440: 'ApplicationManagementTeam',
435: 'ApplicationManagementSubteam',
42: 'Assets',
392: 'CI',
57: 'Metadata',
438: 'ApplicationManagementHosting',
439: 'ApplicationManagementTam',
433: 'ApplicationManagementNumberOfUsers',
407: 'CertificateType',
437: 'ApplicationFunctionCategory',
403: 'ApplicationFunction',
408: 'CertificateClassificationType',
41: 'BusinessImpactAnalyse',
44: 'BusinessImportance',
39: 'HostingType',
390: 'Organisation',
436: 'ApplicationManagementApplicationType',
434: 'ApplicationManagementDynamicsFactor',
432: 'ApplicationManagementComplexityFactor',
431: 'IctGovernanceModel',
};
/** Map from TypeScript type name to Jira Type ID */
export const TYPE_NAME_TO_ID: Record<string, number> = {
'ApplicationComponent': 38,
'Flows': 59,
'Server': 48,
'AzureSubscription': 418,
'PackageBuild': 424,
'Package': 422,
'Certificate': 406,
'PrivilegedUser': 401,
'Domain': 252,
'Supplier': 37,
'Software': 421,
'SoftwarePatch': 423,
'Manual': 430,
'Measures': 391,
'Rebootgroups': 251,
'ApplicationManagementTeam': 440,
'ApplicationManagementSubteam': 435,
'Assets': 42,
'CI': 392,
'Metadata': 57,
'ApplicationManagementHosting': 438,
'ApplicationManagementTam': 439,
'ApplicationManagementNumberOfUsers': 433,
'CertificateType': 407,
'ApplicationFunctionCategory': 437,
'ApplicationFunction': 403,
'CertificateClassificationType': 408,
'BusinessImpactAnalyse': 41,
'BusinessImportance': 44,
'HostingType': 39,
'Organisation': 390,
'ApplicationManagementApplicationType': 436,
'ApplicationManagementDynamicsFactor': 434,
'ApplicationManagementComplexityFactor': 432,
'IctGovernanceModel': 431,
};
/** Map from Jira object type name to TypeScript type name */
export const JIRA_NAME_TO_TYPE: Record<string, string> = {
'Application Component': 'ApplicationComponent',
'Flows': 'Flows',
'Server': 'Server',
'AzureSubscription': 'AzureSubscription',
'PackageBuild': 'PackageBuild',
'Package': 'Package',
'Certificate': 'Certificate',
'Privileged User': 'PrivilegedUser',
'Domain': 'Domain',
'Supplier': 'Supplier',
'Software': 'Software',
'SoftwarePatch': 'SoftwarePatch',
'Manual': 'Manual',
'Measures': 'Measures',
'Rebootgroups': 'Rebootgroups',
'Application Management - Team': 'ApplicationManagementTeam',
'Application Management - Subteam': 'ApplicationManagementSubteam',
'Assets': 'Assets',
'CI': 'CI',
'Metadata': 'Metadata',
'Application Management - Hosting': 'ApplicationManagementHosting',
'Application Management - TAM': 'ApplicationManagementTam',
'Application Management - Number of Users': 'ApplicationManagementNumberOfUsers',
'Certificate Type': 'CertificateType',
'ApplicationFunctionCategory': 'ApplicationFunctionCategory',
'ApplicationFunction': 'ApplicationFunction',
'Certificate ClassificationType': 'CertificateClassificationType',
'Business Impact Analyse': 'BusinessImpactAnalyse',
'Business Importance': 'BusinessImportance',
'Hosting Type': 'HostingType',
'Organisation': 'Organisation',
'Application Management - Application Type': 'ApplicationManagementApplicationType',
'Application Management - Dynamics Factor': 'ApplicationManagementDynamicsFactor',
'Application Management - Complexity Factor': 'ApplicationManagementComplexityFactor',
'ICT Governance Model': 'IctGovernanceModel',
};
// =============================================================================
// Helper Functions
// =============================================================================
/** Get attribute definition by type and field name */
export function getAttributeDefinition(typeName: string, fieldName: string): AttributeDefinition | undefined {
const objectType = OBJECT_TYPES[typeName];
if (!objectType) return undefined;
return objectType.attributes.find(a => a.fieldName === fieldName);
}
/** Get attribute definition by type and Jira attribute ID */
export function getAttributeById(typeName: string, jiraId: number): AttributeDefinition | undefined {
const objectType = OBJECT_TYPES[typeName];
if (!objectType) return undefined;
return objectType.attributes.find(a => a.jiraId === jiraId);
}
/** Get attribute definition by type and Jira attribute name */
export function getAttributeByName(typeName: string, attrName: string): AttributeDefinition | undefined {
const objectType = OBJECT_TYPES[typeName];
if (!objectType) return undefined;
return objectType.attributes.find(a => a.name === attrName);
}
/** Get attribute Jira ID by type and attribute name - throws if not found */
export function getAttributeId(typeName: string, attrName: string): number {
const attr = getAttributeByName(typeName, attrName);
if (!attr) {
throw new Error(`Attribute "${attrName}" not found on type "${typeName}"`);
}
return attr.jiraId;
}
/** Get all reference attributes for a type */
export function getReferenceAttributes(typeName: string): AttributeDefinition[] {
const objectType = OBJECT_TYPES[typeName];
if (!objectType) return [];
return objectType.attributes.filter(a => a.type === 'reference');
}
/** Get all object types sorted by sync priority */
export function getObjectTypesBySyncPriority(): ObjectTypeDefinition[] {
return Object.values(OBJECT_TYPES).sort((a, b) => a.syncPriority - b.syncPriority);
}

View File

@@ -0,0 +1,933 @@
// AUTO-GENERATED FILE - DO NOT EDIT MANUALLY
// Generated from Jira Assets Schema via REST API
// Generated at: 2026-01-09T02:12:50.973Z
//
// Re-generate with: npm run generate-schema
// =============================================================================
// Base Types
// =============================================================================
/** Reference to another CMDB object */
export interface ObjectReference {
objectId: string;
objectKey: string;
label: string;
// Optional enriched data from referenced object
factor?: number;
}
/** Base interface for all CMDB objects */
export interface BaseCMDBObject {
id: string;
objectKey: string;
label: string;
_objectType: string;
_jiraUpdatedAt: string;
_jiraCreatedAt: string;
}
// =============================================================================
// Object Type Interfaces
// =============================================================================
/** Application Component (Jira Type ID: 38, 596 objects) */
export interface ApplicationComponent extends BaseCMDBObject {
_objectType: 'ApplicationComponent';
// Scalar attributes
reference: string | null; // Niet aanpassen. GUID - Enterprise Architect
key: string | null;
searchReference: string | null; // Additionele zoekwoorden t.b.v. search
name: string | null; // Unieke naam object
created: string | null;
updated: string | null;
description: string | null; // * Application description
status: string | null; // Application Lifecycle Management
confluenceSpace: number | null;
zenyaID: number | null;
zenyaURL: string | null;
customDevelopment: boolean | null; // Is er sprake van eigen programmatuur?
piiData: boolean | null; // Maakt applicatie gebruik van Persoonlijk identificeerbare informatie?
medicalData: boolean | null; // Maakt de Application Component gebruik van medische data?
functionalApplicationManagement: string | null;
technicalApplicationManagementPrimary: boolean[];
technicalApplicationManagementSecondary: boolean[];
medischeTechniek: boolean | null; // Is er een link met medische techniek
technischeArchitectuurTA: string | null; // Komt uit Enterprise Architect mee
measures: string | null;
generateConfluenceSpace: boolean | null; // Wordt gebruikt door script om space te genereren - niet verwijderen. Attribuut is hidden
importDate: string | null;
applicationManagementOverrideFTE: number | null;
// Reference attributes
organisation: ObjectReference | null; // -> Organisation
applicationFunction: ObjectReference[]; // -> ApplicationFunction
businessImportance: ObjectReference | null; // -> BusinessImportance
businessImpactAnalyse: ObjectReference | null; // -> BusinessImpactAnalyse
applicationComponentHostingType: ObjectReference | null; // -> HostingType
platform: ObjectReference | null; // -> ApplicationComponent
referencedApplicationComponent: ObjectReference[]; // -> ApplicationComponent
authenticationMethod: ObjectReference | null; // -> ApplicationComponent
monitoring: ObjectReference | null; // -> ApplicationComponent
supplierProduct: ObjectReference | null; // -> Supplier
supplierTechnical: ObjectReference | null; // -> Supplier
supplierImplementation: ObjectReference | null; // -> Supplier
supplierConsultancy: ObjectReference | null; // -> Supplier
businessOwner: ObjectReference | null; // -> Organization Unit
systemOwner: ObjectReference | null; // -> User
technicalApplicationManagement: ObjectReference | null; // -> group
sourceStatus: ObjectReference | null;
ictGovernanceModel: ObjectReference | null; // -> IctGovernanceModel
applicationManagementApplicationType: ObjectReference | null; // -> ApplicationManagementApplicationType
applicationManagementHosting: ObjectReference | null; // -> ApplicationManagementHosting
applicationManagementTAM: ObjectReference | null; // -> ApplicationManagementTam
applicationManagementDynamicsFactor: ObjectReference | null; // -> ApplicationManagementDynamicsFactor
applicationManagementComplexityFactor: ObjectReference | null; // -> ApplicationManagementComplexityFactor
applicationManagementNumberOfUsers: ObjectReference | null; // -> ApplicationManagementNumberOfUsers
applicationManagementSubteam: ObjectReference | null; // -> ApplicationManagementSubteam
}
/** Flows (Jira Type ID: 59, 903 objects) */
export interface Flows extends BaseCMDBObject {
_objectType: 'Flows';
// Scalar attributes
reference: string | null;
presentInImportEnterpriseArchitect: boolean | null;
key: string | null;
name: string | null; // The name of the object
searchReferenceSource: string | null;
searchReferenceTarget: string | null;
type: string | null;
protocol: string | null;
details: string | null;
broker: string | null;
status: string | null;
created: string | null;
updated: string | null;
importDate: string | null;
// Reference attributes
source: ObjectReference | null; // -> ApplicationComponent
target: ObjectReference | null; // -> ApplicationComponent
}
/** Server (Jira Type ID: 48, 909 objects) */
export interface Server extends BaseCMDBObject {
_objectType: 'Server';
// Scalar attributes
key: string | null;
name: string | null; // The name of the object
description: string | null;
status: string | null;
vMLocation: string | null;
vMOSType: string | null;
memoryMB: number | null;
cPUCores: number | null;
nICCount: number | null;
dataDisks: number | null;
privateIPAddress: string | null;
cluster: string | null;
vMSize: string | null;
created: string | null;
updated: string | null;
dBA: boolean | null;
importDate: string | null;
state: string | null;
// Reference attributes
applicationComponent: ObjectReference | null; // -> ApplicationComponent
azureSubscription: ObjectReference[]; // -> AzureSubscription
rebootGroup: ObjectReference | null; // -> Rebootgroups
sourceStatus: ObjectReference | null;
}
/** AzureSubscription (Jira Type ID: 418, 151 objects) */
export interface AzureSubscription extends BaseCMDBObject {
_objectType: 'AzureSubscription';
// Scalar attributes
key: string | null;
name: string | null; // The name of the subscription
status: string | null;
created: string | null;
updated: string | null;
importDate: string | null;
// Reference attributes
server: ObjectReference[]; // -> Server
applicationComponent: ObjectReference | null; // -> ApplicationComponent
}
/** PackageBuild (Jira Type ID: 424, 496 objects) */
export interface PackageBuild extends BaseCMDBObject {
_objectType: 'PackageBuild';
// Scalar attributes
key: string | null;
name: string | null; // naamgeving: software-versie
version: string | null;
created: string | null;
updated: string | null;
description: string | null;
buildNumber: string | null;
architecture: string | null;
language: string | null;
appDelivery: string | null;
aDlocalMemberships: string | null;
aDmemberships: string | null;
entraIDmemberships: string | null;
platform: string | null;
inGoldenImage: boolean | null;
importDate: string | null;
// Reference attributes
software: ObjectReference | null; // -> Software
status: ObjectReference | null;
environment: ObjectReference[];
}
/** Package (Jira Type ID: 422, 299 objects) */
export interface Package extends BaseCMDBObject {
_objectType: 'Package';
// Scalar attributes
key: string | null;
name: string | null; // The name of the object
created: string | null;
updated: string | null;
description: string | null;
status: string | null;
businessImportance: string | null;
authentication: string | null;
maintenanceContract: boolean | null;
importDate: string | null;
// Reference attributes
applicationComponent: ObjectReference | null; // -> ApplicationComponent
supplierProduct: ObjectReference | null; // -> Supplier
supplierTechnical: ObjectReference | null; // -> Supplier
}
/** Certificate (Jira Type ID: 406, 508 objects) */
export interface Certificate extends BaseCMDBObject {
_objectType: 'Certificate';
// Scalar attributes
key: string | null;
name: string | null; // Naam van het certificaat
status: string | null; // Status is Active, Closed, Unknown (date 1970-01-01) or Support Requested (ticket aangemaakt vanwege Expiry Date)
requester: boolean | null; // De aanvrager (medewerker) van het betreffende certificaat
autorenew: boolean | null; // Indien een expiry date beschikbaar, dan op False
expiryDate: string | null; // Als er geen Expiry Date bekend is, wordt de waarde ingesteld op 01-01-1970
reminderMailbox: boolean[];
description: string | null; // Algemene omschrijving
created: string | null;
updated: string | null;
// Reference attributes
type: ObjectReference | null; // -> CertificateType
classificationType: ObjectReference | null; // -> CertificateClassificationType
issuingAuthority: ObjectReference | null; // -> Supplier
issuingSupplier: ObjectReference | null; // -> Supplier
reminderInDays: ObjectReference | null;
certificateOwner: ObjectReference | null; // -> Organization Unit
itOperationsTeam: ObjectReference | null; // -> group
applicationManagement: ObjectReference | null; // -> group
applicationComponent: ObjectReference[]; // -> ApplicationComponent
domain: ObjectReference[]; // -> Domain
server: ObjectReference[]; // -> Server
extraInstallatie: ObjectReference[]; // -> ApplicationComponent
}
/** Privileged User (Jira Type ID: 401, 728 objects) */
export interface PrivilegedUser extends BaseCMDBObject {
_objectType: 'PrivilegedUser';
// Scalar attributes
key: string | null;
name: string | null; // The name of the object
uPN: string | null; // User Principal Name
description: string | null; // Description
topdeskTicket: string | null;
eMail: string | null;
mobile: string | null;
methodsRegistered: string | null;
creationDate: string | null;
expiryDate: string | null;
lastUpdated: string | null;
passwordLastSet: string | null;
passwordNeverExpires: boolean | null;
mfaRegistered: boolean | null;
selfServicePasswordResetUsed: boolean | null;
selfServicePasswordResetEnabled: boolean | null;
lastLogon: string | null;
created: string | null;
updated: string | null;
sID: string | null;
importDate: string | null;
// Reference attributes
applicationComponent: ObjectReference | null; // -> ApplicationComponent
}
/** Domain (Jira Type ID: 252, 796 objects) */
export interface Domain extends BaseCMDBObject {
_objectType: 'Domain';
// Scalar attributes
key: string | null;
name: string | null; // The name of the object
status: string | null;
description: string | null;
registrationDate: string | null;
redirectURL: string | null;
mailEnabled: boolean | null;
confluence: number | null;
created: string | null;
updated: string | null;
// Reference attributes
domainType: ObjectReference | null;
domainFunction: ObjectReference | null;
dnsManagement: ObjectReference | null; // -> Supplier
applicationComponent: ObjectReference[]; // -> ApplicationComponent
applicationComponentMonitoring: ObjectReference[]; // -> ApplicationComponent
websiteAvailability: ObjectReference | null;
businessOwner: ObjectReference | null; // -> Organization Unit
}
/** Supplier (Jira Type ID: 37, 471 objects) */
export interface Supplier extends BaseCMDBObject {
_objectType: 'Supplier';
// Scalar attributes
key: string | null;
name: string | null; // The name of the object
created: string | null;
updated: string | null;
address: string | null;
street: string | null;
houseNumber: string | null;
city: string | null;
postalCode: string | null;
countryCode: string | null;
companyTelephone: string | null;
companyWebsite: string | null;
companyEmail: string | null;
servicedeskTelephone: string | null;
servicedeskEmail: string | null;
consultant1: string | null;
consultant1EMail: string | null;
consultant2: string | null;
consultant2EMail: string | null;
consultant3: string | null;
consultant3EMail: string | null;
}
/** Software (Jira Type ID: 421, 307 objects) */
export interface Software extends BaseCMDBObject {
_objectType: 'Software';
// Scalar attributes
key: string | null;
name: string | null; // The name of the object
created: string | null;
updated: string | null;
description: string | null;
status: string | null;
// Reference attributes
applicationComponent: ObjectReference | null; // -> ApplicationComponent
supplierProduct: ObjectReference | null; // -> Supplier
supplierTechnical: ObjectReference | null; // -> Supplier
}
/** SoftwarePatch (Jira Type ID: 423, 555 objects) */
export interface SoftwarePatch extends BaseCMDBObject {
_objectType: 'SoftwarePatch';
// Scalar attributes
key: string | null;
name: string | null; // naamgeving: software-versie
version: string | null;
created: string | null;
updated: string | null;
description: string | null;
// Reference attributes
software: ObjectReference | null; // -> Software
status: ObjectReference | null;
}
/** Manual (Jira Type ID: 430, 23 objects) */
export interface Manual extends BaseCMDBObject {
_objectType: 'Manual';
// Scalar attributes
key: string | null;
name: string | null; // The name of the object
link: number[]; // Confluence manual page
created: string | null;
updated: string | null;
// Reference attributes
type: ObjectReference | null;
target: ObjectReference[]; // -> Manual
}
/** Measures (Jira Type ID: 391, 11 objects) */
export interface Measures extends BaseCMDBObject {
_objectType: 'Measures';
// Scalar attributes
key: string | null;
name: string | null; // The name of the object
description: string | null;
obliged: string | null;
created: string | null;
updated: string | null;
// Reference attributes
availability: ObjectReference | null;
optional: ObjectReference | null;
}
/** Rebootgroups (Jira Type ID: 251, 14 objects) */
export interface Rebootgroups extends BaseCMDBObject {
_objectType: 'Rebootgroups';
// Scalar attributes
key: string | null;
name: string | null; // The name of the object
rebootDay: string | null;
rebootTime: string | null;
installInformation: string | null;
created: string | null;
updated: string | null;
}
/** Application Management - Team (Jira Type ID: 440, 11 objects) */
export interface ApplicationManagementTeam extends BaseCMDBObject {
_objectType: 'ApplicationManagementTeam';
// Scalar attributes
key: string | null;
name: string | null; // The name of the object
created: string | null;
updated: string | null;
// Reference attributes
type: ObjectReference | null;
}
/** Application Management - Subteam (Jira Type ID: 435, 19 objects) */
export interface ApplicationManagementSubteam extends BaseCMDBObject {
_objectType: 'ApplicationManagementSubteam';
// Scalar attributes
key: string | null;
name: string | null; // The name of the object
created: string | null;
updated: string | null;
description: string | null;
// Reference attributes
applicationManagementTeam: ObjectReference | null; // -> ApplicationManagementTeam
}
/** Assets (Jira Type ID: 42, 0 objects) */
export interface Assets extends BaseCMDBObject {
_objectType: 'Assets';
// Scalar attributes
key: string | null;
name: string | null; // The name of the object
created: string | null;
updated: string | null;
}
/** CI (Jira Type ID: 392, 0 objects) */
export interface CI extends BaseCMDBObject {
_objectType: 'CI';
// Scalar attributes
key: string | null;
name: string | null; // The name of the object
created: string | null;
updated: string | null;
}
/** Metadata (Jira Type ID: 57, 0 objects) */
export interface Metadata extends BaseCMDBObject {
_objectType: 'Metadata';
// Scalar attributes
key: string | null;
name: string | null; // The name of the object
description: string | null;
created: string | null;
updated: string | null;
}
/** Application Management - Hosting (Jira Type ID: 438, 4 objects) */
export interface ApplicationManagementHosting extends BaseCMDBObject {
_objectType: 'ApplicationManagementHosting';
// Scalar attributes
key: string | null;
name: string | null; // The name of the object
created: string | null;
updated: string | null;
description: string | null;
}
/** Application Management - TAM (Jira Type ID: 439, 4 objects) */
export interface ApplicationManagementTam extends BaseCMDBObject {
_objectType: 'ApplicationManagementTam';
// Scalar attributes
key: string | null;
name: string | null; // The name of the object
created: string | null;
updated: string | null;
description: string | null;
}
/** Application Management - Number of Users (Jira Type ID: 433, 7 objects) */
export interface ApplicationManagementNumberOfUsers extends BaseCMDBObject {
_objectType: 'ApplicationManagementNumberOfUsers';
// Scalar attributes
key: string | null;
name: string | null; // The name of the object
created: string | null;
updated: string | null;
examples: string | null;
factor: number | null;
order: number | null;
}
/** Certificate Type (Jira Type ID: 407, 3 objects) */
export interface CertificateType extends BaseCMDBObject {
_objectType: 'CertificateType';
// Scalar attributes
key: string | null;
name: string | null; // The name of the object
created: string | null;
updated: string | null;
}
/** ApplicationFunctionCategory (Jira Type ID: 437, 13 objects) */
export interface ApplicationFunctionCategory extends BaseCMDBObject {
_objectType: 'ApplicationFunctionCategory';
// Scalar attributes
key: string | null;
name: string | null; // The name of the object
created: string | null;
updated: string | null;
description: string | null;
}
/** ApplicationFunction (Jira Type ID: 403, 93 objects) */
export interface ApplicationFunction extends BaseCMDBObject {
_objectType: 'ApplicationFunction';
// Scalar attributes
appFuncGUID: string | null;
key: string | null;
name: string | null; // The name of the object
description: string | null;
keywords: string | null;
created: string | null;
updated: string | null;
// Reference attributes
applicationFunctionCategory: ObjectReference | null; // -> ApplicationFunctionCategory
applicationManagementApplicationCluster: ObjectReference | null; // -> ApplicationManagementSubteam
}
/** Certificate ClassificationType (Jira Type ID: 408, 8 objects) */
export interface CertificateClassificationType extends BaseCMDBObject {
_objectType: 'CertificateClassificationType';
// Scalar attributes
key: string | null;
name: string | null; // The name of the object
description: string | null; // https://jira.zuyderland.nl/browse/CMDB-430
created: string | null;
updated: string | null;
}
/** Business Impact Analyse (Jira Type ID: 41, 6 objects) */
export interface BusinessImpactAnalyse extends BaseCMDBObject {
_objectType: 'BusinessImpactAnalyse';
// Scalar attributes
key: string | null;
name: string | null; // The name of the object
serviceWindow: string | null;
availabilty: string | null;
biaCheck: string | null;
recoveryTimeObjectiveRTO: string | null;
recoveryPointObjectiveRPO: string | null;
environments: string | null;
description: string | null;
indicators: string | null;
created: string | null;
updated: string | null;
}
/** Business Importance (Jira Type ID: 44, 8 objects) */
export interface BusinessImportance extends BaseCMDBObject {
_objectType: 'BusinessImportance';
// Scalar attributes
key: string | null;
name: string | null; // The name of the object
description: string | null;
created: string | null;
updated: string | null;
}
/** Hosting Type (Jira Type ID: 39, 6 objects) */
export interface HostingType extends BaseCMDBObject {
_objectType: 'HostingType';
// Scalar attributes
key: string | null;
name: string | null; // The name of the object
description: string | null;
created: string | null;
updated: string | null;
}
/** Organisation (Jira Type ID: 390, 6 objects) */
export interface Organisation extends BaseCMDBObject {
_objectType: 'Organisation';
// Scalar attributes
key: string | null;
name: string | null; // The name of the object
description: string | null;
created: string | null;
updated: string | null;
}
/** Application Management - Application Type (Jira Type ID: 436, 4 objects) */
export interface ApplicationManagementApplicationType extends BaseCMDBObject {
_objectType: 'ApplicationManagementApplicationType';
// Scalar attributes
key: string | null;
name: string | null; // The name of the object
created: string | null;
updated: string | null;
description: string | null;
}
/** Application Management - Dynamics Factor (Jira Type ID: 434, 4 objects) */
export interface ApplicationManagementDynamicsFactor extends BaseCMDBObject {
_objectType: 'ApplicationManagementDynamicsFactor';
// Scalar attributes
key: string | null;
name: string | null; // The name of the object
created: string | null;
updated: string | null;
summary: string | null;
description: string | null;
factor: number | null;
}
/** Application Management - Complexity Factor (Jira Type ID: 432, 4 objects) */
export interface ApplicationManagementComplexityFactor extends BaseCMDBObject {
_objectType: 'ApplicationManagementComplexityFactor';
// Scalar attributes
key: string | null;
name: string | null; // The name of the object
created: string | null;
updated: string | null;
summary: string | null;
description: string | null;
factor: number | null;
}
/** ICT Governance Model (Jira Type ID: 431, 6 objects) */
export interface IctGovernanceModel extends BaseCMDBObject {
_objectType: 'IctGovernanceModel';
// Scalar attributes
key: string | null;
name: string | null; // The name of the object
created: string | null;
updated: string | null;
summary: string | null;
description: string | null;
remarks: string | null;
application: string | null;
}
// =============================================================================
// Union Types
// =============================================================================
/** Union of all CMDB object types */
export type CMDBObject =
| ApplicationComponent
| Flows
| Server
| AzureSubscription
| PackageBuild
| Package
| Certificate
| PrivilegedUser
| Domain
| Supplier
| Software
| SoftwarePatch
| Manual
| Measures
| Rebootgroups
| ApplicationManagementTeam
| ApplicationManagementSubteam
| Assets
| CI
| Metadata
| ApplicationManagementHosting
| ApplicationManagementTam
| ApplicationManagementNumberOfUsers
| CertificateType
| ApplicationFunctionCategory
| ApplicationFunction
| CertificateClassificationType
| BusinessImpactAnalyse
| BusinessImportance
| HostingType
| Organisation
| ApplicationManagementApplicationType
| ApplicationManagementDynamicsFactor
| ApplicationManagementComplexityFactor
| IctGovernanceModel;
/** All valid object type names */
export type CMDBObjectTypeName =
| 'ApplicationComponent'
| 'Flows'
| 'Server'
| 'AzureSubscription'
| 'PackageBuild'
| 'Package'
| 'Certificate'
| 'PrivilegedUser'
| 'Domain'
| 'Supplier'
| 'Software'
| 'SoftwarePatch'
| 'Manual'
| 'Measures'
| 'Rebootgroups'
| 'ApplicationManagementTeam'
| 'ApplicationManagementSubteam'
| 'Assets'
| 'CI'
| 'Metadata'
| 'ApplicationManagementHosting'
| 'ApplicationManagementTam'
| 'ApplicationManagementNumberOfUsers'
| 'CertificateType'
| 'ApplicationFunctionCategory'
| 'ApplicationFunction'
| 'CertificateClassificationType'
| 'BusinessImpactAnalyse'
| 'BusinessImportance'
| 'HostingType'
| 'Organisation'
| 'ApplicationManagementApplicationType'
| 'ApplicationManagementDynamicsFactor'
| 'ApplicationManagementComplexityFactor'
| 'IctGovernanceModel';
// =============================================================================
// Type Guards
// =============================================================================
export function isApplicationComponent(obj: CMDBObject): obj is ApplicationComponent {
return obj._objectType === 'ApplicationComponent';
}
export function isFlows(obj: CMDBObject): obj is Flows {
return obj._objectType === 'Flows';
}
export function isServer(obj: CMDBObject): obj is Server {
return obj._objectType === 'Server';
}
export function isAzureSubscription(obj: CMDBObject): obj is AzureSubscription {
return obj._objectType === 'AzureSubscription';
}
export function isPackageBuild(obj: CMDBObject): obj is PackageBuild {
return obj._objectType === 'PackageBuild';
}
export function isPackage(obj: CMDBObject): obj is Package {
return obj._objectType === 'Package';
}
export function isCertificate(obj: CMDBObject): obj is Certificate {
return obj._objectType === 'Certificate';
}
export function isPrivilegedUser(obj: CMDBObject): obj is PrivilegedUser {
return obj._objectType === 'PrivilegedUser';
}
export function isDomain(obj: CMDBObject): obj is Domain {
return obj._objectType === 'Domain';
}
export function isSupplier(obj: CMDBObject): obj is Supplier {
return obj._objectType === 'Supplier';
}
export function isSoftware(obj: CMDBObject): obj is Software {
return obj._objectType === 'Software';
}
export function isSoftwarePatch(obj: CMDBObject): obj is SoftwarePatch {
return obj._objectType === 'SoftwarePatch';
}
export function isManual(obj: CMDBObject): obj is Manual {
return obj._objectType === 'Manual';
}
export function isMeasures(obj: CMDBObject): obj is Measures {
return obj._objectType === 'Measures';
}
export function isRebootgroups(obj: CMDBObject): obj is Rebootgroups {
return obj._objectType === 'Rebootgroups';
}
export function isApplicationManagementTeam(obj: CMDBObject): obj is ApplicationManagementTeam {
return obj._objectType === 'ApplicationManagementTeam';
}
export function isApplicationManagementSubteam(obj: CMDBObject): obj is ApplicationManagementSubteam {
return obj._objectType === 'ApplicationManagementSubteam';
}
export function isAssets(obj: CMDBObject): obj is Assets {
return obj._objectType === 'Assets';
}
export function isCI(obj: CMDBObject): obj is CI {
return obj._objectType === 'CI';
}
export function isMetadata(obj: CMDBObject): obj is Metadata {
return obj._objectType === 'Metadata';
}
export function isApplicationManagementHosting(obj: CMDBObject): obj is ApplicationManagementHosting {
return obj._objectType === 'ApplicationManagementHosting';
}
export function isApplicationManagementTam(obj: CMDBObject): obj is ApplicationManagementTam {
return obj._objectType === 'ApplicationManagementTam';
}
export function isApplicationManagementNumberOfUsers(obj: CMDBObject): obj is ApplicationManagementNumberOfUsers {
return obj._objectType === 'ApplicationManagementNumberOfUsers';
}
export function isCertificateType(obj: CMDBObject): obj is CertificateType {
return obj._objectType === 'CertificateType';
}
export function isApplicationFunctionCategory(obj: CMDBObject): obj is ApplicationFunctionCategory {
return obj._objectType === 'ApplicationFunctionCategory';
}
export function isApplicationFunction(obj: CMDBObject): obj is ApplicationFunction {
return obj._objectType === 'ApplicationFunction';
}
export function isCertificateClassificationType(obj: CMDBObject): obj is CertificateClassificationType {
return obj._objectType === 'CertificateClassificationType';
}
export function isBusinessImpactAnalyse(obj: CMDBObject): obj is BusinessImpactAnalyse {
return obj._objectType === 'BusinessImpactAnalyse';
}
export function isBusinessImportance(obj: CMDBObject): obj is BusinessImportance {
return obj._objectType === 'BusinessImportance';
}
export function isHostingType(obj: CMDBObject): obj is HostingType {
return obj._objectType === 'HostingType';
}
export function isOrganisation(obj: CMDBObject): obj is Organisation {
return obj._objectType === 'Organisation';
}
export function isApplicationManagementApplicationType(obj: CMDBObject): obj is ApplicationManagementApplicationType {
return obj._objectType === 'ApplicationManagementApplicationType';
}
export function isApplicationManagementDynamicsFactor(obj: CMDBObject): obj is ApplicationManagementDynamicsFactor {
return obj._objectType === 'ApplicationManagementDynamicsFactor';
}
export function isApplicationManagementComplexityFactor(obj: CMDBObject): obj is ApplicationManagementComplexityFactor {
return obj._objectType === 'ApplicationManagementComplexityFactor';
}
export function isIctGovernanceModel(obj: CMDBObject): obj is IctGovernanceModel {
return obj._objectType === 'IctGovernanceModel';
}

View File

@@ -6,13 +6,18 @@ import cookieParser from 'cookie-parser';
import { config, validateConfig } from './config/env.js';
import { logger } from './services/logger.js';
import { dataService } from './services/dataService.js';
import { syncEngine } from './services/syncEngine.js';
import { cmdbService } from './services/cmdbService.js';
import applicationsRouter from './routes/applications.js';
import classificationsRouter from './routes/classifications.js';
import referenceDataRouter from './routes/referenceData.js';
import dashboardRouter from './routes/dashboard.js';
import configurationRouter from './routes/configuration.js';
import authRouter, { authMiddleware } from './routes/auth.js';
import { jiraAssetsService } from './services/jiraAssets.js';
import searchRouter from './routes/search.js';
import cacheRouter from './routes/cache.js';
import objectsRouter from './routes/objects.js';
import schemaRouter from './routes/schema.js';
// Validate configuration
validateConfig();
@@ -50,16 +55,16 @@ app.use((req, res, next) => {
// Auth middleware - extract session info for all requests
app.use(authMiddleware);
// Set user token on JiraAssets service for each request
// Set user token on CMDBService for each request (for user-specific OAuth)
app.use((req, res, next) => {
// Set user's OAuth token if available
if (req.accessToken) {
jiraAssetsService.setRequestToken(req.accessToken);
cmdbService.setUserToken(req.accessToken);
}
// Clear token after response is sent
res.on('finish', () => {
jiraAssetsService.clearRequestToken();
cmdbService.clearUserToken();
});
next();
@@ -68,12 +73,19 @@ app.use((req, res, next) => {
// Health check
app.get('/health', async (req, res) => {
const jiraConnected = await dataService.testConnection();
const cacheStatus = dataService.getCacheStatus();
res.json({
status: 'ok',
timestamp: new Date().toISOString(),
dataSource: dataService.isUsingJiraAssets() ? 'jira-assets' : 'mock-data',
dataSource: dataService.isUsingJiraAssets() ? 'jira-assets-cached' : 'mock-data',
jiraConnected: dataService.isUsingJiraAssets() ? jiraConnected : null,
aiConfigured: !!config.anthropicApiKey,
cache: {
isWarm: cacheStatus.isWarm,
objectCount: cacheStatus.totalObjects,
lastSync: cacheStatus.lastIncrementalSync,
},
});
});
@@ -91,6 +103,10 @@ app.use('/api/classifications', classificationsRouter);
app.use('/api/reference-data', referenceDataRouter);
app.use('/api/dashboard', dashboardRouter);
app.use('/api/configuration', configurationRouter);
app.use('/api/search', searchRouter);
app.use('/api/cache', cacheRouter);
app.use('/api/objects', objectsRouter);
app.use('/api/schema', schemaRouter);
// Error handling
app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
@@ -108,20 +124,33 @@ app.use((req, res) => {
// Start server
const PORT = config.port;
app.listen(PORT, () => {
app.listen(PORT, async () => {
logger.info(`Server running on http://localhost:${PORT}`);
logger.info(`Environment: ${config.nodeEnv}`);
logger.info(`AI Classification: ${config.anthropicApiKey ? 'Configured' : 'Not configured'}`);
logger.info(`Jira Assets: ${config.jiraPat ? 'Configured' : 'Using mock data'}`);
logger.info(`Jira Assets: ${config.jiraPat ? 'Configured with caching' : 'Using mock data'}`);
// Initialize sync engine if using Jira Assets
if (config.jiraPat && config.jiraSchemaId) {
try {
await syncEngine.initialize();
logger.info('Sync Engine: Initialized and running');
} catch (error) {
logger.error('Failed to initialize sync engine', error);
}
}
});
// Graceful shutdown
process.on('SIGTERM', () => {
logger.info('SIGTERM signal received: closing HTTP server');
const shutdown = () => {
logger.info('Shutdown signal received: stopping services...');
// Stop sync engine
syncEngine.stop();
logger.info('Services stopped, exiting');
process.exit(0);
});
};
process.on('SIGINT', () => {
logger.info('SIGINT signal received: closing HTTP server');
process.exit(0);
});
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);

View File

@@ -1,9 +1,11 @@
import { Router, Request, Response } from 'express';
import { dataService } from '../services/dataService.js';
import { databaseService } from '../services/database.js';
import { cmdbService } from '../services/cmdbService.js';
import { logger } from '../services/logger.js';
import { calculateRequiredEffortApplicationManagement, calculateRequiredEffortApplicationManagementWithBreakdown } from '../services/effortCalculation.js';
import type { SearchFilters, ApplicationUpdateRequest, ReferenceValue, ClassificationResult, ApplicationDetails, ApplicationStatus } from '../types/index.js';
import { calculateRequiredEffortApplicationManagementWithBreakdown } from '../services/effortCalculation.js';
import type { SearchFilters, ReferenceValue, ClassificationResult, ApplicationDetails, ApplicationStatus } from '../types/index.js';
import type { Server, Flows, Certificate, Domain, AzureSubscription, CMDBObjectTypeName } from '../generated/jira-types.js';
const router = Router();
@@ -50,9 +52,12 @@ router.get('/team-dashboard', async (req: Request, res: Response) => {
});
// Get application by ID
// Query params:
// - mode=edit: Force refresh from Jira for editing (includes _jiraUpdatedAt for conflict detection)
router.get('/:id', async (req: Request, res: Response) => {
try {
const { id } = req.params;
const mode = req.query.mode as string | undefined;
// Don't treat special routes as application IDs
if (id === 'team-dashboard' || id === 'calculate-effort' || id === 'search') {
@@ -60,7 +65,10 @@ router.get('/:id', async (req: Request, res: Response) => {
return;
}
const application = await dataService.getApplicationById(id);
// Edit mode: force refresh from Jira for fresh data + conflict detection
const application = mode === 'edit'
? await dataService.getApplicationForEdit(id)
: await dataService.getApplicationById(id);
if (!application) {
res.status(404).json({ error: 'Application not found' });
@@ -74,19 +82,33 @@ router.get('/:id', async (req: Request, res: Response) => {
}
});
// Update application
// Update application with conflict detection
router.put('/:id', async (req: Request, res: Response) => {
try {
const { id } = req.params;
const updates = req.body as {
applicationFunctions?: ReferenceValue[];
dynamicsFactor?: ReferenceValue;
complexityFactor?: ReferenceValue;
numberOfUsers?: ReferenceValue;
governanceModel?: ReferenceValue;
source?: 'AI_ACCEPTED' | 'AI_MODIFIED' | 'MANUAL';
const { updates, _jiraUpdatedAt } = req.body as {
updates?: {
applicationFunctions?: ReferenceValue[];
dynamicsFactor?: ReferenceValue;
complexityFactor?: ReferenceValue;
numberOfUsers?: ReferenceValue;
governanceModel?: ReferenceValue;
applicationSubteam?: ReferenceValue;
applicationTeam?: ReferenceValue;
applicationType?: ReferenceValue;
hostingType?: ReferenceValue;
businessImpactAnalyse?: ReferenceValue;
overrideFTE?: number | null;
applicationManagementHosting?: string;
applicationManagementTAM?: string;
source?: 'AI_ACCEPTED' | 'AI_MODIFIED' | 'MANUAL';
};
_jiraUpdatedAt?: string;
};
// Support both new format (updates object) and legacy format (direct body)
const actualUpdates = updates || req.body;
const application = await dataService.getApplicationById(id);
if (!application) {
res.status(404).json({ error: 'Application not found' });
@@ -95,61 +117,111 @@ router.put('/:id', async (req: Request, res: Response) => {
// Build changes object for history
const changes: ClassificationResult['changes'] = {};
if (updates.applicationFunctions) {
if (actualUpdates.applicationFunctions) {
changes.applicationFunctions = {
from: application.applicationFunctions,
to: updates.applicationFunctions,
to: actualUpdates.applicationFunctions,
};
}
if (updates.dynamicsFactor) {
if (actualUpdates.dynamicsFactor) {
changes.dynamicsFactor = {
from: application.dynamicsFactor,
to: updates.dynamicsFactor,
to: actualUpdates.dynamicsFactor,
};
}
if (updates.complexityFactor) {
if (actualUpdates.complexityFactor) {
changes.complexityFactor = {
from: application.complexityFactor,
to: updates.complexityFactor,
to: actualUpdates.complexityFactor,
};
}
if (updates.numberOfUsers) {
if (actualUpdates.numberOfUsers) {
changes.numberOfUsers = {
from: application.numberOfUsers,
to: updates.numberOfUsers,
to: actualUpdates.numberOfUsers,
};
}
if (updates.governanceModel) {
if (actualUpdates.governanceModel) {
changes.governanceModel = {
from: application.governanceModel,
to: updates.governanceModel,
to: actualUpdates.governanceModel,
};
}
const success = await dataService.updateApplication(id, updates);
// Call updateApplication with conflict detection if _jiraUpdatedAt is provided
const result = await dataService.updateApplication(id, actualUpdates, _jiraUpdatedAt);
if (success) {
// Save to classification history
const classificationResult: ClassificationResult = {
applicationId: id,
applicationName: application.name,
changes,
source: updates.source || 'MANUAL',
timestamp: new Date(),
};
databaseService.saveClassificationResult(classificationResult);
const updatedApp = await dataService.getApplicationById(id);
res.json(updatedApp);
} else {
res.status(500).json({ error: 'Failed to update application' });
// Check for conflicts
if (!result.success && result.conflict) {
// Return 409 Conflict with details
res.status(409).json({
status: 'conflict',
message: 'Object is gewijzigd door iemand anders',
conflicts: result.conflict.conflicts,
jiraUpdatedAt: result.conflict.jiraUpdatedAt,
canMerge: result.conflict.canMerge,
warning: result.conflict.warning,
actions: {
forceOverwrite: true,
merge: result.conflict.canMerge || false,
discard: true,
},
});
return;
}
if (!result.success) {
res.status(500).json({ error: result.error || 'Failed to update application' });
return;
}
// Save to classification history
const classificationResult: ClassificationResult = {
applicationId: id,
applicationName: application.name,
changes,
source: actualUpdates.source || 'MANUAL',
timestamp: new Date(),
};
databaseService.saveClassificationResult(classificationResult);
// Return updated application
const updatedApp = result.data || await dataService.getApplicationById(id);
res.json(updatedApp);
} catch (error) {
logger.error('Failed to update application', error);
res.status(500).json({ error: 'Failed to update application' });
}
});
// Force update (ignore conflicts)
router.put('/:id/force', async (req: Request, res: Response) => {
try {
const { id } = req.params;
const updates = req.body;
const application = await dataService.getApplicationById(id);
if (!application) {
res.status(404).json({ error: 'Application not found' });
return;
}
// Force update without conflict check
const result = await dataService.updateApplication(id, updates);
if (!result.success) {
res.status(500).json({ error: result.error || 'Failed to update application' });
return;
}
const updatedApp = result.data || await dataService.getApplicationById(id);
res.json(updatedApp);
} catch (error) {
logger.error('Failed to force update application', error);
res.status(500).json({ error: 'Failed to force update application' });
}
});
// Calculate FTE effort for an application (real-time calculation without saving)
router.post('/calculate-effort', async (req: Request, res: Response) => {
try {
@@ -180,7 +252,8 @@ router.post('/calculate-effort', async (req: Request, res: Response) => {
complexityFactor: applicationData.complexityFactor || null,
numberOfUsers: applicationData.numberOfUsers || null,
governanceModel: applicationData.governanceModel || null,
applicationCluster: applicationData.applicationCluster || null,
applicationSubteam: applicationData.applicationSubteam || null,
applicationTeam: applicationData.applicationTeam || null,
applicationType: applicationData.applicationType || null,
platform: applicationData.platform || null,
requiredEffortApplicationManagement: null,
@@ -214,4 +287,120 @@ router.get('/:id/history', async (req: Request, res: Response) => {
}
});
// Get related objects for an application (from cache)
router.get('/:id/related/:objectType', async (req: Request, res: Response) => {
try {
const { id, objectType } = req.params;
// Map object type string to CMDBObjectTypeName
const typeMap: Record<string, CMDBObjectTypeName> = {
'Server': 'Server',
'server': 'Server',
'Flows': 'Flows',
'flows': 'Flows',
'Flow': 'Flows',
'flow': 'Flows',
'Connection': 'Flows', // Frontend uses "Connection" for Flows
'connection': 'Flows',
'Certificate': 'Certificate',
'certificate': 'Certificate',
'Domain': 'Domain',
'domain': 'Domain',
'AzureSubscription': 'AzureSubscription',
'azuresubscription': 'AzureSubscription',
};
const typeName = typeMap[objectType];
if (!typeName) {
res.status(400).json({ error: `Unknown object type: ${objectType}` });
return;
}
// Use CMDBService to get related objects from cache
type RelatedObjectType = Server | Flows | Certificate | Domain | AzureSubscription;
let relatedObjects: RelatedObjectType[] = [];
switch (typeName) {
case 'Server':
relatedObjects = await cmdbService.getReferencingObjects<Server>(id, 'Server');
break;
case 'Flows': {
// Flows reference ApplicationComponents via Source and Target attributes
// We need to find Flows where this ApplicationComponent is the target of the reference
relatedObjects = await cmdbService.getReferencingObjects<Flows>(id, 'Flows');
break;
}
case 'Certificate':
relatedObjects = await cmdbService.getReferencingObjects<Certificate>(id, 'Certificate');
break;
case 'Domain':
relatedObjects = await cmdbService.getReferencingObjects<Domain>(id, 'Domain');
break;
case 'AzureSubscription':
relatedObjects = await cmdbService.getReferencingObjects<AzureSubscription>(id, 'AzureSubscription');
break;
default:
relatedObjects = [];
}
// Get requested attributes from query string
const requestedAttrs = req.query.attributes
? String(req.query.attributes).split(',').map(a => a.trim())
: [];
// Format response - must match RelatedObjectsResponse type expected by frontend
const objects = relatedObjects.map(obj => {
// Extract attributes from the object
const attributes: Record<string, string | null> = {};
const objData = obj as Record<string, unknown>;
// If specific attributes are requested, extract those
if (requestedAttrs.length > 0) {
for (const attrName of requestedAttrs) {
// Convert attribute name to camelCase field name
const fieldName = attrName.charAt(0).toLowerCase() + attrName.slice(1).replace(/\s+/g, '');
const value = objData[fieldName] ?? objData[attrName.toLowerCase()] ?? objData[attrName];
if (value === null || value === undefined) {
attributes[attrName] = null;
} else if (typeof value === 'object' && value !== null) {
// ObjectReference - extract label
const ref = value as { label?: string; name?: string; displayValue?: string };
attributes[attrName] = ref.label || ref.name || ref.displayValue || null;
} else {
attributes[attrName] = String(value);
}
}
} else {
// No specific attributes requested - include common ones
if ('status' in objData) {
const status = objData.status;
if (typeof status === 'object' && status !== null) {
attributes['Status'] = (status as { label?: string }).label || String(status);
} else if (status) {
attributes['Status'] = String(status);
}
}
if ('state' in objData) {
attributes['State'] = objData.state ? String(objData.state) : null;
}
}
return {
id: obj.id,
key: obj.objectKey,
label: obj.label,
name: obj.label,
objectType: obj._objectType,
attributes,
};
});
res.json({ objects, total: objects.length });
} catch (error) {
logger.error(`Failed to get related ${req.params.objectType} objects`, error);
res.status(500).json({ error: `Failed to get related objects` });
}
});
export default router;

View File

@@ -18,9 +18,14 @@ declare global {
// Get auth configuration
router.get('/config', (req: Request, res: Response) => {
const authMethod = authService.getAuthMethod();
res.json({
// Configured authentication method ('pat', 'oauth', or 'none')
authMethod,
// Legacy fields for backward compatibility
oauthEnabled: authService.isOAuthEnabled(),
serviceAccountEnabled: authService.isUsingServiceAccount(),
// Jira host for display purposes
jiraHost: config.jiraHost,
});
});

135
backend/src/routes/cache.ts Normal file
View File

@@ -0,0 +1,135 @@
/**
* Cache management routes
*
* Provides endpoints for cache status and manual sync triggers.
*/
import { Router, Request, Response } from 'express';
import { cacheStore } from '../services/cacheStore.js';
import { syncEngine } from '../services/syncEngine.js';
import { logger } from '../services/logger.js';
import { OBJECT_TYPES } from '../generated/jira-schema.js';
import type { CMDBObjectTypeName } from '../generated/jira-types.js';
const router = Router();
// Get cache status
router.get('/status', (req: Request, res: Response) => {
try {
const cacheStats = cacheStore.getStats();
const syncStatus = syncEngine.getStatus();
res.json({
cache: cacheStats,
sync: syncStatus,
supportedTypes: Object.keys(OBJECT_TYPES),
});
} catch (error) {
logger.error('Failed to get cache status', error);
res.status(500).json({ error: 'Failed to get cache status' });
}
});
// Trigger full sync
router.post('/sync', async (req: Request, res: Response) => {
try {
logger.info('Manual full sync triggered');
// Don't wait for completion - return immediately
syncEngine.fullSync().catch(err => {
logger.error('Full sync failed', err);
});
res.json({
status: 'started',
message: 'Full sync started in background',
});
} catch (error) {
logger.error('Failed to trigger full sync', error);
res.status(500).json({ error: 'Failed to trigger sync' });
}
});
// Trigger sync for a specific object type
router.post('/sync/:objectType', async (req: Request, res: Response) => {
try {
const { objectType } = req.params;
// Validate object type
if (!OBJECT_TYPES[objectType]) {
res.status(400).json({
error: `Unknown object type: ${objectType}`,
supportedTypes: Object.keys(OBJECT_TYPES),
});
return;
}
logger.info(`Manual sync triggered for ${objectType}`);
const result = await syncEngine.syncType(objectType as CMDBObjectTypeName);
res.json({
status: 'completed',
objectType,
stats: result,
});
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Failed to sync object type';
logger.error(`Failed to sync object type ${req.params.objectType}`, error);
// Return 409 (Conflict) if sync is already in progress, otherwise 500
const statusCode = errorMessage.includes('already in progress') ? 409 : 500;
res.status(statusCode).json({
error: errorMessage,
objectType: req.params.objectType,
});
}
});
// Clear cache for a specific type
router.delete('/clear/:objectType', (req: Request, res: Response) => {
try {
const { objectType } = req.params;
if (!OBJECT_TYPES[objectType]) {
res.status(400).json({
error: `Unknown object type: ${objectType}`,
supportedTypes: Object.keys(OBJECT_TYPES),
});
return;
}
logger.info(`Clearing cache for ${objectType}`);
const deleted = cacheStore.clearObjectType(objectType as CMDBObjectTypeName);
res.json({
status: 'cleared',
objectType,
deletedCount: deleted,
});
} catch (error) {
logger.error(`Failed to clear cache for ${req.params.objectType}`, error);
res.status(500).json({ error: 'Failed to clear cache' });
}
});
// Clear entire cache
router.delete('/clear', (req: Request, res: Response) => {
try {
logger.info('Clearing entire cache');
cacheStore.clearAll();
res.json({
status: 'cleared',
message: 'Entire cache cleared',
});
} catch (error) {
logger.error('Failed to clear cache', error);
res.status(500).json({ error: 'Failed to clear cache' });
}
});
export default router;

View File

@@ -1,10 +1,15 @@
import { Router, Request, Response } from 'express';
import { readFile, writeFile } from 'fs/promises';
import { join } from 'path';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import { logger } from '../services/logger.js';
import { clearEffortCalculationConfigCache, getEffortCalculationConfigV25 } from '../services/effortCalculation.js';
import type { EffortCalculationConfig, EffortCalculationConfigV25 } from '../config/effortCalculation.js';
// Get __dirname equivalent for ES modules
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const router = Router();
// Path to the configuration files

View File

@@ -1,18 +1,21 @@
import { Router, Request, Response } from 'express';
import { dataService } from '../services/dataService.js';
import { databaseService } from '../services/database.js';
import { syncEngine } from '../services/syncEngine.js';
import { logger } from '../services/logger.js';
import { validateApplicationConfiguration } from '../services/effortCalculation.js';
import type { ApplicationDetails, ApplicationStatus, ReferenceValue } from '../types/index.js';
const router = Router();
// Simple in-memory cache for dashboard stats
interface CachedStats {
data: any;
data: unknown;
timestamp: number;
}
let statsCache: CachedStats | null = null;
const CACHE_TTL_MS = 3 * 60 * 1000; // 3 minutes cache (longer since jiraAssets also caches)
const CACHE_TTL_MS = 3 * 60 * 1000; // 3 minutes cache
// Get dashboard statistics
router.get('/stats', async (req: Request, res: Response) => {
@@ -24,7 +27,8 @@ router.get('/stats', async (req: Request, res: Response) => {
const now = Date.now();
if (!forceRefresh && statsCache && (now - statsCache.timestamp) < CACHE_TTL_MS) {
logger.debug('Returning cached dashboard stats');
return res.json(statsCache.data);
res.json(statsCache.data);
return;
}
logger.info('Dashboard: Fetching fresh stats...');
@@ -33,10 +37,29 @@ router.get('/stats', async (req: Request, res: Response) => {
const includeDistributions = req.query.distributions !== 'false';
const stats = await dataService.getStats(includeDistributions);
const dbStats = databaseService.getStats();
// Get cache status
const cacheStatus = dataService.getCacheStatus();
const syncStatus = syncEngine.getStatus();
const responseData = {
...stats,
classificationStats: dbStats,
cache: {
lastFullSync: cacheStatus.lastFullSync,
lastIncrementalSync: cacheStatus.lastIncrementalSync,
objectCount: cacheStatus.totalObjects,
objectsByType: cacheStatus.objectsByType,
totalRelations: cacheStatus.totalRelations,
isWarm: cacheStatus.isWarm,
dbSizeBytes: cacheStatus.dbSizeBytes,
syncStatus: {
isRunning: syncStatus.isRunning,
isSyncing: syncStatus.isSyncing,
nextIncrementalSync: syncStatus.nextIncrementalSync,
incrementalInterval: syncStatus.incrementalInterval,
},
},
};
// Update cache
@@ -53,11 +76,12 @@ router.get('/stats', async (req: Request, res: Response) => {
// Return cached data if available (even if expired)
if (statsCache) {
logger.info('Dashboard: Returning stale cached data due to error');
return res.json({
...statsCache.data,
res.json({
...statsCache.data as object,
stale: true,
error: 'Using cached data due to API timeout',
});
return;
}
res.status(500).json({ error: 'Failed to get dashboard stats' });
@@ -76,4 +100,101 @@ router.get('/recent', (req: Request, res: Response) => {
}
});
// Get applications with governance model validation issues
router.get('/governance-analysis', async (req: Request, res: Response) => {
try {
logger.info('Governance Analysis: Fetching all applications for validation...');
// Use batched fetching to avoid timeouts
const pageSize = 50; // Smaller batch size for reliability
// Include all statuses so they can be filtered client-side (including Closed)
const statuses: ApplicationStatus[] = ['In Production', 'Implementation', 'Proof of Concept', 'End of support', 'End of life', 'Deprecated', 'Shadow IT', 'Undefined', 'Closed'];
let allApplications: Array<{ id: string; key: string; name: string; status: ApplicationStatus | null; governanceModel?: ReferenceValue | null; applicationType?: ReferenceValue | null }> = [];
let currentPage = 1;
let totalCount = 0;
let hasMore = true;
// Fetch applications in batches
while (hasMore) {
try {
const searchResult = await dataService.searchApplications(
{ statuses },
currentPage,
pageSize
);
if (currentPage === 1) {
totalCount = searchResult.totalCount;
logger.info(`Governance Analysis: Total applications to process: ${totalCount}`);
}
allApplications = allApplications.concat(searchResult.applications as typeof allApplications);
hasMore = searchResult.applications.length === pageSize && allApplications.length < totalCount;
currentPage++;
// Safety limit to prevent infinite loops
if (currentPage > 100) {
logger.warn('Governance Analysis: Reached page limit, stopping fetch');
break;
}
} catch (fetchError) {
logger.error(`Governance Analysis: Error fetching page ${currentPage}`, fetchError);
// Continue with what we have if a single batch fails
hasMore = false;
}
}
logger.info(`Governance Analysis: Fetched ${allApplications.length} applications, validating...`);
const applicationsWithIssues: Array<{
id: string;
key: string;
name: string;
status: string | null;
governanceModel: string | null;
businessImpactAnalyse: string | null;
applicationType: string | null;
warnings: string[];
errors: string[];
}> = [];
// Process each application
for (const app of allApplications) {
// Get full application details for validation
const fullApp = await dataService.getApplicationById(app.id);
if (!fullApp) continue;
const validation = validateApplicationConfiguration(fullApp as ApplicationDetails);
// Only include applications with ERRORS (red warnings)
// Applications with only warnings (yellow) are excluded
if (validation.errors.length > 0) {
applicationsWithIssues.push({
id: app.id,
key: app.key,
name: app.name,
status: app.status,
governanceModel: app.governanceModel?.name || null,
businessImpactAnalyse: fullApp.businessImpactAnalyse?.name || null,
applicationType: app.applicationType?.name || null,
warnings: validation.warnings,
errors: validation.errors,
});
}
}
logger.info(`Governance Analysis: Found ${applicationsWithIssues.length} applications with validation issues`);
res.json({
totalApplications: totalCount,
applicationsWithIssues: applicationsWithIssues.length,
applications: applicationsWithIssues,
});
} catch (error) {
logger.error('Failed to get governance analysis', error);
res.status(500).json({ error: 'Failed to get governance analysis' });
}
});
export default router;

View File

@@ -0,0 +1,176 @@
/**
* Generic object routes
*
* Provides schema-driven access to all CMDB object types.
*/
import { Router, Request, Response } from 'express';
import { cmdbService } from '../services/cmdbService.js';
import { logger } from '../services/logger.js';
import { OBJECT_TYPES } from '../generated/jira-schema.js';
import type { CMDBObject, CMDBObjectTypeName } from '../generated/jira-types.js';
const router = Router();
// Get list of supported object types
router.get('/', (req: Request, res: Response) => {
const types = Object.entries(OBJECT_TYPES).map(([typeName, def]) => ({
typeName,
jiraTypeId: def.jiraTypeId,
name: def.name,
syncPriority: def.syncPriority,
attributeCount: def.attributes.length,
}));
res.json({
types,
totalTypes: types.length,
});
});
// Get all objects of a type
router.get('/:type', async (req: Request, res: Response) => {
try {
const { type } = req.params;
const limit = parseInt(req.query.limit as string) || 1000;
const offset = parseInt(req.query.offset as string) || 0;
const search = req.query.search as string | undefined;
// Validate type
if (!OBJECT_TYPES[type]) {
res.status(400).json({
error: `Unknown object type: ${type}`,
supportedTypes: Object.keys(OBJECT_TYPES),
});
return;
}
const objects = await cmdbService.getObjects(type as CMDBObjectTypeName, {
limit,
offset,
searchTerm: search,
});
const count = cmdbService.countObjects(type as CMDBObjectTypeName);
res.json({
objectType: type,
objects,
count: objects.length,
totalCount: count,
offset,
limit,
});
} catch (error) {
logger.error(`Failed to get objects of type ${req.params.type}`, error);
res.status(500).json({ error: 'Failed to get objects' });
}
});
// Get a specific object by ID
router.get('/:type/:id', async (req: Request, res: Response) => {
try {
const { type, id } = req.params;
const forceRefresh = req.query.refresh === 'true';
// Validate type
if (!OBJECT_TYPES[type]) {
res.status(400).json({
error: `Unknown object type: ${type}`,
supportedTypes: Object.keys(OBJECT_TYPES),
});
return;
}
const object = await cmdbService.getObject(type as CMDBObjectTypeName, id, {
forceRefresh,
});
if (!object) {
res.status(404).json({ error: 'Object not found' });
return;
}
res.json(object);
} catch (error) {
logger.error(`Failed to get object ${req.params.type}/${req.params.id}`, error);
res.status(500).json({ error: 'Failed to get object' });
}
});
// Get related objects
router.get('/:type/:id/related/:relationType', async (req: Request, res: Response) => {
try {
const { type, id, relationType } = req.params;
const attribute = req.query.attribute as string | undefined;
// Validate types
if (!OBJECT_TYPES[type]) {
res.status(400).json({ error: `Unknown source type: ${type}` });
return;
}
if (!OBJECT_TYPES[relationType]) {
res.status(400).json({ error: `Unknown relation type: ${relationType}` });
return;
}
const relatedObjects = await cmdbService.getRelatedObjects<CMDBObject>(
id,
attribute || '',
relationType as CMDBObjectTypeName
);
res.json({
sourceId: id,
sourceType: type,
relationType,
attribute: attribute || null,
objects: relatedObjects,
count: relatedObjects.length,
});
} catch (error) {
logger.error(`Failed to get related objects`, error);
res.status(500).json({ error: 'Failed to get related objects' });
}
});
// Get objects referencing this object (inbound references)
router.get('/:type/:id/referenced-by/:sourceType', async (req: Request, res: Response) => {
try {
const { type, id, sourceType } = req.params;
const attribute = req.query.attribute as string | undefined;
// Validate types
if (!OBJECT_TYPES[type]) {
res.status(400).json({ error: `Unknown target type: ${type}` });
return;
}
if (!OBJECT_TYPES[sourceType]) {
res.status(400).json({ error: `Unknown source type: ${sourceType}` });
return;
}
const referencingObjects = await cmdbService.getReferencingObjects<CMDBObject>(
id,
sourceType as CMDBObjectTypeName,
attribute
);
res.json({
targetId: id,
targetType: type,
sourceType,
attribute: attribute || null,
objects: referencingObjects,
count: referencingObjects.length,
});
} catch (error) {
logger.error(`Failed to get referencing objects`, error);
res.status(500).json({ error: 'Failed to get referencing objects' });
}
});
export default router;

View File

@@ -15,12 +15,14 @@ router.get('/', async (req: Request, res: Response) => {
organisations,
hostingTypes,
applicationFunctions,
applicationClusters,
applicationSubteams,
applicationTeams,
applicationTypes,
businessImportance,
businessImpactAnalyses,
applicationManagementHosting,
applicationManagementTAM,
subteamToTeamMapping,
] = await Promise.all([
dataService.getDynamicsFactors(),
dataService.getComplexityFactors(),
@@ -29,12 +31,14 @@ router.get('/', async (req: Request, res: Response) => {
dataService.getOrganisations(),
dataService.getHostingTypes(),
dataService.getApplicationFunctions(),
dataService.getApplicationClusters(),
dataService.getApplicationSubteams(),
dataService.getApplicationTeams(),
dataService.getApplicationTypes(),
dataService.getBusinessImportance(),
dataService.getBusinessImpactAnalyses(),
dataService.getApplicationManagementHosting(),
dataService.getApplicationManagementTAM(),
dataService.getSubteamToTeamMapping(),
]);
res.json({
@@ -45,12 +49,14 @@ router.get('/', async (req: Request, res: Response) => {
organisations,
hostingTypes,
applicationFunctions,
applicationClusters,
applicationSubteams,
applicationTeams,
applicationTypes,
businessImportance,
businessImpactAnalyses,
applicationManagementHosting,
applicationManagementTAM,
subteamToTeamMapping,
});
} catch (error) {
logger.error('Failed to get reference data', error);
@@ -135,14 +141,25 @@ router.get('/application-functions', async (req: Request, res: Response) => {
}
});
// Get application clusters (from Jira Assets)
router.get('/application-clusters', async (req: Request, res: Response) => {
// Get application subteams (from Jira Assets)
router.get('/application-subteams', async (req: Request, res: Response) => {
try {
const clusters = await dataService.getApplicationClusters();
res.json(clusters);
const subteams = await dataService.getApplicationSubteams();
res.json(subteams);
} catch (error) {
logger.error('Failed to get application clusters', error);
res.status(500).json({ error: 'Failed to get application clusters' });
logger.error('Failed to get application subteams', error);
res.status(500).json({ error: 'Failed to get application subteams' });
}
});
// Get application teams (from Jira Assets)
router.get('/application-teams', async (req: Request, res: Response) => {
try {
const teams = await dataService.getApplicationTeams();
res.json(teams);
} catch (error) {
logger.error('Failed to get application teams', error);
res.status(500).json({ error: 'Failed to get application teams' });
}
});

View File

@@ -0,0 +1,151 @@
import { Router } from 'express';
import { OBJECT_TYPES, SCHEMA_GENERATED_AT, SCHEMA_OBJECT_TYPE_COUNT, SCHEMA_TOTAL_ATTRIBUTES } from '../generated/jira-schema.js';
import type { ObjectTypeDefinition, AttributeDefinition } from '../generated/jira-schema.js';
const router = Router();
// Extended types for API response
interface ObjectTypeWithLinks extends ObjectTypeDefinition {
incomingLinks: Array<{
fromType: string;
fromTypeName: string;
attributeName: string;
isMultiple: boolean;
}>;
outgoingLinks: Array<{
toType: string;
toTypeName: string;
attributeName: string;
isMultiple: boolean;
}>;
}
interface SchemaResponse {
metadata: {
generatedAt: string;
objectTypeCount: number;
totalAttributes: number;
};
objectTypes: Record<string, ObjectTypeWithLinks>;
}
/**
* GET /api/schema
* Returns the complete Jira Assets schema with object types, attributes, and links
*/
router.get('/', (req, res) => {
try {
// Build links between object types
const objectTypesWithLinks: Record<string, ObjectTypeWithLinks> = {};
// First pass: convert all object types
for (const [typeName, typeDef] of Object.entries(OBJECT_TYPES)) {
objectTypesWithLinks[typeName] = {
...typeDef,
incomingLinks: [],
outgoingLinks: [],
};
}
// Second pass: build link relationships
for (const [typeName, typeDef] of Object.entries(OBJECT_TYPES)) {
for (const attr of typeDef.attributes) {
if (attr.type === 'reference' && attr.referenceTypeName) {
// Add outgoing link from this type
objectTypesWithLinks[typeName].outgoingLinks.push({
toType: attr.referenceTypeName,
toTypeName: OBJECT_TYPES[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,
});
}
}
}
}
const response: SchemaResponse = {
metadata: {
generatedAt: SCHEMA_GENERATED_AT,
objectTypeCount: SCHEMA_OBJECT_TYPE_COUNT,
totalAttributes: SCHEMA_TOTAL_ATTRIBUTES,
},
objectTypes: objectTypesWithLinks,
};
res.json(response);
} catch (error) {
console.error('Failed to get schema:', error);
res.status(500).json({ error: 'Failed to get schema' });
}
});
/**
* GET /api/schema/object-type/:typeName
* Returns details for a specific object type
*/
router.get('/object-type/:typeName', (req, res) => {
const { typeName } = req.params;
const typeDef = OBJECT_TYPES[typeName];
if (!typeDef) {
return res.status(404).json({ error: `Object type '${typeName}' not found` });
}
// Build links for this specific type
const incomingLinks: Array<{
fromType: string;
fromTypeName: string;
attributeName: string;
isMultiple: boolean;
}> = [];
const outgoingLinks: Array<{
toType: string;
toTypeName: string;
attributeName: string;
isMultiple: boolean;
}> = [];
// Outgoing links from this type
for (const attr of typeDef.attributes) {
if (attr.type === 'reference' && attr.referenceTypeName) {
outgoingLinks.push({
toType: attr.referenceTypeName,
toTypeName: OBJECT_TYPES[attr.referenceTypeName]?.name || attr.referenceTypeName,
attributeName: attr.name,
isMultiple: attr.isMultiple,
});
}
}
// Incoming links from other types
for (const [otherTypeName, otherTypeDef] of Object.entries(OBJECT_TYPES)) {
for (const attr of otherTypeDef.attributes) {
if (attr.type === 'reference' && attr.referenceTypeName === typeName) {
incomingLinks.push({
fromType: otherTypeName,
fromTypeName: otherTypeDef.name,
attributeName: attr.name,
isMultiple: attr.isMultiple,
});
}
}
}
res.json({
...typeDef,
incomingLinks,
outgoingLinks,
});
});
export default router;

View File

@@ -0,0 +1,74 @@
import { Router, Request, Response } from 'express';
import { cmdbService } from '../services/cmdbService.js';
import { logger } from '../services/logger.js';
import { config } from '../config/env.js';
const router = Router();
// CMDB free-text search endpoint (from cache)
router.get('/', async (req: Request, res: Response) => {
try {
const query = req.query.query as string;
const limit = parseInt(req.query.limit as string, 10) || 100;
if (!query || query.trim().length === 0) {
res.status(400).json({ error: 'Query parameter is required' });
return;
}
logger.info(`CMDB search request: query="${query}", limit=${limit}`);
// Search all types in cache
const results = await cmdbService.searchAllTypes(query.trim(), { limit });
// Group results by object type
const objectTypeMap = new Map<string, { id: number; name: string; iconUrl: string }>();
const formattedResults = results.map(obj => {
const typeName = obj._objectType || 'Unknown';
// Track unique object types
if (!objectTypeMap.has(typeName)) {
objectTypeMap.set(typeName, {
id: objectTypeMap.size + 1,
name: typeName,
iconUrl: '', // Can be enhanced to include actual icons
});
}
const objectType = objectTypeMap.get(typeName)!;
return {
id: parseInt(obj.id, 10) || 0,
key: obj.objectKey,
label: obj.label,
objectTypeId: objectType.id,
avatarUrl: '',
attributes: [], // Can be enhanced to include attributes
};
});
// Build response matching CMDBSearchResponse interface
const response = {
metadata: {
count: formattedResults.length,
offset: 0,
limit: limit,
total: formattedResults.length,
criteria: {
query: query,
type: 'global',
schema: parseInt(config.jiraSchemaId, 10) || 0,
},
},
objectTypes: Array.from(objectTypeMap.values()),
results: formattedResults,
};
res.json(response);
} catch (error) {
logger.error('CMDB search failed', error);
res.status(500).json({ error: 'Failed to search CMDB' });
}
});
export default router;

View File

@@ -268,14 +268,21 @@ class AuthService {
return existed;
}
// Check if OAuth is enabled
// Check if OAuth is enabled (jiraAuthMethod = 'oauth')
isOAuthEnabled(): boolean {
return config.jiraOAuthEnabled && !!config.jiraOAuthClientId && !!config.jiraOAuthClientSecret;
return config.jiraAuthMethod === 'oauth' && !!config.jiraOAuthClientId && !!config.jiraOAuthClientSecret;
}
// Check if using service account (PAT) fallback
// Check if using service account (PAT) mode (jiraAuthMethod = 'pat')
isUsingServiceAccount(): boolean {
return !this.isOAuthEnabled() && !!config.jiraPat;
return config.jiraAuthMethod === 'pat' && !!config.jiraPat;
}
// Get the configured authentication method
getAuthMethod(): 'pat' | 'oauth' | 'none' {
if (this.isOAuthEnabled()) return 'oauth';
if (this.isUsingServiceAccount()) return 'pat';
return 'none';
}
}

View File

@@ -0,0 +1,660 @@
/**
* CacheStore - SQLite cache operations for CMDB objects
*
* Provides fast local storage for CMDB data synced from Jira Assets.
* Uses the generated schema for type-safe operations.
*/
import Database from 'better-sqlite3';
import { join, dirname } from 'path';
import * as path from 'path';
import * as fs from 'fs';
import { logger } from './logger.js';
import type { CMDBObject, CMDBObjectTypeName, ObjectReference } from '../generated/jira-types.js';
import { getReferenceAttributes } from '../generated/jira-schema.js';
// Get current directory for ESM
const currentFileUrl = new URL(import.meta.url);
const __dirname = dirname(currentFileUrl.pathname);
const CACHE_DB_PATH = join(__dirname, '../../data/cmdb-cache.db');
export interface CacheStats {
totalObjects: number;
objectsByType: Record<string, number>;
totalRelations: number;
lastFullSync: string | null;
lastIncrementalSync: string | null;
isWarm: boolean;
dbSizeBytes: number;
}
export interface QueryOptions {
limit?: number;
offset?: number;
orderBy?: string;
orderDir?: 'ASC' | 'DESC';
}
class CacheStore {
private db: Database.Database;
private initialized: boolean = false;
constructor() {
// Ensure data directory exists
const dataDir = dirname(CACHE_DB_PATH);
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true });
}
this.db = new Database(CACHE_DB_PATH);
this.initialize();
}
private initialize(): void {
if (this.initialized) return;
// Read and execute the generated schema
const schemaPath = join(__dirname, '../generated/db-schema.sql');
if (fs.existsSync(schemaPath)) {
const schema = fs.readFileSync(schemaPath, 'utf-8');
this.db.exec(schema);
logger.info('CacheStore: Database schema initialized from generated file');
} else {
// Fallback: create tables directly
this.db.exec(`
CREATE TABLE IF NOT EXISTS cached_objects (
id TEXT PRIMARY KEY,
object_key TEXT NOT NULL UNIQUE,
object_type TEXT NOT NULL,
label TEXT NOT NULL,
data JSON NOT NULL,
jira_updated_at TEXT,
jira_created_at TEXT,
cached_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS object_relations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source_id TEXT NOT NULL,
target_id TEXT NOT NULL,
attribute_name TEXT NOT NULL,
source_type TEXT NOT NULL,
target_type TEXT NOT NULL,
UNIQUE(source_id, target_id, attribute_name)
);
CREATE TABLE IF NOT EXISTS sync_metadata (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_objects_type ON cached_objects(object_type);
CREATE INDEX IF NOT EXISTS idx_objects_key ON cached_objects(object_key);
CREATE INDEX IF NOT EXISTS idx_objects_updated ON cached_objects(jira_updated_at);
CREATE INDEX IF NOT EXISTS idx_objects_label ON cached_objects(label);
CREATE INDEX IF NOT EXISTS idx_relations_source ON object_relations(source_id);
CREATE INDEX IF NOT EXISTS idx_relations_target ON object_relations(target_id);
CREATE INDEX IF NOT EXISTS idx_relations_source_type ON object_relations(source_type);
CREATE INDEX IF NOT EXISTS idx_relations_target_type ON object_relations(target_type);
CREATE INDEX IF NOT EXISTS idx_relations_attr ON object_relations(attribute_name);
`);
logger.info('CacheStore: Database schema initialized (fallback)');
}
this.initialized = true;
}
// ==========================================================================
// Object CRUD Operations
// ==========================================================================
/**
* Get a single object by ID
*/
getObject<T extends CMDBObject>(typeName: CMDBObjectTypeName, id: string): T | null {
const stmt = this.db.prepare(`
SELECT data FROM cached_objects
WHERE id = ? AND object_type = ?
`);
const row = stmt.get(id, typeName) as { data: string } | undefined;
if (!row) return null;
try {
return JSON.parse(row.data) as T;
} catch (error) {
logger.error(`CacheStore: Failed to parse object ${id}`, error);
return null;
}
}
/**
* Get a single object by object key (e.g., "ICMT-123")
*/
getObjectByKey<T extends CMDBObject>(typeName: CMDBObjectTypeName, objectKey: string): T | null {
const stmt = this.db.prepare(`
SELECT data FROM cached_objects
WHERE object_key = ? AND object_type = ?
`);
const row = stmt.get(objectKey, typeName) as { data: string } | undefined;
if (!row) return null;
try {
return JSON.parse(row.data) as T;
} catch (error) {
logger.error(`CacheStore: Failed to parse object ${objectKey}`, error);
return null;
}
}
/**
* Get all objects of a specific type
*/
getObjects<T extends CMDBObject>(
typeName: CMDBObjectTypeName,
options?: QueryOptions
): T[] {
const limit = options?.limit || 10000;
const offset = options?.offset || 0;
const orderBy = options?.orderBy || 'label';
const orderDir = options?.orderDir || 'ASC';
const stmt = this.db.prepare(`
SELECT data FROM cached_objects
WHERE object_type = ?
ORDER BY ${orderBy} ${orderDir}
LIMIT ? OFFSET ?
`);
const rows = stmt.all(typeName, limit, offset) as { data: string }[];
return rows.map(row => {
try {
return JSON.parse(row.data) as T;
} catch {
return null;
}
}).filter((obj): obj is T => obj !== null);
}
/**
* Count objects of a specific type
*/
countObjects(typeName: CMDBObjectTypeName): number {
const stmt = this.db.prepare(`
SELECT COUNT(*) as count FROM cached_objects
WHERE object_type = ?
`);
const row = stmt.get(typeName) as { count: number };
return row.count;
}
/**
* Search objects by label (case-insensitive)
*/
searchByLabel<T extends CMDBObject>(
typeName: CMDBObjectTypeName,
searchTerm: string,
options?: QueryOptions
): T[] {
const limit = options?.limit || 100;
const offset = options?.offset || 0;
const stmt = this.db.prepare(`
SELECT data FROM cached_objects
WHERE object_type = ? AND label LIKE ?
ORDER BY label ASC
LIMIT ? OFFSET ?
`);
const rows = stmt.all(typeName, `%${searchTerm}%`, limit, offset) as { data: string }[];
return rows.map(row => {
try {
return JSON.parse(row.data) as T;
} catch {
return null;
}
}).filter((obj): obj is T => obj !== null);
}
/**
* Search across all object types
*/
searchAllTypes(searchTerm: string, options?: QueryOptions): CMDBObject[] {
const limit = options?.limit || 100;
const offset = options?.offset || 0;
const stmt = this.db.prepare(`
SELECT data FROM cached_objects
WHERE label LIKE ? OR object_key LIKE ?
ORDER BY object_type, label ASC
LIMIT ? OFFSET ?
`);
const pattern = `%${searchTerm}%`;
const rows = stmt.all(pattern, pattern, limit, offset) as { data: string }[];
return rows.map(row => {
try {
return JSON.parse(row.data) as CMDBObject;
} catch {
return null;
}
}).filter((obj): obj is CMDBObject => obj !== null);
}
/**
* Upsert a single object
*/
upsertObject<T extends CMDBObject>(typeName: CMDBObjectTypeName, object: T): void {
const stmt = this.db.prepare(`
INSERT INTO cached_objects (id, object_key, object_type, label, data, jira_updated_at, jira_created_at, cached_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
object_key = excluded.object_key,
label = excluded.label,
data = excluded.data,
jira_updated_at = excluded.jira_updated_at,
cached_at = excluded.cached_at
`);
stmt.run(
object.id,
object.objectKey,
typeName,
object.label,
JSON.stringify(object),
object._jiraUpdatedAt || null,
object._jiraCreatedAt || null,
new Date().toISOString()
);
}
/**
* Batch upsert objects (much faster for bulk operations)
*/
batchUpsertObjects<T extends CMDBObject>(typeName: CMDBObjectTypeName, objects: T[]): void {
if (objects.length === 0) return;
const stmt = this.db.prepare(`
INSERT INTO cached_objects (id, object_key, object_type, label, data, jira_updated_at, jira_created_at, cached_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
object_key = excluded.object_key,
label = excluded.label,
data = excluded.data,
jira_updated_at = excluded.jira_updated_at,
cached_at = excluded.cached_at
`);
const now = new Date().toISOString();
const batchInsert = this.db.transaction((objs: T[]) => {
for (const obj of objs) {
stmt.run(
obj.id,
obj.objectKey,
typeName,
obj.label,
JSON.stringify(obj),
obj._jiraUpdatedAt || null,
obj._jiraCreatedAt || null,
now
);
}
});
batchInsert(objects);
logger.debug(`CacheStore: Batch upserted ${objects.length} ${typeName} objects`);
}
/**
* Delete an object by ID
*/
deleteObject(typeName: CMDBObjectTypeName, id: string): boolean {
const stmt = this.db.prepare(`
DELETE FROM cached_objects
WHERE id = ? AND object_type = ?
`);
const result = stmt.run(id, typeName);
// Also delete related relations
this.deleteRelationsForObject(id);
return result.changes > 0;
}
/**
* Clear all objects of a specific type
*/
clearObjectType(typeName: CMDBObjectTypeName): number {
// First get all IDs to delete relations
const idsStmt = this.db.prepare(`
SELECT id FROM cached_objects WHERE object_type = ?
`);
const ids = idsStmt.all(typeName) as { id: string }[];
// Delete relations
for (const { id } of ids) {
this.deleteRelationsForObject(id);
}
// Delete objects
const stmt = this.db.prepare(`
DELETE FROM cached_objects WHERE object_type = ?
`);
const result = stmt.run(typeName);
logger.info(`CacheStore: Cleared ${result.changes} ${typeName} objects`);
return result.changes;
}
/**
* Clear entire cache
*/
clearAll(): void {
this.db.exec('DELETE FROM cached_objects');
this.db.exec('DELETE FROM object_relations');
logger.info('CacheStore: Cleared all cached data');
}
// ==========================================================================
// Relation Operations
// ==========================================================================
/**
* Store a relation between two objects
*/
upsertRelation(
sourceId: string,
targetId: string,
attributeName: string,
sourceType: string,
targetType: string
): void {
const stmt = this.db.prepare(`
INSERT INTO object_relations (source_id, target_id, attribute_name, source_type, target_type)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(source_id, target_id, attribute_name) DO UPDATE SET
source_type = excluded.source_type,
target_type = excluded.target_type
`);
stmt.run(sourceId, targetId, attributeName, sourceType, targetType);
}
/**
* Batch upsert relations
*/
batchUpsertRelations(relations: Array<{
sourceId: string;
targetId: string;
attributeName: string;
sourceType: string;
targetType: string;
}>): void {
if (relations.length === 0) return;
const stmt = this.db.prepare(`
INSERT INTO object_relations (source_id, target_id, attribute_name, source_type, target_type)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(source_id, target_id, attribute_name) DO UPDATE SET
source_type = excluded.source_type,
target_type = excluded.target_type
`);
const batchInsert = this.db.transaction((rels: typeof relations) => {
for (const rel of rels) {
stmt.run(rel.sourceId, rel.targetId, rel.attributeName, rel.sourceType, rel.targetType);
}
});
batchInsert(relations);
logger.debug(`CacheStore: Batch upserted ${relations.length} relations`);
}
/**
* Get related objects (outbound references from an object)
*/
getRelatedObjects<T extends CMDBObject>(
sourceId: string,
targetTypeName: CMDBObjectTypeName,
attributeName?: string
): T[] {
let query = `
SELECT co.data FROM cached_objects co
JOIN object_relations rel ON co.id = rel.target_id
WHERE rel.source_id = ? AND co.object_type = ?
`;
const params: (string | undefined)[] = [sourceId, targetTypeName];
if (attributeName) {
query += ' AND rel.attribute_name = ?';
params.push(attributeName);
}
const stmt = this.db.prepare(query);
const rows = stmt.all(...params) as { data: string }[];
return rows.map(row => {
try {
return JSON.parse(row.data) as T;
} catch {
return null;
}
}).filter((obj): obj is T => obj !== null);
}
/**
* Get objects that reference the given object (inbound references)
*/
getReferencingObjects<T extends CMDBObject>(
targetId: string,
sourceTypeName: CMDBObjectTypeName,
attributeName?: string
): T[] {
let query = `
SELECT co.data FROM cached_objects co
JOIN object_relations rel ON co.id = rel.source_id
WHERE rel.target_id = ? AND co.object_type = ?
`;
const params: (string | undefined)[] = [targetId, sourceTypeName];
if (attributeName) {
query += ' AND rel.attribute_name = ?';
params.push(attributeName);
}
const stmt = this.db.prepare(query);
const rows = stmt.all(...params) as { data: string }[];
return rows.map(row => {
try {
return JSON.parse(row.data) as T;
} catch {
return null;
}
}).filter((obj): obj is T => obj !== null);
}
/**
* Delete all relations for an object
*/
deleteRelationsForObject(objectId: string): void {
const stmt = this.db.prepare(`
DELETE FROM object_relations
WHERE source_id = ? OR target_id = ?
`);
stmt.run(objectId, objectId);
}
/**
* Extract and store relations from an object based on its type schema
*/
extractAndStoreRelations<T extends CMDBObject>(typeName: CMDBObjectTypeName, object: T): void {
const refAttributes = getReferenceAttributes(typeName);
const relations: Array<{
sourceId: string;
targetId: string;
attributeName: string;
sourceType: string;
targetType: string;
}> = [];
for (const attrDef of refAttributes) {
const value = (object as unknown as Record<string, unknown>)[attrDef.fieldName];
if (!value) continue;
const targetType = attrDef.referenceTypeName || 'Unknown';
if (attrDef.isMultiple && Array.isArray(value)) {
for (const ref of value as ObjectReference[]) {
if (ref?.objectId) {
relations.push({
sourceId: object.id,
targetId: ref.objectId,
attributeName: attrDef.name,
sourceType: typeName,
targetType,
});
}
}
} else if (!attrDef.isMultiple) {
const ref = value as ObjectReference;
if (ref?.objectId) {
relations.push({
sourceId: object.id,
targetId: ref.objectId,
attributeName: attrDef.name,
sourceType: typeName,
targetType,
});
}
}
}
if (relations.length > 0) {
this.batchUpsertRelations(relations);
}
}
// ==========================================================================
// Sync Metadata Operations
// ==========================================================================
/**
* Get sync metadata value
*/
getSyncMetadata(key: string): string | null {
const stmt = this.db.prepare(`
SELECT value FROM sync_metadata WHERE key = ?
`);
const row = stmt.get(key) as { value: string } | undefined;
return row?.value || null;
}
/**
* Set sync metadata value
*/
setSyncMetadata(key: string, value: string): void {
const stmt = this.db.prepare(`
INSERT INTO sync_metadata (key, value, updated_at)
VALUES (?, ?, ?)
ON CONFLICT(key) DO UPDATE SET
value = excluded.value,
updated_at = excluded.updated_at
`);
stmt.run(key, value, new Date().toISOString());
}
/**
* Delete sync metadata
*/
deleteSyncMetadata(key: string): void {
const stmt = this.db.prepare(`
DELETE FROM sync_metadata WHERE key = ?
`);
stmt.run(key);
}
// ==========================================================================
// Statistics
// ==========================================================================
/**
* Get cache statistics
*/
getStats(): CacheStats {
// Count by type
const typeCountStmt = this.db.prepare(`
SELECT object_type, COUNT(*) as count
FROM cached_objects
GROUP BY object_type
`);
const typeCounts = typeCountStmt.all() as { object_type: string; count: number }[];
const objectsByType: Record<string, number> = {};
let totalObjects = 0;
for (const { object_type, count } of typeCounts) {
objectsByType[object_type] = count;
totalObjects += count;
}
// Count relations
const relCountStmt = this.db.prepare(`
SELECT COUNT(*) as count FROM object_relations
`);
const relCount = (relCountStmt.get() as { count: number }).count;
// Get sync metadata
const lastFullSync = this.getSyncMetadata('lastFullSync');
const lastIncrementalSync = this.getSyncMetadata('lastIncrementalSync');
// Check if cache is warm (has Application Components)
const isWarm = (objectsByType['ApplicationComponent'] || 0) > 0;
// Get database file size
let dbSizeBytes = 0;
try {
const stats = fs.statSync(CACHE_DB_PATH);
dbSizeBytes = stats.size;
} catch {
// Ignore
}
return {
totalObjects,
objectsByType,
totalRelations: relCount,
lastFullSync,
lastIncrementalSync,
isWarm,
dbSizeBytes,
};
}
/**
* Check if cache is warm (has data)
*/
isWarm(): boolean {
const count = this.countObjects('ApplicationComponent');
return count > 0;
}
/**
* Close database connection
*/
close(): void {
this.db.close();
}
}
// Export singleton instance
export const cacheStore = new CacheStore();

View File

@@ -372,7 +372,7 @@ async function formatApplicationFunctionsForPrompt(
return sections.join('\n\n');
}
// Format reference objects for prompt (Application Type, Dynamics Factor, etc.)
// Format reference objects for prompt (Application Type, etc.)
function formatReferenceObjectsForPrompt(
objects: ReferenceValue[],
useSummary: boolean = false
@@ -391,10 +391,30 @@ function formatReferenceObjectsForPrompt(
.join('\n');
}
// Format factors (Dynamics/Complexity) with description for AI prompt
function formatFactorsForPrompt(objects: ReferenceValue[]): string {
if (objects.length === 0) {
return 'Geen factoren beschikbaar.';
}
return objects
.map((obj) => {
const parts: string[] = [` - ${obj.key}: ${obj.name}`];
if (obj.factor !== undefined) {
parts[0] += ` (factor: ${obj.factor})`;
}
if (obj.description) {
parts.push(` ${obj.description}`);
}
return parts.join('\n');
})
.join('\n');
}
// Format reference objects with emphasis on exact name (for fields where AI must use exact name)
function formatReferenceObjectsWithExactNames(
objects: ReferenceValue[],
useSummary: boolean = false
useDescription: boolean = false
): string {
if (objects.length === 0) {
return 'Geen objecten beschikbaar.';
@@ -402,9 +422,7 @@ function formatReferenceObjectsWithExactNames(
return objects
.map((obj) => {
const displayText = useSummary && obj.summary
? obj.summary
: obj.description || '';
const displayText = useDescription && obj.description ? obj.description : '';
// Emphasize the exact name that should be used
return ` - **"${obj.name}"**${displayText ? ` - ${displayText}` : ''}`;
})
@@ -892,8 +910,8 @@ class AIService {
applicationFunctionCategories
);
const applicationTypesFormatted = formatReferenceObjectsForPrompt(applicationTypes, false);
const dynamicsFactorsFormatted = formatReferenceObjectsForPrompt(dynamicsFactors, true);
const complexityFactorsFormatted = formatReferenceObjectsForPrompt(complexityFactors, true);
const dynamicsFactorsFormatted = formatFactorsForPrompt(dynamicsFactors);
const complexityFactorsFormatted = formatFactorsForPrompt(complexityFactors);
const applicationManagementHostingFormatted = formatReferenceObjectsWithExactNames(applicationManagementHostingOptions, true);
const businessImpactAnalysesFormatted = formatBusinessImpactAnalysesForPrompt(businessImpactAnalyses);
const businessImpactIndicatorsFormatted = formatBusinessImpactIndicatorsForPrompt(businessImpactAnalyses);
@@ -1133,8 +1151,8 @@ class AIService {
applicationFunctionCategories
);
const applicationTypesFormatted = formatReferenceObjectsForPrompt(applicationTypes, false);
const dynamicsFactorsFormatted = formatReferenceObjectsForPrompt(dynamicsFactors, true);
const complexityFactorsFormatted = formatReferenceObjectsForPrompt(complexityFactors, true);
const dynamicsFactorsFormatted = formatFactorsForPrompt(dynamicsFactors);
const complexityFactorsFormatted = formatFactorsForPrompt(complexityFactors);
const applicationManagementHostingFormatted = formatReferenceObjectsWithExactNames(applicationManagementHostingOptions, true);
const businessImpactAnalysesFormatted = formatBusinessImpactAnalysesForPrompt(businessImpactAnalyses);
const businessImpactIndicatorsFormatted = formatBusinessImpactIndicatorsForPrompt(businessImpactAnalyses);

View File

@@ -0,0 +1,445 @@
/**
* CMDBService - Universal schema-driven CMDB service
*
* Provides a unified interface for all CMDB operations:
* - Reads from cache for fast access
* - Write-through to Jira with conflict detection
* - Schema-driven parsing and updates
*/
import { logger } from './logger.js';
import { cacheStore, type CacheStats } from './cacheStore.js';
import { jiraAssetsClient, type JiraUpdatePayload, JiraObjectNotFoundError } from './jiraAssetsClient.js';
import { conflictResolver, type ConflictCheckResult } from './conflictResolver.js';
import { OBJECT_TYPES, getAttributeDefinition } from '../generated/jira-schema.js';
import type { CMDBObject, CMDBObjectTypeName, ObjectReference } from '../generated/jira-types.js';
// =============================================================================
// Types
// =============================================================================
export interface GetObjectOptions {
/** Force refresh from Jira (bypasses cache) */
forceRefresh?: boolean;
}
export interface UpdateResult {
success: boolean;
data?: CMDBObject;
conflict?: ConflictCheckResult;
error?: string;
}
export interface SearchOptions {
limit?: number;
offset?: number;
searchTerm?: string;
}
// =============================================================================
// Service Implementation
// =============================================================================
class CMDBService {
// ==========================================================================
// Read Operations
// ==========================================================================
/**
* Get a single object by ID
* By default reads from cache; use forceRefresh to fetch from Jira
*/
async getObject<T extends CMDBObject>(
typeName: CMDBObjectTypeName,
id: string,
options?: GetObjectOptions
): Promise<T | null> {
// Force refresh: always fetch from Jira
if (options?.forceRefresh) {
return this.fetchAndCacheObject<T>(typeName, id);
}
// Try cache first
const cached = cacheStore.getObject<T>(typeName, id);
if (cached) {
return cached;
}
// Cache miss: fetch from Jira
return this.fetchAndCacheObject<T>(typeName, id);
}
/**
* Get a single object by object key (e.g., "ICMT-123")
*/
async getObjectByKey<T extends CMDBObject>(
typeName: CMDBObjectTypeName,
objectKey: string,
options?: GetObjectOptions
): Promise<T | null> {
// Force refresh: search Jira by key
if (options?.forceRefresh) {
const typeDef = OBJECT_TYPES[typeName];
if (!typeDef) return null;
const iql = `objectType = "${typeDef.name}" AND Key = "${objectKey}"`;
const result = await jiraAssetsClient.searchObjects(iql, 1, 1);
if (result.objects.length === 0) return null;
const parsed = jiraAssetsClient.parseObject<T>(result.objects[0]);
if (parsed) {
cacheStore.upsertObject(typeName, parsed);
cacheStore.extractAndStoreRelations(typeName, parsed);
}
return parsed;
}
// Try cache first
const cached = cacheStore.getObjectByKey<T>(typeName, objectKey);
if (cached) {
return cached;
}
// Cache miss: search Jira
return this.getObjectByKey(typeName, objectKey, { forceRefresh: true });
}
/**
* Fetch a single object from Jira and update cache
* If the object was deleted from Jira (404), it will be removed from the local cache
*/
private async fetchAndCacheObject<T extends CMDBObject>(
typeName: CMDBObjectTypeName,
id: string
): Promise<T | null> {
try {
const jiraObj = await jiraAssetsClient.getObject(id);
if (!jiraObj) return null;
const parsed = jiraAssetsClient.parseObject<T>(jiraObj);
if (parsed) {
cacheStore.upsertObject(typeName, parsed);
cacheStore.extractAndStoreRelations(typeName, parsed);
}
return parsed;
} catch (error) {
// If object was deleted from Jira, remove it from our cache
if (error instanceof JiraObjectNotFoundError) {
const deleted = cacheStore.deleteObject(typeName, id);
if (deleted) {
logger.info(`CMDBService: Removed deleted object ${typeName}/${id} from cache`);
}
return null;
}
// Re-throw other errors
throw error;
}
}
/**
* Get all objects of a type from cache
*/
async getObjects<T extends CMDBObject>(
typeName: CMDBObjectTypeName,
options?: SearchOptions
): Promise<T[]> {
if (options?.searchTerm) {
return cacheStore.searchByLabel<T>(typeName, options.searchTerm, {
limit: options.limit,
offset: options.offset,
});
}
return cacheStore.getObjects<T>(typeName, {
limit: options?.limit,
offset: options?.offset,
});
}
/**
* Count objects of a type in cache
*/
countObjects(typeName: CMDBObjectTypeName): number {
return cacheStore.countObjects(typeName);
}
/**
* Search across all object types
*/
async searchAllTypes(searchTerm: string, options?: { limit?: number }): Promise<CMDBObject[]> {
return cacheStore.searchAllTypes(searchTerm, { limit: options?.limit });
}
/**
* Get related objects (outbound references)
*/
async getRelatedObjects<T extends CMDBObject>(
sourceId: string,
attributeName: string,
targetTypeName: CMDBObjectTypeName
): Promise<T[]> {
return cacheStore.getRelatedObjects<T>(sourceId, targetTypeName, attributeName);
}
/**
* Get objects that reference the given object (inbound references)
*/
async getReferencingObjects<T extends CMDBObject>(
targetId: string,
sourceTypeName: CMDBObjectTypeName,
attributeName?: string
): Promise<T[]> {
return cacheStore.getReferencingObjects<T>(targetId, sourceTypeName, attributeName);
}
// ==========================================================================
// Write Operations
// ==========================================================================
/**
* Update an object with conflict detection
*
* @param typeName - The object type
* @param id - The object ID
* @param updates - Field updates (only changed fields)
* @param originalUpdatedAt - The _jiraUpdatedAt from when the object was loaded for editing
*/
async updateObject(
typeName: CMDBObjectTypeName,
id: string,
updates: Record<string, unknown>,
originalUpdatedAt: string
): Promise<UpdateResult> {
try {
// 1. Check for conflicts
const conflictResult = await conflictResolver.checkConflict(
typeName,
id,
originalUpdatedAt,
updates
);
if (conflictResult.hasConflict && conflictResult.conflicts && conflictResult.conflicts.length > 0) {
return {
success: false,
conflict: conflictResult,
};
}
// 2. Build Jira update payload
const payload = this.buildUpdatePayload(typeName, updates);
if (payload.attributes.length === 0) {
logger.warn(`CMDBService: No attributes to update for ${typeName} ${id}`);
return { success: true };
}
// 3. Send update to Jira
const success = await jiraAssetsClient.updateObject(id, payload);
if (!success) {
return {
success: false,
error: 'Failed to update object in Jira',
};
}
// 4. Fetch fresh data and update cache
const freshData = await this.fetchAndCacheObject(typeName, id);
logger.info(`CMDBService: Updated ${typeName} ${id}`);
return {
success: true,
data: freshData || undefined,
};
} catch (error) {
logger.error(`CMDBService: Update failed for ${typeName} ${id}`, error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
}
/**
* Force update without conflict check (use with caution)
*/
async forceUpdateObject(
typeName: CMDBObjectTypeName,
id: string,
updates: Record<string, unknown>
): Promise<UpdateResult> {
try {
const payload = this.buildUpdatePayload(typeName, updates);
if (payload.attributes.length === 0) {
return { success: true };
}
const success = await jiraAssetsClient.updateObject(id, payload);
if (!success) {
return {
success: false,
error: 'Failed to update object in Jira',
};
}
const freshData = await this.fetchAndCacheObject(typeName, id);
return {
success: true,
data: freshData || undefined,
};
} catch (error) {
logger.error(`CMDBService: Force update failed for ${typeName} ${id}`, error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
}
/**
* Build Jira update payload from field updates
*/
private buildUpdatePayload(
typeName: CMDBObjectTypeName,
updates: Record<string, unknown>
): JiraUpdatePayload {
const attributes: JiraUpdatePayload['attributes'] = [];
logger.debug(`CMDBService.buildUpdatePayload: Building payload for ${typeName}`, {
updateKeys: Object.keys(updates),
updates: JSON.stringify(updates, null, 2)
});
for (const [fieldName, value] of Object.entries(updates)) {
const attrDef = getAttributeDefinition(typeName, fieldName);
if (!attrDef) {
logger.warn(`CMDBService: Unknown attribute ${fieldName} for ${typeName}`);
continue;
}
if (!attrDef.isEditable) {
logger.warn(`CMDBService: Attribute ${fieldName} is not editable`);
continue;
}
const attrValues = this.buildAttributeValues(value, attrDef);
logger.debug(`CMDBService.buildUpdatePayload: Attribute ${fieldName} (jiraId: ${attrDef.jiraId}, type: ${attrDef.type}, isMultiple: ${attrDef.isMultiple})`, {
inputValue: JSON.stringify(value),
outputValues: JSON.stringify(attrValues)
});
attributes.push({
objectTypeAttributeId: attrDef.jiraId,
objectAttributeValues: attrValues,
});
}
logger.debug(`CMDBService.buildUpdatePayload: Final payload`, {
attributeCount: attributes.length,
payload: JSON.stringify({ attributes }, null, 2)
});
return { attributes };
}
/**
* Build attribute values for Jira API
*/
private buildAttributeValues(
value: unknown,
attrDef: { type: string; isMultiple: boolean }
): Array<{ value?: string }> {
// Null/undefined = clear the field
if (value === null || value === undefined) {
return [];
}
// Reference type
if (attrDef.type === 'reference') {
if (attrDef.isMultiple && Array.isArray(value)) {
return (value as ObjectReference[]).map(ref => ({
value: ref.objectKey,
}));
} else if (!attrDef.isMultiple) {
const ref = value as ObjectReference;
return [{ value: ref.objectKey }];
}
return [];
}
// Boolean
if (attrDef.type === 'boolean') {
return [{ value: value ? 'true' : 'false' }];
}
// Number types
if (attrDef.type === 'integer' || attrDef.type === 'float') {
return [{ value: String(value) }];
}
// String types
return [{ value: String(value) }];
}
// ==========================================================================
// Cache Management
// ==========================================================================
/**
* Get cache statistics
*/
getCacheStats(): CacheStats {
return cacheStore.getStats();
}
/**
* Check if cache has data
*/
isCacheWarm(): boolean {
return cacheStore.isWarm();
}
/**
* Clear cache for a specific type
*/
clearCacheForType(typeName: CMDBObjectTypeName): void {
cacheStore.clearObjectType(typeName);
}
/**
* Clear entire cache
*/
clearCache(): void {
cacheStore.clearAll();
}
// ==========================================================================
// User Token Management (for OAuth)
// ==========================================================================
/**
* Set user token for current request
*/
setUserToken(token: string | null): void {
jiraAssetsClient.setRequestToken(token);
}
/**
* Clear user token
*/
clearUserToken(): void {
jiraAssetsClient.clearRequestToken();
}
}
// Export singleton instance
export const cmdbService = new CMDBService();

View File

@@ -0,0 +1,254 @@
/**
* ConflictResolver - Detects and reports conflicts when updating CMDB objects
*
* Implements optimistic locking by comparing timestamps and checking
* for field-level conflicts.
*/
import { logger } from './logger.js';
import { jiraAssetsClient, JiraObjectNotFoundError } from './jiraAssetsClient.js';
import { OBJECT_TYPES, getAttributeDefinition } from '../generated/jira-schema.js';
import type { CMDBObjectTypeName } from '../generated/jira-types.js';
// =============================================================================
// Types
// =============================================================================
export interface FieldConflict {
field: string;
fieldName: string;
proposedValue: unknown;
jiraValue: unknown;
}
export interface ConflictCheckResult {
hasConflict: boolean;
conflicts?: FieldConflict[];
jiraUpdatedAt?: string;
warning?: string;
canMerge?: boolean;
}
// =============================================================================
// Conflict Resolver Implementation
// =============================================================================
class ConflictResolver {
/**
* Check for conflicts before updating an object
*
* @param typeName - The object type
* @param objectId - The object ID
* @param originalUpdatedAt - The _jiraUpdatedAt from when the user started editing
* @param proposedChanges - The changes the user wants to make
*/
async checkConflict(
typeName: CMDBObjectTypeName,
objectId: string,
originalUpdatedAt: string,
proposedChanges: Record<string, unknown>
): Promise<ConflictCheckResult> {
try {
// 1. Fetch current state from Jira
let jiraObj;
try {
jiraObj = await jiraAssetsClient.getObject(objectId);
} catch (error) {
if (error instanceof JiraObjectNotFoundError) {
return {
hasConflict: true,
warning: 'Object has been deleted from Jira',
};
}
throw error;
}
if (!jiraObj) {
return {
hasConflict: true,
warning: 'Object not found in Jira',
};
}
const currentUpdatedAt = jiraObj.updated || '';
// 2. Compare timestamps
if (currentUpdatedAt === originalUpdatedAt) {
// No changes since user started editing - safe to update
return { hasConflict: false };
}
// 3. Timestamp differs - check field-level conflicts
logger.info(`ConflictResolver: Timestamp mismatch for ${objectId}. Original: ${originalUpdatedAt}, Current: ${currentUpdatedAt}`);
const typeDef = OBJECT_TYPES[typeName];
if (!typeDef) {
return {
hasConflict: true,
warning: `Unknown object type: ${typeName}`,
};
}
const conflictingFields: FieldConflict[] = [];
for (const [fieldName, proposedValue] of Object.entries(proposedChanges)) {
const attrDef = getAttributeDefinition(typeName, fieldName);
if (!attrDef) continue;
// Find the attribute in Jira response
const jiraAttr = jiraObj.attributes.find(
a => a.objectTypeAttributeId === attrDef.jiraId ||
a.objectTypeAttribute?.name === attrDef.name
);
// Extract current value from Jira
const jiraValue = this.extractAttributeValue(jiraAttr, attrDef);
// Compare values
if (!this.valuesEqual(proposedValue, jiraValue, attrDef.type)) {
conflictingFields.push({
field: attrDef.name,
fieldName,
proposedValue,
jiraValue,
});
}
}
if (conflictingFields.length === 0) {
// Object was updated but not on the same fields - safe to merge
return {
hasConflict: false,
warning: 'Object was updated but no field conflicts detected',
jiraUpdatedAt: currentUpdatedAt,
canMerge: true,
};
}
// Real conflicts exist
return {
hasConflict: true,
conflicts: conflictingFields,
jiraUpdatedAt: currentUpdatedAt,
canMerge: false,
};
} catch (error) {
logger.error(`ConflictResolver: Error checking conflict for ${objectId}`, error);
return {
hasConflict: true,
warning: 'Error checking for conflicts',
};
}
}
/**
* Extract a typed value from a Jira attribute
*/
private extractAttributeValue(
jiraAttr: { objectAttributeValues: Array<{
value?: string;
displayValue?: string;
referencedObject?: { id: number; objectKey: string; label: string };
}> } | undefined,
attrDef: { type: string; isMultiple: boolean }
): unknown {
if (!jiraAttr?.objectAttributeValues?.length) {
return attrDef.isMultiple ? [] : null;
}
const values = jiraAttr.objectAttributeValues;
switch (attrDef.type) {
case 'reference': {
const refs = values
.filter(v => v.referencedObject)
.map(v => ({
objectId: v.referencedObject!.id.toString(),
objectKey: v.referencedObject!.objectKey,
label: v.referencedObject!.label,
}));
return attrDef.isMultiple ? refs : refs[0] || null;
}
case 'boolean': {
const val = values[0]?.value;
return val === 'true' || val === 'Ja';
}
case 'integer': {
const val = values[0]?.value;
return val ? parseInt(val, 10) : null;
}
case 'float': {
const val = values[0]?.value;
return val ? parseFloat(val) : null;
}
default:
return values[0]?.displayValue ?? values[0]?.value ?? null;
}
}
/**
* Compare two values for equality
*/
private valuesEqual(
proposed: unknown,
jira: unknown,
type: string
): boolean {
// Both null/undefined
if (proposed == null && jira == null) {
return true;
}
// One is null
if (proposed == null || jira == null) {
return false;
}
// Reference type
if (type === 'reference') {
return this.referencesEqual(proposed, jira);
}
// Arrays
if (Array.isArray(proposed) && Array.isArray(jira)) {
if (proposed.length !== jira.length) return false;
// For reference arrays, compare by objectId
if (type === 'reference') {
const proposedIds = new Set((proposed as Array<{ objectId?: string }>).map(r => r.objectId));
const jiraIds = new Set((jira as Array<{ objectId?: string }>).map(r => r.objectId));
if (proposedIds.size !== jiraIds.size) return false;
for (const id of proposedIds) {
if (!jiraIds.has(id)) return false;
}
return true;
}
return JSON.stringify(proposed) === JSON.stringify(jira);
}
// Primitives
return proposed === jira;
}
/**
* Compare reference values
*/
private referencesEqual(proposed: unknown, jira: unknown): boolean {
// Compare by objectId or objectKey
const propRef = proposed as { objectId?: string; objectKey?: string } | null;
const jiraRef = jira as { objectId?: string; objectKey?: string } | null;
if (!propRef && !jiraRef) return true;
if (!propRef || !jiraRef) return false;
return propRef.objectId === jiraRef.objectId ||
propRef.objectKey === jiraRef.objectKey;
}
}
// Export singleton instance
export const conflictResolver = new ConflictResolver();

File diff suppressed because it is too large Load Diff

View File

@@ -8,10 +8,15 @@ import {
HostingRule,
} from '../config/effortCalculation.js';
import { readFileSync, existsSync } from 'fs';
import { join } from 'path';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import { logger } from './logger.js';
import type { ApplicationDetails, EffortCalculationBreakdown } from '../types/index.js';
// Get __dirname equivalent for ES modules
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Path to the configuration file (v25)
const CONFIG_FILE_PATH_V25 = join(__dirname, '../../data/effort-calculation-config-v25.json');

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,425 @@
/**
* JiraAssetsClient - Low-level Jira Assets API client for CMDB caching
*
* This client handles direct API calls to Jira Insight/Assets and provides
* methods for fetching, parsing, and updating CMDB objects.
*/
import { config } from '../config/env.js';
import { logger } from './logger.js';
import { OBJECT_TYPES } from '../generated/jira-schema.js';
import type { CMDBObject, CMDBObjectTypeName, ObjectReference } from '../generated/jira-types.js';
import type { JiraAssetsObject, JiraAssetsAttribute, JiraAssetsSearchResponse } from '../types/index.js';
// =============================================================================
// Types
// =============================================================================
/** Error thrown when an object is not found in Jira (404) */
export class JiraObjectNotFoundError extends Error {
constructor(public objectId: string) {
super(`Object ${objectId} not found in Jira`);
this.name = 'JiraObjectNotFoundError';
}
}
export interface JiraUpdatePayload {
objectTypeId?: number; // Optional for updates (PUT) - only needed for creates (POST)
attributes: Array<{
objectTypeAttributeId: number;
objectAttributeValues: Array<{ value?: string }>; // value can be undefined when clearing
}>;
}
// Map from Jira object type ID to our type name
const TYPE_ID_TO_NAME: Record<number, CMDBObjectTypeName> = {};
const JIRA_NAME_TO_TYPE: Record<string, CMDBObjectTypeName> = {};
// Build lookup maps from schema
for (const [typeName, typeDef] of Object.entries(OBJECT_TYPES)) {
TYPE_ID_TO_NAME[typeDef.jiraTypeId] = typeName as CMDBObjectTypeName;
JIRA_NAME_TO_TYPE[typeDef.name] = typeName as CMDBObjectTypeName;
}
// =============================================================================
// JiraAssetsClient Implementation
// =============================================================================
class JiraAssetsClient {
private baseUrl: string;
private defaultHeaders: Record<string, string>;
private isDataCenter: boolean | null = null;
private requestToken: string | null = null;
constructor() {
this.baseUrl = `${config.jiraHost}/rest/insight/1.0`;
this.defaultHeaders = {
'Content-Type': 'application/json',
'Accept': 'application/json',
};
// Add PAT authentication if configured
if (config.jiraAuthMethod === 'pat' && config.jiraPat) {
this.defaultHeaders['Authorization'] = `Bearer ${config.jiraPat}`;
}
}
// ==========================================================================
// Request Token Management (for user-context requests)
// ==========================================================================
setRequestToken(token: string): void {
this.requestToken = token;
}
clearRequestToken(): void {
this.requestToken = null;
}
// ==========================================================================
// API Detection
// ==========================================================================
private async detectApiType(): Promise<void> {
if (this.isDataCenter !== null) return;
// Detect based on host URL pattern:
// - Jira Cloud uses *.atlassian.net domains
// - Everything else (custom domains) is Data Center / on-premise
if (config.jiraHost.includes('atlassian.net')) {
this.isDataCenter = false;
logger.info('JiraAssetsClient: Detected Jira Cloud (Assets API) based on host URL');
} else {
this.isDataCenter = true;
logger.info('JiraAssetsClient: Detected Jira Data Center (Insight API) based on host URL');
}
}
private getHeaders(): Record<string, string> {
const headers = { ...this.defaultHeaders };
// Use request-scoped token if available (for user context)
if (this.requestToken) {
headers['Authorization'] = `Bearer ${this.requestToken}`;
}
return headers;
}
// ==========================================================================
// Core API Methods
// ==========================================================================
private async request<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
const url = `${this.baseUrl}${endpoint}`;
logger.debug(`JiraAssetsClient: ${options.method || 'GET'} ${url}`);
const response = await fetch(url, {
...options,
headers: {
...this.getHeaders(),
...options.headers,
},
});
if (!response.ok) {
const text = await response.text();
throw new Error(`Jira API error ${response.status}: ${text}`);
}
return response.json() as Promise<T>;
}
// ==========================================================================
// Public API Methods
// ==========================================================================
async testConnection(): Promise<boolean> {
try {
await this.detectApiType();
const response = await fetch(`${this.baseUrl}/objectschema/${config.jiraSchemaId}`, {
headers: this.getHeaders(),
});
return response.ok;
} catch (error) {
logger.error('JiraAssetsClient: Connection test failed', error);
return false;
}
}
async getObject(objectId: string): Promise<JiraAssetsObject | null> {
try {
return await this.request<JiraAssetsObject>(`/object/${objectId}`);
} catch (error) {
// Check if this is a 404 (object not found / deleted)
if (error instanceof Error && error.message.includes('404')) {
logger.info(`JiraAssetsClient: Object ${objectId} not found in Jira (likely deleted)`);
throw new JiraObjectNotFoundError(objectId);
}
logger.error(`JiraAssetsClient: Failed to get object ${objectId}`, error);
return null;
}
}
async searchObjects(
iql: string,
page: number = 1,
pageSize: number = 50
): Promise<{ objects: JiraAssetsObject[]; totalCount: number; hasMore: boolean }> {
await this.detectApiType();
let response: JiraAssetsSearchResponse;
if (this.isDataCenter) {
// Try modern AQL endpoint first
try {
const params = new URLSearchParams({
qlQuery: iql,
page: page.toString(),
resultPerPage: pageSize.toString(),
includeAttributes: 'true',
includeAttributesDeep: '1',
objectSchemaId: config.jiraSchemaId,
});
response = await this.request<JiraAssetsSearchResponse>(`/aql/objects?${params.toString()}`);
} catch (error) {
// Fallback to deprecated IQL endpoint
logger.warn(`JiraAssetsClient: AQL endpoint failed, falling back to IQL: ${error}`);
const params = new URLSearchParams({
iql,
page: page.toString(),
resultPerPage: pageSize.toString(),
includeAttributes: 'true',
includeAttributesDeep: '1',
objectSchemaId: config.jiraSchemaId,
});
response = await this.request<JiraAssetsSearchResponse>(`/iql/objects?${params.toString()}`);
}
} else {
// Jira Cloud uses POST for AQL
response = await this.request<JiraAssetsSearchResponse>('/aql/objects', {
method: 'POST',
body: JSON.stringify({
qlQuery: iql,
page,
resultPerPage: pageSize,
includeAttributes: true,
}),
});
}
const totalCount = response.totalFilterCount || response.totalCount || 0;
const hasMore = response.objectEntries.length === pageSize && page * pageSize < totalCount;
return {
objects: response.objectEntries || [],
totalCount,
hasMore,
};
}
async getAllObjectsOfType(
typeName: CMDBObjectTypeName,
batchSize: number = 40
): Promise<JiraAssetsObject[]> {
const typeDef = OBJECT_TYPES[typeName];
if (!typeDef) {
logger.warn(`JiraAssetsClient: Unknown type ${typeName}`);
return [];
}
const allObjects: JiraAssetsObject[] = [];
let page = 1;
let hasMore = true;
while (hasMore) {
const iql = `objectType = "${typeDef.name}"`;
const result = await this.searchObjects(iql, page, batchSize);
allObjects.push(...result.objects);
hasMore = result.hasMore;
page++;
}
logger.info(`JiraAssetsClient: Fetched ${allObjects.length} ${typeName} objects`);
return allObjects;
}
async getUpdatedObjectsSince(
since: Date,
_batchSize: number = 40
): Promise<JiraAssetsObject[]> {
await this.detectApiType();
// Jira Data Center's IQL doesn't support filtering by 'updated' attribute
if (this.isDataCenter) {
logger.debug('JiraAssetsClient: Incremental sync via IQL not supported on Data Center, skipping');
return [];
}
// For Jira Cloud, we could use updated >= "date" in IQL
const iql = `updated >= "${since.toISOString()}"`;
const result = await this.searchObjects(iql, 1, 1000);
return result.objects;
}
async updateObject(objectId: string, payload: JiraUpdatePayload): Promise<boolean> {
try {
logger.info(`JiraAssetsClient.updateObject: Sending update for object ${objectId}`, {
attributeCount: payload.attributes.length,
payload: JSON.stringify(payload, null, 2)
});
await this.request(`/object/${objectId}`, {
method: 'PUT',
body: JSON.stringify(payload),
});
logger.info(`JiraAssetsClient.updateObject: Successfully updated object ${objectId}`);
return true;
} catch (error) {
logger.error(`JiraAssetsClient: Failed to update object ${objectId}`, error);
return false;
}
}
// ==========================================================================
// Object Parsing
// ==========================================================================
parseObject<T extends CMDBObject>(jiraObj: JiraAssetsObject): T | null {
const typeId = jiraObj.objectType?.id;
const typeName = TYPE_ID_TO_NAME[typeId] || JIRA_NAME_TO_TYPE[jiraObj.objectType?.name];
if (!typeName) {
logger.warn(`JiraAssetsClient: Unknown object type: ${jiraObj.objectType?.name} (ID: ${typeId})`);
return null;
}
const typeDef = OBJECT_TYPES[typeName];
if (!typeDef) {
return null;
}
const result: Record<string, unknown> = {
id: jiraObj.id.toString(),
objectKey: jiraObj.objectKey,
label: jiraObj.label,
_objectType: typeName,
_jiraUpdatedAt: jiraObj.updated || new Date().toISOString(),
_jiraCreatedAt: jiraObj.created || new Date().toISOString(),
};
// Parse each attribute based on schema
for (const attrDef of typeDef.attributes) {
const jiraAttr = this.findAttribute(jiraObj.attributes, attrDef.jiraId, attrDef.name);
result[attrDef.fieldName] = this.parseAttributeValue(jiraAttr, attrDef);
}
return result as T;
}
private findAttribute(
attributes: JiraAssetsAttribute[],
jiraId: number,
name: string
): JiraAssetsAttribute | undefined {
// Try by ID first
let attr = attributes.find(a => a.objectTypeAttributeId === jiraId);
if (attr) return attr;
// Try by name
attr = attributes.find(a =>
a.objectTypeAttribute?.name === name ||
a.objectTypeAttribute?.name?.toLowerCase() === name.toLowerCase()
);
return attr;
}
private parseAttributeValue(
jiraAttr: JiraAssetsAttribute | undefined,
attrDef: { type: string; isMultiple: boolean }
): unknown {
if (!jiraAttr?.objectAttributeValues?.length) {
return attrDef.isMultiple ? [] : null;
}
const values = jiraAttr.objectAttributeValues;
switch (attrDef.type) {
case 'reference': {
const refs = values
.filter(v => v.referencedObject)
.map(v => ({
objectId: v.referencedObject!.id.toString(),
objectKey: v.referencedObject!.objectKey,
label: v.referencedObject!.label,
} as ObjectReference));
return attrDef.isMultiple ? refs : refs[0] || null;
}
case 'text':
case 'textarea':
case 'url':
case 'email':
case 'select':
case 'user': {
const val = values[0]?.displayValue ?? values[0]?.value ?? null;
// Strip HTML if present
if (val && typeof val === 'string' && val.includes('<')) {
return this.stripHtml(val);
}
return val;
}
case 'integer': {
const val = values[0]?.value;
return val ? parseInt(val, 10) : null;
}
case 'float': {
const val = values[0]?.value;
return val ? parseFloat(val) : null;
}
case 'boolean': {
const val = values[0]?.value;
return val === 'true' || val === 'Ja';
}
case 'date':
case 'datetime': {
return values[0]?.value ?? values[0]?.displayValue ?? null;
}
case 'status': {
const statusVal = values[0]?.status;
if (statusVal) {
return statusVal.name || null;
}
return values[0]?.displayValue ?? values[0]?.value ?? null;
}
default:
return values[0]?.displayValue ?? values[0]?.value ?? null;
}
}
private stripHtml(html: string): string {
return html
.replace(/<[^>]*>/g, '')
.replace(/&nbsp;/g, ' ')
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
.replace(/\s+/g, ' ')
.trim();
}
}
// Export singleton instance
export const jiraAssetsClient = new JiraAssetsClient();

View File

@@ -36,7 +36,8 @@ const mockApplications: ApplicationDetails[] = [
complexityFactor: { objectId: '4', key: 'CMP-4', name: '4 - Zeer hoog' },
numberOfUsers: null,
governanceModel: { objectId: 'A', key: 'GOV-A', name: 'Centraal Beheer' },
applicationCluster: null,
applicationSubteam: null,
applicationTeam: null,
applicationType: null,
platform: null,
requiredEffortApplicationManagement: null,
@@ -63,7 +64,8 @@ const mockApplications: ApplicationDetails[] = [
complexityFactor: { objectId: '3', key: 'CMP-3', name: '3 - Hoog' },
numberOfUsers: null,
governanceModel: null,
applicationCluster: null,
applicationSubteam: null,
applicationTeam: null,
applicationType: null,
platform: null,
requiredEffortApplicationManagement: null,
@@ -90,7 +92,8 @@ const mockApplications: ApplicationDetails[] = [
complexityFactor: null,
numberOfUsers: null,
governanceModel: { objectId: 'C', key: 'GOV-C', name: 'Uitbesteed met ICMT-Regie' },
applicationCluster: null,
applicationSubteam: null,
applicationTeam: null,
applicationType: null,
platform: null,
requiredEffortApplicationManagement: null,
@@ -117,7 +120,8 @@ const mockApplications: ApplicationDetails[] = [
complexityFactor: { objectId: '4', key: 'CMP-4', name: '4 - Zeer hoog' },
numberOfUsers: null,
governanceModel: { objectId: 'A', key: 'GOV-A', name: 'Centraal Beheer' },
applicationCluster: null,
applicationSubteam: null,
applicationTeam: null,
applicationType: null,
platform: null,
requiredEffortApplicationManagement: null,
@@ -144,7 +148,8 @@ const mockApplications: ApplicationDetails[] = [
complexityFactor: { objectId: '2', key: 'CMP-2', name: '2 - Gemiddeld' },
numberOfUsers: null,
governanceModel: null,
applicationCluster: null,
applicationSubteam: null,
applicationTeam: null,
applicationType: null,
platform: null,
requiredEffortApplicationManagement: null,
@@ -171,7 +176,8 @@ const mockApplications: ApplicationDetails[] = [
complexityFactor: { objectId: '3', key: 'CMP-3', name: '3 - Hoog' },
numberOfUsers: { objectId: '7', key: 'USR-7', name: '> 15.000' },
governanceModel: { objectId: 'A', key: 'GOV-A', name: 'Centraal Beheer' },
applicationCluster: null,
applicationSubteam: null,
applicationTeam: null,
applicationType: null,
platform: null,
requiredEffortApplicationManagement: null,
@@ -198,7 +204,8 @@ const mockApplications: ApplicationDetails[] = [
complexityFactor: { objectId: '2', key: 'CMP-2', name: '2 - Gemiddeld' },
numberOfUsers: null,
governanceModel: null,
applicationCluster: null,
applicationSubteam: null,
applicationTeam: null,
applicationType: null,
platform: null,
requiredEffortApplicationManagement: null,
@@ -225,7 +232,8 @@ const mockApplications: ApplicationDetails[] = [
complexityFactor: { objectId: '2', key: 'CMP-2', name: '2 - Gemiddeld' },
numberOfUsers: { objectId: '6', key: 'USR-6', name: '10.000 - 15.000' },
governanceModel: { objectId: 'C', key: 'GOV-C', name: 'Uitbesteed met ICMT-Regie' },
applicationCluster: null,
applicationSubteam: null,
applicationTeam: null,
applicationType: null,
platform: null,
requiredEffortApplicationManagement: null,
@@ -252,7 +260,8 @@ const mockApplications: ApplicationDetails[] = [
complexityFactor: { objectId: '1', key: 'CMP-1', name: '1 - Laag' },
numberOfUsers: { objectId: '4', key: 'USR-4', name: '2.000 - 5.000' },
governanceModel: { objectId: 'C', key: 'GOV-C', name: 'Uitbesteed met ICMT-Regie' },
applicationCluster: null,
applicationSubteam: null,
applicationTeam: null,
applicationType: null,
platform: null,
requiredEffortApplicationManagement: null,
@@ -279,7 +288,8 @@ const mockApplications: ApplicationDetails[] = [
complexityFactor: { objectId: '1', key: 'CMP-1', name: '1 - Laag' },
numberOfUsers: { objectId: '1', key: 'USR-1', name: '< 100' },
governanceModel: { objectId: 'D', key: 'GOV-D', name: 'Uitbesteed met Business-Regie' },
applicationCluster: null,
applicationSubteam: null,
applicationTeam: null,
applicationType: null,
platform: null,
requiredEffortApplicationManagement: null,
@@ -347,10 +357,10 @@ const mockBusinessImpactAnalyses: ReferenceValue[] = [
{ objectId: '9', key: 'BIA-9', name: 'BIA-2022-0089 (Klasse C)' },
];
const mockApplicationClusters: ReferenceValue[] = [
{ objectId: '1', key: 'CLUSTER-1', name: 'Zorgapplicaties' },
{ objectId: '2', key: 'CLUSTER-2', name: 'Bedrijfsvoering' },
{ objectId: '3', key: 'CLUSTER-3', name: 'Infrastructuur' },
const mockApplicationSubteams: ReferenceValue[] = [
{ objectId: '1', key: 'SUBTEAM-1', name: 'Zorgapplicaties' },
{ objectId: '2', key: 'SUBTEAM-2', name: 'Bedrijfsvoering' },
{ objectId: '3', key: 'SUBTEAM-3', name: 'Infrastructuur' },
];
const mockApplicationTypes: ReferenceValue[] = [
@@ -420,11 +430,11 @@ export class MockDataService {
filtered = filtered.filter((app) => !!app.complexityFactor);
}
// Apply applicationCluster filter
if (filters.applicationCluster === 'empty') {
filtered = filtered.filter((app) => !app.applicationCluster);
} else if (filters.applicationCluster === 'filled') {
filtered = filtered.filter((app) => !!app.applicationCluster);
// Apply applicationSubteam filter
if (filters.applicationSubteam === 'empty') {
filtered = filtered.filter((app) => !app.applicationSubteam);
} else if (filters.applicationSubteam === 'filled') {
filtered = filtered.filter((app) => !!app.applicationSubteam);
}
// Apply applicationType filter
@@ -468,7 +478,8 @@ export class MockDataService {
governanceModel: app.governanceModel,
dynamicsFactor: app.dynamicsFactor,
complexityFactor: app.complexityFactor,
applicationCluster: app.applicationCluster,
applicationSubteam: app.applicationSubteam,
applicationTeam: app.applicationTeam,
applicationType: app.applicationType,
platform: app.platform,
requiredEffortApplicationManagement: effort,
@@ -501,7 +512,8 @@ export class MockDataService {
complexityFactor?: ReferenceValue;
numberOfUsers?: ReferenceValue;
governanceModel?: ReferenceValue;
applicationCluster?: ReferenceValue;
applicationSubteam?: ReferenceValue;
applicationTeam?: ReferenceValue;
applicationType?: ReferenceValue;
hostingType?: ReferenceValue;
businessImpactAnalyse?: ReferenceValue;
@@ -527,8 +539,11 @@ export class MockDataService {
if (updates.governanceModel !== undefined) {
app.governanceModel = updates.governanceModel;
}
if (updates.applicationCluster !== undefined) {
app.applicationCluster = updates.applicationCluster;
if (updates.applicationSubteam !== undefined) {
app.applicationSubteam = updates.applicationSubteam;
}
if (updates.applicationTeam !== undefined) {
app.applicationTeam = updates.applicationTeam;
}
if (updates.applicationType !== undefined) {
app.applicationType = updates.applicationType;
@@ -539,12 +554,6 @@ export class MockDataService {
if (updates.businessImpactAnalyse !== undefined) {
app.businessImpactAnalyse = updates.businessImpactAnalyse;
}
if (updates.applicationCluster !== undefined) {
app.applicationCluster = updates.applicationCluster;
}
if (updates.applicationType !== undefined) {
app.applicationType = updates.applicationType;
}
return true;
}
@@ -601,7 +610,7 @@ export class MockDataService {
return [];
}
async getApplicationClusters(): Promise<ReferenceValue[]> {
async getApplicationSubteams(): Promise<ReferenceValue[]> {
// Return empty for mock - in real implementation, this comes from Jira
return [];
}
@@ -671,7 +680,8 @@ export class MockDataService {
governanceModel: app.governanceModel,
dynamicsFactor: app.dynamicsFactor,
complexityFactor: app.complexityFactor,
applicationCluster: app.applicationCluster,
applicationSubteam: app.applicationSubteam,
applicationTeam: app.applicationTeam,
applicationType: app.applicationType,
platform: app.platform,
requiredEffortApplicationManagement: app.requiredEffortApplicationManagement,
@@ -726,8 +736,8 @@ export class MockDataService {
});
}
// Group all applications (regular + platforms + workloads) by cluster
const clusterMap = new Map<string, {
// Group all applications (regular + platforms + workloads) by subteam
const subteamMap = new Map<string, {
regular: ApplicationListItem[];
platforms: import('../types/index.js').PlatformWithWorkloads[];
}>();
@@ -739,39 +749,39 @@ export class MockDataService {
platforms: [],
};
// Group regular applications by cluster
// Group regular applications by subteam
for (const app of regularApplications) {
if (app.applicationCluster) {
const clusterId = app.applicationCluster.objectId;
if (!clusterMap.has(clusterId)) {
clusterMap.set(clusterId, { regular: [], platforms: [] });
if (app.applicationSubteam) {
const subteamId = app.applicationSubteam.objectId;
if (!subteamMap.has(subteamId)) {
subteamMap.set(subteamId, { regular: [], platforms: [] });
}
clusterMap.get(clusterId)!.regular.push(app);
subteamMap.get(subteamId)!.regular.push(app);
} else {
unassigned.regular.push(app);
}
}
// Group platforms by cluster
// Group platforms by subteam
for (const platformWithWorkloads of platformsWithWorkloads) {
const platform = platformWithWorkloads.platform;
if (platform.applicationCluster) {
const clusterId = platform.applicationCluster.objectId;
if (!clusterMap.has(clusterId)) {
clusterMap.set(clusterId, { regular: [], platforms: [] });
if (platform.applicationSubteam) {
const subteamId = platform.applicationSubteam.objectId;
if (!subteamMap.has(subteamId)) {
subteamMap.set(subteamId, { regular: [], platforms: [] });
}
clusterMap.get(clusterId)!.platforms.push(platformWithWorkloads);
subteamMap.get(subteamId)!.platforms.push(platformWithWorkloads);
} else {
unassigned.platforms.push(platformWithWorkloads);
}
}
// Get all clusters
const allClusters = mockApplicationClusters;
const clusters = allClusters.map(cluster => {
const clusterData = clusterMap.get(cluster.objectId) || { regular: [], platforms: [] };
const regularApps = clusterData.regular;
const platforms = clusterData.platforms;
// Build subteams from mock data
const allSubteams = mockApplicationSubteams;
const subteams: import('../types/index.js').TeamDashboardSubteam[] = allSubteams.map(subteamRef => {
const subteamData = subteamMap.get(subteamRef.objectId) || { regular: [], platforms: [] };
const regularApps = subteamData.regular;
const platforms = subteamData.platforms;
// Calculate total effort: regular apps + platforms (including their workloads)
const regularEffort = regularApps.reduce((sum, app) =>
@@ -803,7 +813,7 @@ export class MockDataService {
}
return {
cluster,
subteam: subteamRef,
applications: regularApps,
platforms,
totalEffort,
@@ -812,7 +822,28 @@ export class MockDataService {
applicationCount,
byGovernanceModel,
};
});
}).filter(s => s.applicationCount > 0); // Only include subteams with apps
// Create a virtual team containing all subteams (since Team doesn't exist in mock data)
const virtualTeam: import('../types/index.js').TeamDashboardTeam = {
team: {
objectId: 'mock-team-1',
key: 'TEAM-1',
name: 'Mock Team',
teamType: 'Business',
},
subteams,
totalEffort: subteams.reduce((sum, s) => sum + s.totalEffort, 0),
minEffort: subteams.reduce((sum, s) => sum + s.minEffort, 0),
maxEffort: subteams.reduce((sum, s) => sum + s.maxEffort, 0),
applicationCount: subteams.reduce((sum, s) => sum + s.applicationCount, 0),
byGovernanceModel: subteams.reduce((acc, s) => {
for (const [key, count] of Object.entries(s.byGovernanceModel)) {
acc[key] = (acc[key] || 0) + count;
}
return acc;
}, {} as Record<string, number>),
};
// Calculate unassigned totals
const unassignedRegularEffort = unassigned.regular.reduce((sum, app) =>
@@ -842,8 +873,9 @@ export class MockDataService {
}
return {
clusters,
teams: subteams.length > 0 ? [virtualTeam] : [],
unassigned: {
subteam: null,
applications: unassigned.regular,
platforms: unassigned.platforms,
totalEffort: unassignedTotalEffort,

View File

@@ -0,0 +1,463 @@
/**
* SyncEngine - Background synchronization service
*
* Handles:
* - Full sync at startup and periodically
* - Incremental sync every 30 seconds
* - Schema-driven sync for all object types
*/
import { logger } from './logger.js';
import { cacheStore } from './cacheStore.js';
import { jiraAssetsClient, JiraObjectNotFoundError } from './jiraAssetsClient.js';
import { OBJECT_TYPES, getObjectTypesBySyncPriority } from '../generated/jira-schema.js';
import type { CMDBObject, CMDBObjectTypeName } from '../generated/jira-types.js';
// =============================================================================
// Types
// =============================================================================
export interface SyncStats {
objectType: string;
objectsProcessed: number;
relationsExtracted: number;
duration: number;
}
export interface SyncResult {
success: boolean;
stats: SyncStats[];
totalObjects: number;
totalRelations: number;
duration: number;
error?: string;
}
export interface SyncEngineStatus {
isRunning: boolean;
isSyncing: boolean;
lastFullSync: string | null;
lastIncrementalSync: string | null;
nextIncrementalSync: string | null;
incrementalInterval: number;
}
// =============================================================================
// Configuration
// =============================================================================
const DEFAULT_INCREMENTAL_INTERVAL = 30_000; // 30 seconds
const DEFAULT_BATCH_SIZE = 50;
// =============================================================================
// Sync Engine Implementation
// =============================================================================
class SyncEngine {
private isRunning: boolean = false;
private isSyncing: boolean = false; // For full/incremental syncs
private syncingTypes: Set<CMDBObjectTypeName> = new Set(); // Track which types are being synced
private incrementalTimer: NodeJS.Timeout | null = null;
private incrementalInterval: number;
private batchSize: number;
private lastIncrementalSync: Date | null = null;
constructor() {
this.incrementalInterval = parseInt(
process.env.SYNC_INCREMENTAL_INTERVAL_MS || String(DEFAULT_INCREMENTAL_INTERVAL),
10
);
this.batchSize = parseInt(
process.env.JIRA_API_BATCH_SIZE || String(DEFAULT_BATCH_SIZE),
10
);
}
// ==========================================================================
// Lifecycle
// ==========================================================================
/**
* Initialize the sync engine
* Performs initial sync if cache is cold, then starts incremental sync
*/
async initialize(): Promise<void> {
if (this.isRunning) {
logger.warn('SyncEngine: Already running');
return;
}
logger.info('SyncEngine: Initializing...');
this.isRunning = true;
// Check if we need a full sync
const stats = cacheStore.getStats();
const lastFullSync = stats.lastFullSync;
const needsFullSync = !stats.isWarm || !lastFullSync || this.isStale(lastFullSync, 24 * 60 * 60 * 1000);
if (needsFullSync) {
logger.info('SyncEngine: Cache is cold or stale, starting full sync in background...');
// Run full sync in background (non-blocking)
this.fullSync().catch(err => {
logger.error('SyncEngine: Background full sync failed', err);
});
} else {
logger.info('SyncEngine: Cache is warm, skipping initial full sync');
}
// Start incremental sync scheduler
this.startIncrementalSyncScheduler();
logger.info('SyncEngine: Initialized');
}
/**
* Stop the sync engine
*/
stop(): void {
logger.info('SyncEngine: Stopping...');
this.isRunning = false;
if (this.incrementalTimer) {
clearInterval(this.incrementalTimer);
this.incrementalTimer = null;
}
logger.info('SyncEngine: Stopped');
}
/**
* Check if a timestamp is stale
*/
private isStale(timestamp: string, maxAgeMs: number): boolean {
const age = Date.now() - new Date(timestamp).getTime();
return age > maxAgeMs;
}
// ==========================================================================
// Full Sync
// ==========================================================================
/**
* Perform a full sync of all object types
*/
async fullSync(): Promise<SyncResult> {
if (this.isSyncing) {
logger.warn('SyncEngine: Sync already in progress');
return {
success: false,
stats: [],
totalObjects: 0,
totalRelations: 0,
duration: 0,
error: 'Sync already in progress',
};
}
this.isSyncing = true;
const startTime = Date.now();
const stats: SyncStats[] = [];
let totalObjects = 0;
let totalRelations = 0;
logger.info('SyncEngine: Starting full sync...');
try {
// Get object types sorted by sync priority
const objectTypes = getObjectTypesBySyncPriority();
for (const typeDef of objectTypes) {
const typeStat = await this.syncObjectType(typeDef.typeName as CMDBObjectTypeName);
stats.push(typeStat);
totalObjects += typeStat.objectsProcessed;
totalRelations += typeStat.relationsExtracted;
}
// Update sync metadata
const now = new Date().toISOString();
cacheStore.setSyncMetadata('lastFullSync', now);
cacheStore.setSyncMetadata('lastIncrementalSync', now);
this.lastIncrementalSync = new Date();
const duration = Date.now() - startTime;
logger.info(`SyncEngine: Full sync complete. ${totalObjects} objects, ${totalRelations} relations in ${duration}ms`);
return {
success: true,
stats,
totalObjects,
totalRelations,
duration,
};
} catch (error) {
logger.error('SyncEngine: Full sync failed', error);
return {
success: false,
stats,
totalObjects,
totalRelations,
duration: Date.now() - startTime,
error: error instanceof Error ? error.message : 'Unknown error',
};
} finally {
this.isSyncing = false;
}
}
/**
* Sync a single object type
*/
private async syncObjectType(typeName: CMDBObjectTypeName): Promise<SyncStats> {
const startTime = Date.now();
let objectsProcessed = 0;
let relationsExtracted = 0;
try {
const typeDef = OBJECT_TYPES[typeName];
if (!typeDef) {
logger.warn(`SyncEngine: Unknown type ${typeName}`);
return { objectType: typeName, objectsProcessed: 0, relationsExtracted: 0, duration: 0 };
}
logger.debug(`SyncEngine: Syncing ${typeName}...`);
// Fetch all objects from Jira
const jiraObjects = await jiraAssetsClient.getAllObjectsOfType(typeName, this.batchSize);
// Parse and cache objects
const parsedObjects: CMDBObject[] = [];
for (const jiraObj of jiraObjects) {
const parsed = jiraAssetsClient.parseObject(jiraObj);
if (parsed) {
parsedObjects.push(parsed);
}
}
// Batch upsert to cache
if (parsedObjects.length > 0) {
cacheStore.batchUpsertObjects(typeName, parsedObjects);
objectsProcessed = parsedObjects.length;
// Extract relations
for (const obj of parsedObjects) {
cacheStore.extractAndStoreRelations(typeName, obj);
relationsExtracted++;
}
}
const duration = Date.now() - startTime;
logger.debug(`SyncEngine: Synced ${objectsProcessed} ${typeName} objects in ${duration}ms`);
return {
objectType: typeName,
objectsProcessed,
relationsExtracted,
duration,
};
} catch (error) {
logger.error(`SyncEngine: Failed to sync ${typeName}`, error);
return {
objectType: typeName,
objectsProcessed,
relationsExtracted,
duration: Date.now() - startTime,
};
}
}
// ==========================================================================
// Incremental Sync
// ==========================================================================
/**
* Start the incremental sync scheduler
*/
private startIncrementalSyncScheduler(): void {
if (this.incrementalTimer) {
clearInterval(this.incrementalTimer);
}
logger.info(`SyncEngine: Starting incremental sync scheduler (every ${this.incrementalInterval}ms)`);
this.incrementalTimer = setInterval(() => {
if (!this.isSyncing && this.isRunning) {
this.incrementalSync().catch(err => {
logger.error('SyncEngine: Incremental sync failed', err);
});
}
}, this.incrementalInterval);
}
/**
* Perform an incremental sync (only updated objects)
*
* Note: On Jira Data Center, IQL-based incremental sync is not supported.
* We instead check if a periodic full sync is needed.
*/
async incrementalSync(): Promise<{ success: boolean; updatedCount: number }> {
if (this.isSyncing) {
return { success: false, updatedCount: 0 };
}
this.isSyncing = true;
try {
// Get the last sync time
const lastSyncStr = cacheStore.getSyncMetadata('lastIncrementalSync');
const since = lastSyncStr
? new Date(lastSyncStr)
: new Date(Date.now() - 60000); // Default: last minute
logger.debug(`SyncEngine: Incremental sync since ${since.toISOString()}`);
// Fetch updated objects from Jira
const updatedObjects = await jiraAssetsClient.getUpdatedObjectsSince(since, this.batchSize);
// If no objects returned (e.g., Data Center doesn't support IQL incremental sync),
// check if we should trigger a full sync instead
if (updatedObjects.length === 0) {
const lastFullSyncStr = cacheStore.getSyncMetadata('lastFullSync');
if (lastFullSyncStr) {
const lastFullSync = new Date(lastFullSyncStr);
const fullSyncAge = Date.now() - lastFullSync.getTime();
const FULL_SYNC_INTERVAL = 15 * 60 * 1000; // 15 minutes
if (fullSyncAge > FULL_SYNC_INTERVAL) {
logger.info('SyncEngine: Triggering periodic full sync (incremental not available)');
// Release the lock before calling fullSync
this.isSyncing = false;
await this.fullSync();
return { success: true, updatedCount: 0 };
}
}
// Update timestamp even if no objects were synced
const now = new Date();
cacheStore.setSyncMetadata('lastIncrementalSync', now.toISOString());
this.lastIncrementalSync = now;
return { success: true, updatedCount: 0 };
}
let updatedCount = 0;
for (const jiraObj of updatedObjects) {
const parsed = jiraAssetsClient.parseObject(jiraObj);
if (parsed) {
const typeName = parsed._objectType as CMDBObjectTypeName;
cacheStore.upsertObject(typeName, parsed);
cacheStore.extractAndStoreRelations(typeName, parsed);
updatedCount++;
}
}
// Update sync metadata
const now = new Date();
cacheStore.setSyncMetadata('lastIncrementalSync', now.toISOString());
this.lastIncrementalSync = now;
if (updatedCount > 0) {
logger.info(`SyncEngine: Incremental sync updated ${updatedCount} objects`);
}
return { success: true, updatedCount };
} catch (error) {
logger.error('SyncEngine: Incremental sync failed', error);
return { success: false, updatedCount: 0 };
} finally {
this.isSyncing = false;
}
}
// ==========================================================================
// Manual Sync Triggers
// ==========================================================================
/**
* Trigger a sync for a specific object type
* Allows concurrent syncs for different types, but blocks if:
* - A full sync is in progress
* - An incremental sync is in progress
* - This specific type is already being synced
*/
async syncType(typeName: CMDBObjectTypeName): Promise<SyncStats> {
// Block if a full or incremental sync is running
if (this.isSyncing) {
throw new Error('Full or incremental sync already in progress');
}
// Block if this specific type is already being synced
if (this.syncingTypes.has(typeName)) {
throw new Error(`Sync already in progress for ${typeName}`);
}
this.syncingTypes.add(typeName);
try {
return await this.syncObjectType(typeName);
} finally {
this.syncingTypes.delete(typeName);
}
}
/**
* Force sync a single object
* If the object was deleted from Jira, it will be removed from the local cache
*/
async syncObject(typeName: CMDBObjectTypeName, objectId: string): Promise<boolean> {
try {
const jiraObj = await jiraAssetsClient.getObject(objectId);
if (!jiraObj) return false;
const parsed = jiraAssetsClient.parseObject(jiraObj);
if (!parsed) return false;
cacheStore.upsertObject(typeName, parsed);
cacheStore.extractAndStoreRelations(typeName, parsed);
return true;
} catch (error) {
// If object was deleted from Jira, remove it from our cache
if (error instanceof JiraObjectNotFoundError) {
const deleted = cacheStore.deleteObject(typeName, objectId);
if (deleted) {
logger.info(`SyncEngine: Removed deleted object ${typeName}/${objectId} from cache`);
}
return false;
}
logger.error(`SyncEngine: Failed to sync object ${objectId}`, error);
return false;
}
}
// ==========================================================================
// Status
// ==========================================================================
/**
* Get current sync engine status
*/
getStatus(): SyncEngineStatus {
const stats = cacheStore.getStats();
let nextIncrementalSync: string | null = null;
if (this.isRunning && this.lastIncrementalSync) {
const nextTime = new Date(this.lastIncrementalSync.getTime() + this.incrementalInterval);
nextIncrementalSync = nextTime.toISOString();
}
return {
isRunning: this.isRunning,
isSyncing: this.isSyncing,
lastFullSync: stats.lastFullSync,
lastIncrementalSync: stats.lastIncrementalSync,
nextIncrementalSync,
incrementalInterval: this.incrementalInterval,
};
}
}
// Export singleton instance
export const syncEngine = new SyncEngine();

View File

@@ -26,6 +26,7 @@ export interface ReferenceValue {
remarks?: string; // Remarks attribute for Governance Model
application?: string; // Application attribute for Governance Model
indicators?: string; // Indicators attribute for Business Impact Analyse
teamType?: string; // Type attribute for Team objects (Business, Enabling, Staf)
}
// Application list item (summary view)
@@ -38,7 +39,8 @@ export interface ApplicationListItem {
governanceModel: ReferenceValue | null;
dynamicsFactor: ReferenceValue | null;
complexityFactor: ReferenceValue | null;
applicationCluster: ReferenceValue | null;
applicationSubteam: ReferenceValue | null;
applicationTeam: ReferenceValue | null;
applicationType: ReferenceValue | null;
platform: ReferenceValue | null; // Reference to parent Platform Application Component
requiredEffortApplicationManagement: number | null; // Calculated field
@@ -74,7 +76,8 @@ export interface ApplicationDetails {
complexityFactor: ReferenceValue | null;
numberOfUsers: ReferenceValue | null;
governanceModel: ReferenceValue | null;
applicationCluster: ReferenceValue | null;
applicationSubteam: ReferenceValue | null;
applicationTeam: ReferenceValue | null;
applicationType: ReferenceValue | null;
platform: ReferenceValue | null; // Reference to parent Platform Application Component
requiredEffortApplicationManagement: number | null; // Calculated field
@@ -92,7 +95,7 @@ export interface SearchFilters {
governanceModel?: 'all' | 'filled' | 'empty';
dynamicsFactor?: 'all' | 'filled' | 'empty';
complexityFactor?: 'all' | 'filled' | 'empty';
applicationCluster?: 'all' | 'filled' | 'empty';
applicationSubteam?: 'all' | 'filled' | 'empty' | string; // Can be 'all', 'empty', or a specific subteam name
applicationType?: 'all' | 'filled' | 'empty';
organisation?: string;
hostingType?: string;
@@ -168,7 +171,8 @@ export interface PendingChanges {
complexityFactor?: { from: ReferenceValue | null; to: ReferenceValue };
numberOfUsers?: { from: ReferenceValue | null; to: ReferenceValue };
governanceModel?: { from: ReferenceValue | null; to: ReferenceValue };
applicationCluster?: { from: ReferenceValue | null; to: ReferenceValue };
applicationSubteam?: { from: ReferenceValue | null; to: ReferenceValue };
applicationTeam?: { from: ReferenceValue | null; to: ReferenceValue };
applicationType?: { from: ReferenceValue | null; to: ReferenceValue };
}
@@ -189,7 +193,8 @@ export interface ReferenceOptions {
numberOfUsers: ReferenceValue[];
governanceModels: ReferenceValue[];
applicationFunctions: ReferenceValue[];
applicationClusters: ReferenceValue[];
applicationSubteams: ReferenceValue[];
applicationTeams: ReferenceValue[];
applicationTypes: ReferenceValue[];
organisations: ReferenceValue[];
hostingTypes: ReferenceValue[];
@@ -297,6 +302,31 @@ export interface PlatformWithWorkloads {
totalEffort: number; // platformEffort + workloadsEffort
}
// Subteam level in team dashboard hierarchy
export interface TeamDashboardSubteam {
subteam: ReferenceValue | null;
applications: ApplicationListItem[]; // Regular applications (non-Platform, non-Workload)
platforms: PlatformWithWorkloads[]; // Platforms with their workloads
totalEffort: number; // Sum of all applications + platforms + workloads
minEffort: number; // Sum of all minimum FTE values
maxEffort: number; // Sum of all maximum FTE values
applicationCount: number; // Count of all applications (including platforms and workloads)
byGovernanceModel: Record<string, number>; // Distribution per governance model
}
// Team level in team dashboard hierarchy (contains subteams)
export interface TeamDashboardTeam {
team: ReferenceValue | null; // team.teamType contains "Business", "Enabling", or "Staf"
subteams: TeamDashboardSubteam[];
// Aggregated KPIs (sum of all subteams)
totalEffort: number;
minEffort: number;
maxEffort: number;
applicationCount: number;
byGovernanceModel: Record<string, number>;
}
// Legacy type for backward compatibility (deprecated)
export interface TeamDashboardCluster {
cluster: ReferenceValue | null;
applications: ApplicationListItem[]; // Regular applications (non-Platform, non-Workload)
@@ -309,16 +339,8 @@ export interface TeamDashboardCluster {
}
export interface TeamDashboardData {
clusters: TeamDashboardCluster[];
unassigned: {
applications: ApplicationListItem[]; // Regular applications (non-Platform, non-Workload)
platforms: PlatformWithWorkloads[]; // Platforms with their workloads
totalEffort: number; // Sum of all applications + platforms + workloads
minEffort: number; // Sum of all minimum FTE values
maxEffort: number; // Sum of all maximum FTE values
applicationCount: number; // Count of all applications (including platforms and workloads)
byGovernanceModel: Record<string, number>; // Distribution per governance model
};
teams: TeamDashboardTeam[];
unassigned: TeamDashboardSubteam; // Apps without team assignment
}
// Jira Assets API types
@@ -347,6 +369,9 @@ export interface JiraAssetsAttribute {
objectKey: string;
label: string;
};
status?: {
name: string;
};
}>;
}
@@ -364,7 +389,8 @@ export interface ApplicationUpdateRequest {
complexityFactor?: string;
numberOfUsers?: string;
governanceModel?: string;
applicationCluster?: string;
applicationSubteam?: string;
applicationTeam?: string;
applicationType?: string;
hostingType?: string;
businessImpactAnalyse?: string;