217 lines
5.9 KiB
TypeScript
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);
|
|
}
|