/home/techb158/cosmic.abdallabala.com/src/services
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
};