Files
kintone-customize-manager/src/main/kintone-api.ts
2026-03-15 12:46:35 +08:00

217 lines
5.9 KiB
TypeScript

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<T>(operation: () => Promise<T>): Promise<T> {
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<AppResponse[]> {
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<AppDetail> {
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<FileContent> {
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<AppCustomizeParameter, "app">,
): Promise<void> {
return this.withErrorHandling(async () => {
await this.client.app.updateAppCustomize({ ...config, app: appId });
});
}
async deployApp(appId: string): Promise<void> {
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);
}