import { KintoneRestAPIClient } from "@kintone/rest-api-client"; import type { KintoneRestAPIError } from "@kintone/rest-api-client"; import type { DomainWithPassword } from "@shared/types/domain"; import { type AppResponse, type AppDetail, type FileContent, type KintoneApiError, AppCustomizeParameter, } from "@shared/types/kintone"; import { getErrorMessage } from "./errors"; /** * Custom error class for Kintone API errors */ export class KintoneError extends Error { public readonly code?: string; public readonly id?: string; public readonly statusCode?: number; constructor( message: string, apiError?: KintoneApiError, statusCode?: number, ) { super(message); this.name = "KintoneError"; this.code = apiError?.code; this.id = apiError?.id; this.statusCode = statusCode; } } /** * Kintone REST API Client */ export class KintoneClient { private client: KintoneRestAPIClient; private domain: string; constructor(domainConfig: DomainWithPassword) { this.domain = domainConfig.domain; this.client = new KintoneRestAPIClient({ baseUrl: `https://${domainConfig.domain}`, auth: { username: domainConfig.username, password: domainConfig.password, }, }); } private convertError(error: unknown): KintoneError { if (error && typeof error === "object" && "code" in error) { const apiError = error as KintoneRestAPIError; return new KintoneError( apiError.message, { code: apiError.code, message: apiError.message, id: apiError.id }, apiError.status, ); } if (error instanceof Error) { return new KintoneError(error.message); } return new KintoneError(getErrorMessage("unknownError")); } private async withErrorHandling(operation: () => Promise): Promise { try { return await operation(); } catch (error) { throw this.convertError(error); } } // ==================== App APIs ==================== /** * Get all apps with pagination support * Fetches all apps by making multiple requests if needed */ async getApps(options?: { limit?: number; offset?: number; }): Promise { return this.withErrorHandling(async () => { // If pagination options provided, use them directly if (options?.limit !== undefined || options?.offset !== undefined) { const params: { limit?: number; offset?: number } = {}; if (options.limit) params.limit = options.limit; if (options.offset) params.offset = options.offset; const response = await this.client.app.getApps(params); return response.apps; } // Otherwise, fetch all apps (pagination handled internally) const allApps: AppResponse[] = []; const limit = 100; // Max allowed by Kintone API let offset = 0; let hasMore = true; while (hasMore) { const response = await this.client.app.getApps({ limit, offset }); allApps.push(...response.apps); // If we got fewer than limit, we've reached the end if (response.apps.length < limit) { hasMore = false; } else { offset += limit; } } return allApps; }); } async getAppDetail(appId: string): Promise { return this.withErrorHandling(async () => { const [appInfo, customizeInfo] = await Promise.all([ this.client.app.getApp({ id: appId }), this.client.app.getAppCustomize({ app: appId }), ]); return { ...appInfo, customization: customizeInfo, }; }); } // ==================== File APIs ==================== async getFileContent(fileKey: string): Promise { return this.withErrorHandling(async () => { const data = await this.client.file.downloadFile({ fileKey }); const buffer = Buffer.from(data); const content = buffer.toString("base64"); return { fileKey, name: fileKey, size: buffer.byteLength, mimeType: "application/octet-stream", content, }; }); } async uploadFile( content: string | Buffer, fileName: string, _mimeType?: string, ): Promise<{ fileKey: string }> { return this.withErrorHandling(async () => { const response = await this.client.file.uploadFile({ file: { name: fileName, data: content }, }); return { fileKey: response.fileKey }; }); } // ==================== Deploy APIs ==================== async updateAppCustomize( appId: string, config: Omit, ): Promise { return this.withErrorHandling(async () => { await this.client.app.updateAppCustomize({ ...config, app: appId }); }); } async deployApp(appId: string): Promise { return this.withErrorHandling(async () => { await this.client.app.deployApp({ apps: [{ app: appId }] }); }); } async getDeployStatus( appId: string, ): Promise<"PROCESSING" | "SUCCESS" | "FAIL" | "CANCEL"> { return this.withErrorHandling(async () => { const response = await this.client.app.getDeployStatus({ apps: [appId] }); return response.apps[0]?.status || "FAIL"; }); } // ==================== Utility Methods ==================== async testConnection(): Promise<{ success: boolean; error?: string }> { try { // Use limit=1 to minimize data transfer for faster connection testing await this.client.app.getApps({ limit: 1 }); return { success: true }; } catch (error) { return { success: false, error: error instanceof KintoneError ? error.message : getErrorMessage("connectionFailed"), }; } } getDomain(): string { return this.domain; } } export function createKintoneClient(domain: DomainWithPassword): KintoneClient { return new KintoneClient(domain); }