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:
@@ -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",
|
||||
|
||||
982
backend/scripts/generate-schema.ts
Normal file
982
backend/scripts/generate-schema.ts
Normal 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);
|
||||
});
|
||||
@@ -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(', ')}`);
|
||||
|
||||
54
backend/src/generated/db-schema.sql
Normal file
54
backend/src/generated/db-schema.sql
Normal 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);
|
||||
894
backend/src/generated/jira-schema.ts
Normal file
894
backend/src/generated/jira-schema.ts
Normal 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);
|
||||
}
|
||||
933
backend/src/generated/jira-types.ts
Normal file
933
backend/src/generated/jira-types.ts
Normal 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';
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
135
backend/src/routes/cache.ts
Normal 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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
176
backend/src/routes/objects.ts
Normal file
176
backend/src/routes/objects.ts
Normal 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;
|
||||
|
||||
@@ -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' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
151
backend/src/routes/schema.ts
Normal file
151
backend/src/routes/schema.ts
Normal 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;
|
||||
74
backend/src/routes/search.ts
Normal file
74
backend/src/routes/search.ts
Normal 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;
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
660
backend/src/services/cacheStore.ts
Normal file
660
backend/src/services/cacheStore.ts
Normal 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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
445
backend/src/services/cmdbService.ts
Normal file
445
backend/src/services/cmdbService.ts
Normal 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();
|
||||
|
||||
254
backend/src/services/conflictResolver.ts
Normal file
254
backend/src/services/conflictResolver.ts
Normal 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
@@ -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
425
backend/src/services/jiraAssetsClient.ts
Normal file
425
backend/src/services/jiraAssetsClient.ts
Normal 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(/ /g, ' ')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const jiraAssetsClient = new JiraAssetsClient();
|
||||
|
||||
@@ -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,
|
||||
|
||||
463
backend/src/services/syncEngine.ts
Normal file
463
backend/src/services/syncEngine.ts
Normal 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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user