feat(core): implement Kintone Customize Manager core features
Wave 1 - Foundation: - Add shared TypeScript type definitions (domain, kintone, version, ipc) - Implement storage module with safeStorage encryption - Implement Kintone REST API client with authentication - Add IPC handlers for all features - Expose APIs via preload contextBridge - Setup Zustand stores for state management Wave 2 - Domain Management: - Implement Domain store with IPC actions - Create DomainManager, DomainList, DomainForm components - Connect UI components to store Wave 3 - Resource Browsing: - Create SpaceTree component for browsing spaces/apps - Create AppDetail component for app configuration view - Create CodeViewer component with syntax highlighting Wave 4 - Deployment: - Create FileUploader drag-and-drop component - Create DeployDialog with step-by-step deployment flow - Implement deployment IPC handler with auto-backup Wave 5 - Version Management: - Create VersionHistory component - Implement version storage and rollback logic Wave 6 - Integration: - Integrate all components into main App layout - Update main process entry point - Configure TypeScript paths for renderer imports
This commit is contained in:
543
src/main/ipc-handlers.ts
Normal file
543
src/main/ipc-handlers.ts
Normal file
@@ -0,0 +1,543 @@
|
||||
/**
|
||||
* 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,
|
||||
saveVersion,
|
||||
listVersions,
|
||||
deleteVersion,
|
||||
saveDownload,
|
||||
saveBackup,
|
||||
isSecureStorageAvailable,
|
||||
getStorageBackend,
|
||||
} from "./storage";
|
||||
import { KintoneClient, KintoneError } from "./kintone-api";
|
||||
import type { Result } from "@renderer/types/ipc";
|
||||
import type {
|
||||
CreateDomainParams,
|
||||
UpdateDomainParams,
|
||||
GetSpacesParams,
|
||||
GetAppsParams,
|
||||
GetAppDetailParams,
|
||||
GetFileContentParams,
|
||||
DeployParams,
|
||||
DeployResult,
|
||||
DownloadParams,
|
||||
DownloadResult,
|
||||
GetVersionsParams,
|
||||
RollbackParams,
|
||||
} from "@renderer/types/ipc";
|
||||
import type { Domain, DomainWithStatus } from "@renderer/types/domain";
|
||||
import type {
|
||||
Version,
|
||||
DownloadMetadata,
|
||||
BackupMetadata,
|
||||
} from "@renderer/types/version";
|
||||
|
||||
// Cache for Kintone clients
|
||||
const clientCache = new Map<string, KintoneClient>();
|
||||
|
||||
/**
|
||||
* Get or create a Kintone client for a domain
|
||||
*/
|
||||
async function getClient(domainId: string): Promise<KintoneClient> {
|
||||
if (clientCache.has(domainId)) {
|
||||
return clientCache.get(domainId)!;
|
||||
}
|
||||
|
||||
const domainWithPassword = await getDomain(domainId);
|
||||
if (!domainWithPassword) {
|
||||
throw new Error(`Domain not found: ${domainId}`);
|
||||
}
|
||||
|
||||
const client = new KintoneClient(domainWithPassword);
|
||||
clientCache.set(domainId, client);
|
||||
return client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to wrap IPC handlers with error handling
|
||||
*/
|
||||
function handle<T>(channel: string, handler: () => Promise<T>): void {
|
||||
ipcMain.handle(channel, async (): Promise<Result<T>> => {
|
||||
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<P, T>(
|
||||
channel: string,
|
||||
handler: (params: P) => Promise<T>,
|
||||
): void {
|
||||
ipcMain.handle(channel, async (_event, params: P): Promise<Result<T>> => {
|
||||
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
|
||||
*/
|
||||
function registerCreateDomain(): void {
|
||||
handleWithParams<CreateDomainParams, Domain>(
|
||||
"createDomain",
|
||||
async (params) => {
|
||||
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<UpdateDomainParams, Domain>(
|
||||
"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<string, void>("deleteDomain", async (id) => {
|
||||
await deleteDomain(id);
|
||||
clientCache.delete(id);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Test domain connection
|
||||
*/
|
||||
function registerTestConnection(): void {
|
||||
handleWithParams<string, DomainWithStatus>("testConnection", async (id) => {
|
||||
const domainWithPassword = await getDomain(id);
|
||||
if (!domainWithPassword) {
|
||||
throw new Error(`Domain not found: ${id}`);
|
||||
}
|
||||
|
||||
const client = new KintoneClient(domainWithPassword);
|
||||
const result = await client.testConnection();
|
||||
|
||||
return {
|
||||
...domainWithPassword,
|
||||
connectionStatus: result.success ? "connected" : "error",
|
||||
connectionError: result.error,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== Browse IPC Handlers ====================
|
||||
|
||||
/**
|
||||
* Get spaces
|
||||
*/
|
||||
function registerGetSpaces(): void {
|
||||
handleWithParams<
|
||||
GetSpacesParams,
|
||||
Awaited<ReturnType<KintoneClient["getSpaces"]>>
|
||||
>("getSpaces", async (params) => {
|
||||
const client = await getClient(params.domainId);
|
||||
return client.getSpaces();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get apps
|
||||
*/
|
||||
function registerGetApps(): void {
|
||||
handleWithParams<
|
||||
GetAppsParams,
|
||||
Awaited<ReturnType<KintoneClient["getApps"]>>
|
||||
>("getApps", async (params) => {
|
||||
const client = await getClient(params.domainId);
|
||||
return client.getApps(params.spaceId);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get app detail
|
||||
*/
|
||||
function registerGetAppDetail(): void {
|
||||
handleWithParams<
|
||||
GetAppDetailParams,
|
||||
Awaited<ReturnType<KintoneClient["getAppDetail"]>>
|
||||
>("getAppDetail", async (params) => {
|
||||
const client = await getClient(params.domainId);
|
||||
return client.getAppDetail(params.appId);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file content
|
||||
*/
|
||||
function registerGetFileContent(): void {
|
||||
handleWithParams<
|
||||
GetFileContentParams,
|
||||
Awaited<ReturnType<KintoneClient["getFileContent"]>>
|
||||
>("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<DeployParams, DeployResult>("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<string, Buffer>();
|
||||
const backupFileList: BackupMetadata["files"] = [];
|
||||
|
||||
// Add JS files to backup
|
||||
for (const js of appDetail.customization?.javascript?.pc || []) {
|
||||
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?.stylesheet?.pc || []) {
|
||||
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 = {
|
||||
javascript: {
|
||||
pc: uploadedFiles
|
||||
.filter((f) => f.type === "js" && f.position.startsWith("pc_"))
|
||||
.map((f) => ({
|
||||
type: "FILE" as const,
|
||||
file: { fileKey: f.fileKey },
|
||||
})),
|
||||
mobile: uploadedFiles
|
||||
.filter((f) => f.type === "js" && f.position.startsWith("mobile_"))
|
||||
.map((f) => ({
|
||||
type: "FILE" as const,
|
||||
file: { fileKey: f.fileKey },
|
||||
})),
|
||||
},
|
||||
stylesheet: {
|
||||
pc: uploadedFiles
|
||||
.filter((f) => f.type === "css" && f.position === "pc_css")
|
||||
.map((f) => ({
|
||||
type: "FILE" as const,
|
||||
file: { fileKey: f.fileKey },
|
||||
})),
|
||||
mobile: uploadedFiles
|
||||
.filter((f) => f.type === "css" && f.position === "mobile_css")
|
||||
.map((f) => ({
|
||||
type: "FILE" as const,
|
||||
file: { fileKey: f.fileKey },
|
||||
})),
|
||||
},
|
||||
};
|
||||
|
||||
// Update app customization
|
||||
await client.updateAppCustomize(params.appId, newConfig);
|
||||
|
||||
// Deploy the changes
|
||||
await client.deployApp(params.appId);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
backupPath,
|
||||
backupMetadata,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== Download IPC Handlers ====================
|
||||
|
||||
/**
|
||||
* Download files from Kintone
|
||||
*/
|
||||
function registerDownload(): void {
|
||||
handleWithParams<DownloadParams, DownloadResult>(
|
||||
"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<string, Buffer>();
|
||||
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?.javascript?.pc
|
||||
: fileType === "pc_css"
|
||||
? appDetail.customization?.stylesheet?.pc
|
||||
: fileType === "mobile_js"
|
||||
? appDetail.customization?.javascript?.mobile
|
||||
: appDetail.customization?.stylesheet?.mobile;
|
||||
|
||||
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<GetVersionsParams, Version[]>(
|
||||
"getVersions",
|
||||
async (params) => {
|
||||
return listVersions(params.domainId, params.appId);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a version
|
||||
*/
|
||||
function registerDeleteVersion(): void {
|
||||
handleWithParams<string, void>("deleteVersion", async (id) => {
|
||||
return deleteVersion(id);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Rollback to a previous version
|
||||
*/
|
||||
function registerRollback(): void {
|
||||
handleWithParams<RollbackParams, DeployResult>("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();
|
||||
|
||||
// Browse
|
||||
registerGetSpaces();
|
||||
registerGetApps();
|
||||
registerGetAppDetail();
|
||||
registerGetFileContent();
|
||||
|
||||
// Deploy
|
||||
registerDeploy();
|
||||
|
||||
// Download
|
||||
registerDownload();
|
||||
|
||||
// Version
|
||||
registerGetVersions();
|
||||
registerDeleteVersion();
|
||||
registerRollback();
|
||||
|
||||
console.log("IPC handlers registered");
|
||||
}
|
||||
Reference in New Issue
Block a user