/home/techb158/cosmic.abdallabala.com/src/services
NameSizeModeActions
dashboardService.js21770666editdlrm
gateWorkflowService.js72960666editdlrm
integrationService.js274610666editdlrm
oauthService.js92820666editdlrm
reportingService.js186700666editdlrm
Edit: /home/techb158/cosmic.abdallabala.com/src/services/integrationService.js (27461B)
const { list, findById, insert, update, audit } = require("../storage/jsonDatabase.js"); const { createLivePmClient, parseLiveConfig } = require("../integrations/livePmClientFactory.js"); const SUPPORTED_PM_PROVIDERS = { Trello: { label: "Trello", externalProjectName: "Board", workItemName: "Card", mitigationObjectName: "Checklist item", statusObjectName: "List", riskFieldName: "Custom field or badge", evidenceObjectName: "Attachment or comment" }, Jira: { label: "Jira", externalProjectName: "Project", workItemName: "Issue", mitigationObjectName: "Sub-task or linked issue", statusObjectName: "Workflow status", riskFieldName: "Custom field", evidenceObjectName: "Attachment or comment" }, Asana: { label: "Asana", externalProjectName: "Project", workItemName: "Task", mitigationObjectName: "Subtask", statusObjectName: "Section or custom status field", riskFieldName: "Custom field", evidenceObjectName: "Attachment or comment" }, "Microsoft Planner": { label: "Microsoft Planner", externalProjectName: "Plan", workItemName: "Task", mitigationObjectName: "Checklist item", statusObjectName: "Bucket or progress state", riskFieldName: "Category, description, or task details", evidenceObjectName: "Task reference or checklist evidence" } }; const VALID_PROVIDERS = Object.keys(SUPPORTED_PM_PROVIDERS); const VALID_STATUSES = ["Connected", "Needs configuration", "Disabled"]; const VALID_DIRECTIONS = ["COSMIC to PM", "PM to COSMIC", "Bidirectional"]; function normalizeProvider(value) { const provider = value || "Trello"; if (!VALID_PROVIDERS.includes(provider)) { throw new Error(`provider must be one of: ${VALID_PROVIDERS.join(", ")}`); } return provider; } function normalizeConnectionStatus(value) { const status = value || "Needs configuration"; if (!VALID_STATUSES.includes(status)) { throw new Error(`connection_status must be one of: ${VALID_STATUSES.join(", ")}`); } return status; } function normalizeSyncDirection(value) { const direction = value || "COSMIC to PM"; if (!VALID_DIRECTIONS.includes(direction)) { throw new Error(`sync_direction must be one of: ${VALID_DIRECTIONS.join(", ")}`); } return direction; } function toProviderOptions() { return VALID_PROVIDERS.map(provider => Object.assign({ provider }, SUPPORTED_PM_PROVIDERS[provider])); } function parseJsonObject(value) { if (!value) return {}; if (typeof value === "object") return value; try { return JSON.parse(value); } catch (_error) { return {}; } } function providerSlug(provider) { return String(provider || "pm") .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/(^-|-$)/g, ""); } function compactId(id) { return String(id || "item").replace(/[^A-Za-z0-9]+/g, "").slice(-8).toUpperCase() || "ITEM"; } function buildExternalId(provider, localEntityId) { return `${providerSlug(provider).toUpperCase()}-${compactId(localEntityId)}`; } function buildExternalUrl(integration, externalId) { const base = integration.base_url || `https://example.invalid/${providerSlug(integration.provider)}`; return `${base.replace(/\/$/, "")}/${encodeURIComponent(externalId)}`; } function riskStatusToExternalStatus(provider, risk) { if (provider === "Trello") { if (risk.status === "Closed") return "Approved for deployment"; if (risk.residual_score >= 50 || risk.residualScore >= 50) return "Blocked by risk"; if (risk.status === "In mitigation") return "In mitigation"; return risk.lifecycle_phase || risk.lifecyclePhase || "Risk register"; } if (provider === "Jira") { if (risk.status === "Closed") return "Done"; if (risk.status === "In mitigation") return "In Progress"; return "To Do"; } if (provider === "Asana") { if (risk.status === "Closed") return "Complete"; if (risk.status === "In mitigation") return "In progress"; return "Open"; } if (provider === "Microsoft Planner") { if (risk.status === "Closed") return "Completed"; if (risk.status === "In mitigation") return "In progress"; if (risk.residual_score >= 50 || risk.residualScore >= 50) return "Blocked bucket"; return "Not started"; } return risk.status || "Open"; } function buildFieldMapping(provider, risk) { const capabilities = SUPPORTED_PM_PROVIDERS[provider]; return { provider, cosmicRiskId: risk.id, title: risk.title, dimension: risk.dimension, lifecyclePhase: risk.lifecycle_phase || risk.lifecyclePhase, probability: Number(risk.probability || 0), impact: Number(risk.impact || 0), detectability: Number(risk.detectability || 0), status: risk.status, approvalStatus: risk.approval_status || risk.approvalStatus, owner: risk.owner_display_name || risk.owner || "Unassigned", mapping: { project: capabilities.externalProjectName, workItem: capabilities.workItemName, mitigation: capabilities.mitigationObjectName, status: capabilities.statusObjectName, riskScore: capabilities.riskFieldName, evidence: capabilities.evidenceObjectName } }; } function toIntegrationView(row) { const provider = row.provider; const capabilities = SUPPORTED_PM_PROVIDERS[provider] || {}; return { id: row.id, projectId: row.project_id, provider, providerLabel: capabilities.label || provider, workspaceName: row.workspace_name || "", externalProjectKey: row.external_project_key || "", baseUrl: row.base_url || "", authMode: row.auth_mode || "OAuth or API token", connectionStatus: row.connection_status || "Needs configuration", liveEnabled: row.live_enabled === true, liveConfig: parseJsonObject(row.live_config_json), syncDirection: row.sync_direction || "COSMIC to PM", lastSyncAt: row.last_sync_at || null, capabilities, createdAt: row.created_at, updatedAt: row.updated_at }; } function toMappingView(database, row) { const integration = findById(database, "project_management_integrations", row.integration_id); const risk = row.local_entity_type === "Risk" ? findById(database, "risks", row.local_entity_id) : null; return { id: row.id, projectId: row.project_id, integrationId: row.integration_id, provider: integration ? integration.provider : row.provider || "Unknown", providerLabel: integration ? integration.provider : row.provider || "Unknown", localEntityType: row.local_entity_type, localEntityId: row.local_entity_id, localTitle: risk ? risk.title : row.local_title || "Unknown local item", externalItemType: row.external_item_type, externalItemId: row.external_item_id, externalItemKey: row.external_item_key, externalUrl: row.external_url || "", externalStatus: row.external_status || "Not synced", syncStatus: row.sync_status || "Pending", fieldMapping: parseJsonObject(row.field_mapping_json), lastSyncedAt: row.last_synced_at || null, createdAt: row.created_at, updatedAt: row.updated_at }; } function toSyncRunView(row) { return { id: row.id, projectId: row.project_id, integrationId: row.integration_id, provider: row.provider || "Unknown", status: row.status, startedAt: row.started_at, finishedAt: row.finished_at, createdCount: Number(row.created_count || 0), updatedCount: Number(row.updated_count || 0), failedCount: Number(row.failed_count || 0), summary: row.summary || "", createdAt: row.created_at, updatedAt: row.updated_at }; } class IntegrationService { constructor(database, options = {}) { this.database = database; this.oauthService = options.oauthService || null; this.fetchImpl = options.fetchImpl || global.fetch; this.env = options.env || process.env; } listSupportedProviders() { return toProviderOptions(); } listIntegrations(projectId) { const db = this.database.read(); return list(db, "project_management_integrations", item => item.project_id === projectId) .sort((a, b) => String(a.provider).localeCompare(String(b.provider))) .map(toIntegrationView); } getIntegration(integrationId) { const db = this.database.read(); const row = findById(db, "project_management_integrations", integrationId); return row ? toIntegrationView(row) : null; } createIntegration(projectId, payload, actorUserId = "system") { if (!payload || typeof payload !== "object") throw new Error("Integration payload is required"); const provider = normalizeProvider(payload.provider); return this.database.transaction(db => { if (!findById(db, "projects", projectId)) throw new Error(`Project not found: ${projectId}`); const existing = list(db, "project_management_integrations", item => item.project_id === projectId && item.provider === provider)[0]; if (existing) throw new Error(`${provider} integration already exists for this project`); const integration = insert(db, "project_management_integrations", { project_id: projectId, provider, workspace_name: payload.workspaceName || payload.workspace_name || `${provider} Workspace`, external_project_key: payload.externalProjectKey || payload.external_project_key || `${providerSlug(provider).toUpperCase()}-AI-RISK`, base_url: payload.baseUrl || payload.base_url || `https://example.invalid/${providerSlug(provider)}`, auth_mode: payload.authMode || payload.auth_mode || "OAuth or API token", connection_status: normalizeConnectionStatus(payload.connectionStatus || payload.connection_status || "Needs configuration"), sync_direction: normalizeSyncDirection(payload.syncDirection || payload.sync_direction || "COSMIC to PM"), last_sync_at: null, live_enabled: payload.liveEnabled === true || payload.live_enabled === true, live_config_json: JSON.stringify(payload.liveConfig || payload.live_config || {}) }, "PMI"); audit(db, { project_id: projectId, actor_user_id: actorUserId, entity_type: "ProjectManagementIntegration", entity_id: integration.id, action: "create", after_json: integration }); return toIntegrationView(integration); }); } updateIntegration(integrationId, payload, actorUserId = "system") { if (!payload || typeof payload !== "object") throw new Error("Integration patch is required"); return this.database.transaction(db => { const before = findById(db, "project_management_integrations", integrationId); if (!before) return null; const patch = {}; if (payload.workspaceName !== undefined || payload.workspace_name !== undefined) patch.workspace_name = payload.workspaceName || payload.workspace_name || ""; if (payload.externalProjectKey !== undefined || payload.external_project_key !== undefined) patch.external_project_key = payload.externalProjectKey || payload.external_project_key || ""; if (payload.baseUrl !== undefined || payload.base_url !== undefined) patch.base_url = payload.baseUrl || payload.base_url || ""; if (payload.authMode !== undefined || payload.auth_mode !== undefined) patch.auth_mode = payload.authMode || payload.auth_mode || "OAuth or API token"; if (payload.connectionStatus !== undefined || payload.connection_status !== undefined) patch.connection_status = normalizeConnectionStatus(payload.connectionStatus || payload.connection_status); if (payload.syncDirection !== undefined || payload.sync_direction !== undefined) patch.sync_direction = normalizeSyncDirection(payload.syncDirection || payload.sync_direction); if (payload.liveEnabled !== undefined || payload.live_enabled !== undefined) patch.live_enabled = payload.liveEnabled === true || payload.live_enabled === true; if (payload.liveConfig !== undefined || payload.live_config !== undefined) patch.live_config_json = JSON.stringify(payload.liveConfig || payload.live_config || {}); const after = update(db, "project_management_integrations", integrationId, patch); audit(db, { project_id: before.project_id, actor_user_id: actorUserId, entity_type: "ProjectManagementIntegration", entity_id: integrationId, action: "update", before_json: before, after_json: after }); return toIntegrationView(after); }); } listMappings(projectId) { const db = this.database.read(); return list(db, "external_work_item_mappings", item => item.project_id === projectId) .sort((a, b) => String(a.provider || "").localeCompare(String(b.provider || "")) || String(a.local_entity_id).localeCompare(String(b.local_entity_id))) .map(item => toMappingView(db, item)); } listSyncRuns(projectId) { const db = this.database.read(); return list(db, "project_management_sync_runs", item => item.project_id === projectId) .sort((a, b) => String(b.started_at || b.created_at || "").localeCompare(String(a.started_at || a.created_at || ""))) .map(toSyncRunView); } createRiskMapping(integrationId, riskId, payload = {}, actorUserId = "system") { return this.database.transaction(db => { const integration = findById(db, "project_management_integrations", integrationId); if (!integration) return null; const risk = findById(db, "risks", riskId); if (!risk) throw new Error(`Risk not found: ${riskId}`); if (risk.project_id !== integration.project_id) throw new Error("Risk and integration belong to different projects"); const existing = list(db, "external_work_item_mappings", item => item.integration_id === integrationId && item.local_entity_type === "Risk" && item.local_entity_id === riskId)[0]; if (existing) return toMappingView(db, existing); const externalId = payload.externalItemId || payload.external_item_id || buildExternalId(integration.provider, risk.id); const mapping = insert(db, "external_work_item_mappings", { project_id: integration.project_id, integration_id: integration.id, provider: integration.provider, local_entity_type: "Risk", local_entity_id: risk.id, local_title: risk.title, external_item_type: SUPPORTED_PM_PROVIDERS[integration.provider].workItemName, external_item_id: externalId, external_item_key: payload.externalItemKey || payload.external_item_key || externalId, external_url: payload.externalUrl || payload.external_url || buildExternalUrl(integration, externalId), external_status: payload.externalStatus || payload.external_status || riskStatusToExternalStatus(integration.provider, risk), sync_status: "Synced", field_mapping_json: JSON.stringify(buildFieldMapping(integration.provider, risk)), last_synced_at: new Date().toISOString() }, "PMMAP"); audit(db, { project_id: integration.project_id, actor_user_id: actorUserId, entity_type: "ExternalWorkItemMapping", entity_id: mapping.id, action: "create", after_json: mapping }); return toMappingView(db, mapping); }); } getLiveStatus(integrationId) { const db = this.database.read(); const integration = findById(db, "project_management_integrations", integrationId); if (!integration) return null; const oauthStatus = this.oauthService ? this.oauthService.getProviderStatus(integration.provider) : null; return { integration: toIntegrationView(integration), liveModeEnabled: this.env.COSMIC_LIVE_PM_ENABLED === "true", credentials: oauthStatus, requiresLiveModeFlag: "COSMIC_LIVE_PM_ENABLED=true", storedTokenAvailable: oauthStatus ? oauthStatus.storedTokenAvailable : false, configurationHints: this.getProviderConfigurationHints(integration) }; } getProviderConfigurationHints(integration) { if (!integration) return []; if (integration.provider === "Trello") return ["Set TRELLO_API_KEY", "Authorize and store a Trello token or set TRELLO_TOKEN", "Set integration external project key to a Trello list ID or set TRELLO_LIST_ID"]; if (integration.provider === "Jira") return ["Use Jira OAuth 2.0 with JIRA_CLIENT_ID, JIRA_CLIENT_SECRET, and JIRA_CLOUD_ID", "Alternative: JIRA_API_EMAIL, JIRA_API_TOKEN, and JIRA_BASE_URL for API-token basic auth", "Set integration external project key to the Jira project key or set JIRA_PROJECT_KEY"]; if (integration.provider === "Asana") return ["Set ASANA_CLIENT_ID and ASANA_CLIENT_SECRET", "Authorize and store an Asana token or set ASANA_ACCESS_TOKEN", "Set integration external project key to an Asana project GID or set ASANA_PROJECT_GID"]; if (integration.provider === "Microsoft Planner") return ["Set MS_CLIENT_ID, MS_CLIENT_SECRET, and MS_TENANT_ID", "Authorize Microsoft Graph or set MICROSOFT_GRAPH_ACCESS_TOKEN", "Set integration external project key to a Planner plan ID and liveConfig.bucketId or set PLANNER_PLAN_ID and PLANNER_BUCKET_ID"]; return []; } async testLiveIntegration(integrationId) { const db = this.database.read(); const integration = findById(db, "project_management_integrations", integrationId); if (!integration) return null; if (this.env.COSMIC_LIVE_PM_ENABLED !== "true") { return { ok: false, dryRun: true, provider: integration.provider, message: "Live PM API calls are disabled. Set COSMIC_LIVE_PM_ENABLED=true to call third-party APIs.", status: this.getLiveStatus(integrationId) }; } if (!this.oauthService) throw new Error("OAuthService is required for live PM clients"); const client = createLivePmClient(integration.provider, integration, this.oauthService, { env: this.env, fetchImpl: this.fetchImpl }); const result = await client.testConnection(); this.database.transaction(tx => { update(tx, "project_management_integrations", integrationId, { connection_status: "Connected", live_enabled: true }); audit(tx, { project_id: integration.project_id, actor_user_id: "system", entity_type: "ProjectManagementIntegration", entity_id: integrationId, action: "live-test", after_json: { provider: integration.provider, account: result.account } }); }); return result; } getMitigationsForRisk(db, riskId) { return list(db, "mitigation_actions", item => item.risk_id === riskId); } async syncIntegrationLive(integrationId, payload = {}, actorUserId = "system") { const initialDb = this.database.read(); const integration = findById(initialDb, "project_management_integrations", integrationId); if (!integration) return null; if (this.env.COSMIC_LIVE_PM_ENABLED !== "true") { return { dryRun: true, message: "Live PM API calls are disabled. Set COSMIC_LIVE_PM_ENABLED=true to create or update external work items.", fallback: this.syncIntegration(integrationId, Object.assign({}, payload, { summary: `${integration.provider} simulated sync only. Live mode disabled.` }), actorUserId) }; } if (!this.oauthService) throw new Error("OAuthService is required for live PM sync"); const client = createLivePmClient(integration.provider, integration, this.oauthService, { env: this.env, fetchImpl: this.fetchImpl }); const risks = list(initialDb, "risks", risk => risk.project_id === integration.project_id); const startedAt = new Date().toISOString(); let createdCount = 0; let updatedCount = 0; let failedCount = 0; const failures = []; for (const risk of risks) { try { const dbSnapshot = this.database.read(); const existing = list(dbSnapshot, "external_work_item_mappings", item => item.integration_id === integration.id && item.local_entity_type === "Risk" && item.local_entity_id === risk.id)[0]; const mitigations = this.getMitigationsForRisk(dbSnapshot, risk.id); const external = existing ? await client.updateRiskWorkItem(existing, risk, mitigations, integration) : await client.createRiskWorkItem(risk, mitigations, integration); this.database.transaction(db => { if (existing) { update(db, "external_work_item_mappings", existing.id, { local_title: risk.title, external_item_id: external.externalId || existing.external_item_id, external_item_key: external.externalKey || existing.external_item_key, external_url: external.externalUrl || existing.external_url, external_status: external.externalStatus || riskStatusToExternalStatus(integration.provider, risk), sync_status: "Live synced", field_mapping_json: JSON.stringify(buildFieldMapping(integration.provider, risk)), last_synced_at: new Date().toISOString() }); updatedCount += 1; } else { insert(db, "external_work_item_mappings", { project_id: integration.project_id, integration_id: integration.id, provider: integration.provider, local_entity_type: "Risk", local_entity_id: risk.id, local_title: risk.title, external_item_type: SUPPORTED_PM_PROVIDERS[integration.provider].workItemName, external_item_id: external.externalId, external_item_key: external.externalKey || external.externalId, external_url: external.externalUrl || "", external_status: external.externalStatus || riskStatusToExternalStatus(integration.provider, risk), sync_status: "Live synced", field_mapping_json: JSON.stringify(buildFieldMapping(integration.provider, risk)), last_synced_at: new Date().toISOString() }, "PMMAP"); createdCount += 1; } }); } catch (error) { failedCount += 1; failures.push({ riskId: risk.id, message: error.message }); } } const finishedAt = new Date().toISOString(); const status = failedCount ? "Completed with errors" : "Completed"; return this.database.transaction(db => { const syncRun = insert(db, "project_management_sync_runs", { project_id: integration.project_id, integration_id: integration.id, provider: integration.provider, status, started_at: startedAt, finished_at: finishedAt, created_count: createdCount, updated_count: updatedCount, failed_count: failedCount, summary: `${integration.provider} live sync created ${createdCount}, updated ${updatedCount}, failed ${failedCount}.`, failure_json: JSON.stringify(failures) }, "SYNC"); update(db, "project_management_integrations", integration.id, { connection_status: failedCount === risks.length ? "Needs configuration" : "Connected", live_enabled: true, last_sync_at: finishedAt }); audit(db, { project_id: integration.project_id, actor_user_id: actorUserId, entity_type: "ProjectManagementSyncRun", entity_id: syncRun.id, action: "live-sync", after_json: syncRun }); return { integration: toIntegrationView(findById(db, "project_management_integrations", integration.id)), syncRun: toSyncRunView(syncRun), failures, mappings: list(db, "external_work_item_mappings", item => item.integration_id === integration.id).map(item => toMappingView(db, item)) }; }); } syncIntegration(integrationId, payload = {}, actorUserId = "system") { return this.database.transaction(db => { const integration = findById(db, "project_management_integrations", integrationId); if (!integration) return null; const startedAt = new Date().toISOString(); let createdCount = 0; let updatedCount = 0; let failedCount = 0; const risks = list(db, "risks", risk => risk.project_id === integration.project_id); risks.forEach(risk => { try { const existing = list(db, "external_work_item_mappings", item => item.integration_id === integration.id && item.local_entity_type === "Risk" && item.local_entity_id === risk.id)[0]; const externalStatus = riskStatusToExternalStatus(integration.provider, risk); if (existing) { update(db, "external_work_item_mappings", existing.id, { local_title: risk.title, external_status: externalStatus, sync_status: "Synced", field_mapping_json: JSON.stringify(buildFieldMapping(integration.provider, risk)), last_synced_at: startedAt }); updatedCount += 1; } else { const externalId = buildExternalId(integration.provider, risk.id); insert(db, "external_work_item_mappings", { project_id: integration.project_id, integration_id: integration.id, provider: integration.provider, local_entity_type: "Risk", local_entity_id: risk.id, local_title: risk.title, external_item_type: SUPPORTED_PM_PROVIDERS[integration.provider].workItemName, external_item_id: externalId, external_item_key: externalId, external_url: buildExternalUrl(integration, externalId), external_status: externalStatus, sync_status: "Synced", field_mapping_json: JSON.stringify(buildFieldMapping(integration.provider, risk)), last_synced_at: startedAt }, "PMMAP"); createdCount += 1; } } catch (_error) { failedCount += 1; } }); const finishedAt = new Date().toISOString(); const status = failedCount ? "Completed with errors" : "Completed"; const syncRun = insert(db, "project_management_sync_runs", { project_id: integration.project_id, integration_id: integration.id, provider: integration.provider, status, started_at: startedAt, finished_at: finishedAt, created_count: createdCount, updated_count: updatedCount, failed_count: failedCount, summary: payload.summary || `${integration.provider} sync created ${createdCount} mapping(s) and updated ${updatedCount} mapping(s).` }, "SYNC"); update(db, "project_management_integrations", integration.id, { connection_status: "Connected", last_sync_at: finishedAt }); audit(db, { project_id: integration.project_id, actor_user_id: actorUserId, entity_type: "ProjectManagementSyncRun", entity_id: syncRun.id, action: "sync", after_json: syncRun }); return { integration: toIntegrationView(findById(db, "project_management_integrations", integration.id)), syncRun: toSyncRunView(syncRun), mappings: list(db, "external_work_item_mappings", item => item.integration_id === integration.id).map(item => toMappingView(db, item)) }; }); } } module.exports = { IntegrationService, SUPPORTED_PM_PROVIDERS, VALID_PROVIDERS, toProviderOptions };