/** * IPC Handlers * Bridges renderer requests to main process modules * Based on REQUIREMENTS.md:228-268 */ import { ipcMain } from "electron"; import { v4 as uuidv4 } from "uuid"; import { saveDomain, getDomain, deleteDomain, listDomains, listVersions, deleteVersion, saveDownload, saveBackup, } from "./storage"; import { SelfKintoneClient, createKintoneClient } from "./kintone-api"; import type { Result } from "@shared/types/ipc"; import type { CreateDomainParams, UpdateDomainParams, TestDomainConnectionParams, GetAppsParams, GetAppDetailParams, GetFileContentParams, DeployParams, DeployResult, DownloadParams, DownloadResult, GetVersionsParams, RollbackParams, } from "@shared/types/ipc"; import type { Domain, DomainWithStatus, DomainWithPassword } from "@shared/types/domain"; import type { Version, DownloadMetadata, BackupMetadata, } from "@shared/types/version"; import type { AppCustomizeResponse } from "@shared/types/kintone"; // Cache for Kintone clients const clientCache = new Map(); /** * Get or create a Kintone client for a domain */ async function getClient(domainId: string): Promise { if (clientCache.has(domainId)) { return clientCache.get(domainId)!; } const domainWithPassword = await getDomain(domainId); if (!domainWithPassword) { throw new Error(`Domain not found: ${domainId}`); } const client = createKintoneClient(domainWithPassword); clientCache.set(domainId, client); return client; } /** * Helper to wrap IPC handlers with error handling */ function handle(channel: string, handler: () => Promise): void { ipcMain.handle(channel, async (): Promise> => { try { const data = await handler(); return { success: true, data }; } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; return { success: false, error: message }; } }); } /** * Helper to wrap IPC handlers with parameters */ function handleWithParams( channel: string, handler: (params: P) => Promise, ): void { ipcMain.handle(channel, async (_event, params: P): Promise> => { try { const data = await handler(params); return { success: true, data }; } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; return { success: false, error: message }; } }); } // ==================== Domain Management IPC Handlers ==================== /** * Get all domains */ function registerGetDomains(): void { handle("getDomains", async () => { return listDomains(); }); } /** * Create a new domain * Deduplication: Check if domain+username already exists */ function registerCreateDomain(): void { handleWithParams( "createDomain", async (params) => { // Check for duplicate domain+username const existingDomains = await listDomains(); const duplicate = existingDomains.find( (d) => d.domain.toLowerCase() === params.domain.toLowerCase() && d.username.toLowerCase() === params.username.toLowerCase() ); if (duplicate) { throw new Error( `Domain "${params.domain}" with user "${params.username}" already exists. Please edit the existing domain instead.` ); } const now = new Date().toISOString(); const domain: Domain = { id: uuidv4(), name: params.name, domain: params.domain, username: params.username, authType: params.authType, apiToken: params.apiToken, createdAt: now, updatedAt: now, }; await saveDomain(domain, params.password); return domain; }, ); } /** * Update an existing domain */ function registerUpdateDomain(): void { handleWithParams( "updateDomain", async (params) => { const domains = await listDomains(); const existing = domains.find((d) => d.id === params.id); if (!existing) { throw new Error(`Domain not found: ${params.id}`); } const updated: Domain = { ...existing, name: params.name ?? existing.name, domain: params.domain ?? existing.domain, username: params.username ?? existing.username, authType: params.authType ?? existing.authType, apiToken: params.apiToken ?? existing.apiToken, updatedAt: new Date().toISOString(), }; // If password is provided, update it if (params.password) { await saveDomain(updated, params.password); } else { // Get existing password and re-save const existingWithPassword = await getDomain(params.id); if (existingWithPassword) { await saveDomain(updated, existingWithPassword.password); } } // Clear cached client clientCache.delete(params.id); return updated; }, ); } /** * Delete a domain */ function registerDeleteDomain(): void { handleWithParams("deleteDomain", async (id) => { await deleteDomain(id); clientCache.delete(id); }); } /** * Test domain connection */ function registerTestConnection(): void { handleWithParams("testConnection", async (id) => { const domainWithPassword = await getDomain(id); if (!domainWithPassword) { throw new Error(`Domain not found: ${id}`); } const client = createKintoneClient(domainWithPassword); const result = await client.testConnection(); return { ...domainWithPassword, connectionStatus: result.success ? "connected" : "error", connectionError: result.error, }; }); } /** * Test domain connection with temporary credentials */ function registerTestDomainConnection(): void { handleWithParams( "testDomainConnection", async (params) => { const tempDomain: DomainWithPassword = { id: "temp", name: "temp", domain: params.domain, username: params.username, password: params.password || "", authType: params.authType, apiToken: params.apiToken, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; const client = createKintoneClient(tempDomain); const result = await client.testConnection(); if (!result.success) { throw new Error(result.error || "Connection failed"); } return true; }, ); } // ==================== Browse IPC Handlers ==================== /** * Get apps */ function registerGetApps(): void { handleWithParams< GetAppsParams, Awaited> >("getApps", async (params) => { const client = await getClient(params.domainId); return client.getApps({ limit: params.limit, offset: params.offset, }); }); } /** * Get app detail */ function registerGetAppDetail(): void { handleWithParams< GetAppDetailParams, Awaited> >("getAppDetail", async (params) => { const client = await getClient(params.domainId); return client.getAppDetail(params.appId); }); } /** * Get file content */ function registerGetFileContent(): void { handleWithParams< GetFileContentParams, Awaited> >("getFileContent", async (params) => { const client = await getClient(params.domainId); return client.getFileContent(params.fileKey); }); } // ==================== Deploy IPC Handlers ==================== /** * Deploy files to Kintone */ function registerDeploy(): void { handleWithParams("deploy", async (params) => { const client = await getClient(params.domainId); const domainWithPassword = await getDomain(params.domainId); if (!domainWithPassword) { throw new Error(`Domain not found: ${params.domainId}`); } // Get current app config for backup const appDetail = await client.getAppDetail(params.appId); // Create backup of current configuration const backupFiles = new Map(); const backupFileList: BackupMetadata["files"] = []; // Add JS files to backup for (const js of appDetail.customization?.desktop?.js || []) { if (js.file?.fileKey) { const fileContent = await client.getFileContent(js.file.fileKey); const content = Buffer.from(fileContent.content || "", "base64"); backupFiles.set(`pc/${js.file.name || js.file.fileKey}.js`, content); backupFileList.push({ type: "pc", fileType: "js", fileName: js.file.name || js.file.fileKey, fileKey: js.file.fileKey, size: content.length, path: `pc/${js.file.name || js.file.fileKey}.js`, }); } } // Add CSS files to backup for (const css of appDetail.customization?.desktop?.css || []) { if (css.file?.fileKey) { const fileContent = await client.getFileContent(css.file.fileKey); const content = Buffer.from(fileContent.content || "", "base64"); backupFiles.set(`pc/${css.file.name || css.file.fileKey}.css`, content); backupFileList.push({ type: "pc", fileType: "css", fileName: css.file.name || css.file.fileKey, fileKey: css.file.fileKey, size: content.length, path: `pc/${css.file.name || css.file.fileKey}.css`, }); } } // Save backup const backupMetadata: BackupMetadata = { backedUpAt: new Date().toISOString(), domain: domainWithPassword.domain, domainId: params.domainId, appId: params.appId, appName: appDetail.name, files: backupFileList, trigger: "deploy", }; const backupPath = await saveBackup(backupMetadata, backupFiles); // Upload new files const uploadedFiles: Array<{ type: "js" | "css"; position: string; fileKey: string; }> = []; for (const file of params.files) { const fileKey = await client.uploadFile(file.content, file.fileName); uploadedFiles.push({ type: file.fileType, position: file.position, fileKey: fileKey.fileKey, }); } // Build new customization config // Note: This is simplified - real implementation would merge with existing config const newConfig: AppCustomizeResponse = { desktop: { js: uploadedFiles .filter((f) => f.type === "js" && f.position.startsWith("pc_")) .map((f) => ({ type: "FILE" as const, file: { fileKey: f.fileKey }, })), css: uploadedFiles .filter((f) => f.type === "css" && f.position === "pc_css") .map((f) => ({ type: "FILE" as const, file: { fileKey: f.fileKey }, })), }, mobile: { js: uploadedFiles .filter((f) => f.type === "js" && f.position.startsWith("mobile_")) .map((f) => ({ type: "FILE" as const, file: { fileKey: f.fileKey }, })), css: uploadedFiles .filter((f) => f.type === "css" && f.position === "mobile_css") .map((f) => ({ type: "FILE" as const, file: { fileKey: f.fileKey }, })), }, scope: "ALL", revision: "-1", }; // Update app customization // Deploy the changes await client.deployApp(params.appId); return { success: true, backupPath, backupMetadata, }; }); } // ==================== Download IPC Handlers ==================== /** * Download files from Kintone */ function registerDownload(): void { handleWithParams( "download", async (params) => { const client = await getClient(params.domainId); const domainWithPassword = await getDomain(params.domainId); if (!domainWithPassword) { throw new Error(`Domain not found: ${params.domainId}`); } const appDetail = await client.getAppDetail(params.appId); const downloadFiles = new Map(); const downloadFileList: DownloadMetadata["files"] = []; // Download based on requested file types const fileTypes = params.fileTypes || [ "pc_js", "pc_css", "mobile_js", "mobile_css", ]; for (const fileType of fileTypes) { const files = fileType === "pc_js" ? appDetail.customization?.desktop?.js : fileType === "pc_css" ? appDetail.customization?.desktop?.css : fileType === "mobile_js" ? appDetail.customization?.mobile?.js : appDetail.customization?.mobile?.css; for (const file of files || []) { if (file.file?.fileKey) { const content = await client.getFileContent(file.file.fileKey); const buffer = Buffer.from(content.content || "", "base64"); const fileName = file.file.name || file.file.fileKey; const type = fileType.includes("mobile") ? "mobile" : "pc"; const ext = fileType.includes("js") ? "js" : "css"; downloadFiles.set(`${type}/${fileName}`, buffer); downloadFileList.push({ type, fileType: ext, fileName, fileKey: file.file.fileKey, size: buffer.length, path: `${type}/${fileName}`, }); } } } // Save download metadata const metadata: DownloadMetadata = { downloadedAt: new Date().toISOString(), domain: domainWithPassword.domain, domainId: params.domainId, appId: params.appId, appName: appDetail.name, spaceId: appDetail.spaceId || "", spaceName: "", // Would need to fetch from space API files: downloadFileList, source: "kintone", }; const path = await saveDownload(metadata, downloadFiles); return { success: true, path, metadata, }; }, ); } // ==================== Version IPC Handlers ==================== /** * Get versions for an app */ function registerGetVersions(): void { handleWithParams( "getVersions", async (params) => { return listVersions(params.domainId, params.appId); }, ); } /** * Delete a version */ function registerDeleteVersion(): void { handleWithParams("deleteVersion", async (id) => { return deleteVersion(id); }); } /** * Rollback to a previous version */ function registerRollback(): void { handleWithParams("rollback", async (_params) => { // This would read the version file and redeploy // Simplified implementation - would need full implementation throw new Error("Rollback not yet implemented"); }); } // ==================== Register All Handlers ==================== /** * Register all IPC handlers */ export function registerIpcHandlers(): void { // Domain management registerGetDomains(); registerCreateDomain(); registerUpdateDomain(); registerDeleteDomain(); registerTestConnection(); registerTestDomainConnection(); // Browse registerGetApps(); registerGetAppDetail(); registerGetFileContent(); // Deploy registerDeploy(); // Download registerDownload(); // Version registerGetVersions(); registerDeleteVersion(); registerRollback(); console.log("IPC handlers registered"); }