KintoneHttpClient.js•7.06 kB
// src/repositories/base/http/KintoneHttpClient.js
import { KintoneApiError } from './KintoneApiError.js';
const DEFAULT_TIMEOUT_MS = 60000;
function buildPath({ endpointName, guestSpaceId, preview = false }) {
const guestPath = guestSpaceId !== undefined && guestSpaceId !== null
? `/guest/${guestSpaceId}`
: '';
const previewPath = preview ? '/preview' : '';
return `/k${guestPath}/v1${previewPath}/${endpointName}.json`;
}
const FORM_DATA_SUPPORTED = typeof FormData !== 'undefined';
function isFormData(value) {
return FORM_DATA_SUPPORTED && value instanceof FormData;
}
function stripUndefined(value) {
if (value === null || value === undefined) {
return undefined;
}
if (Array.isArray(value)) {
return value.map(stripUndefined);
}
if (typeof value === 'object' && !isFormData(value) && !(value instanceof Date) && !(value instanceof Buffer)) {
const result = {};
for (const [key, entry] of Object.entries(value)) {
if (entry !== undefined) {
const cleaned = stripUndefined(entry);
if (cleaned !== undefined) {
result[key] = cleaned;
}
}
}
return result;
}
return value;
}
export class KintoneHttpClient {
constructor(credentials) {
this.credentials = credentials;
this.baseUrl = `https://${credentials.domain}`;
this.authHeader = Buffer.from(`${credentials.username}:${credentials.password}`).toString('base64');
}
getDefaultHeaders() {
return {
'X-Cybozu-Authorization': this.authHeader,
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'application/json'
};
}
async request(method, endpointName, { params, data, preview = false, headers = {}, responseType, rawResponse = false } = {}) {
const path = buildPath({
endpointName,
guestSpaceId: this.credentials.guestSpaceId,
preview
});
const url = new URL(path, this.baseUrl);
const cleanedParams = params ? stripUndefined(params) : undefined;
if (cleanedParams) {
for (const [key, value] of Object.entries(cleanedParams)) {
if (value === undefined || value === null) {
continue;
}
if (Array.isArray(value)) {
for (const item of value) {
if (item !== undefined && item !== null) {
url.searchParams.append(key, String(item));
}
}
} else {
url.searchParams.append(key, String(value));
}
}
}
const requestHeaders = new Headers(this.getDefaultHeaders());
for (const [key, value] of Object.entries(headers)) {
if (value !== undefined && value !== null) {
requestHeaders.set(key, String(value));
}
}
const init = {
method: method.toUpperCase(),
headers: requestHeaders,
signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS)
};
const expectedResponseType = responseType || 'json';
if (data !== undefined) {
if (isFormData(data)) {
// fetchがboundary付きヘッダーを自動付与するため、Content-Typeは削除
requestHeaders.delete('Content-Type');
init.body = data;
} else {
const payload = stripUndefined(data);
init.body = JSON.stringify(payload);
if (!requestHeaders.has('Content-Type')) {
requestHeaders.set('Content-Type', 'application/json');
}
}
}
let response;
try {
response = await fetch(url, init);
} catch (error) {
if (error instanceof KintoneApiError) {
throw error;
}
const isAbortError = error?.name === 'AbortError';
throw new KintoneApiError(
isAbortError ? 'kintone API request timed out' : 'kintone API request failed without response',
{
status: null,
responseBody: null
}
);
}
const parseErrorBody = async () => {
const contentType = response.headers.get('content-type') || '';
if (contentType.includes('application/json')) {
try {
return await response.json();
} catch (parseError) {
return null;
}
}
try {
return await response.text();
} catch (parseError) {
return null;
}
};
if (!response.ok) {
const body = await parseErrorBody();
const message = typeof body === 'object' && body?.message
? body.message
: `kintone API request failed with status ${response.status}`;
throw new KintoneApiError(message, {
status: response.status,
code: typeof body === 'object' ? body?.code : undefined,
errors: typeof body === 'object' ? body?.errors : undefined,
responseBody: body
});
}
if (rawResponse) {
const dataBuffer = expectedResponseType === 'arraybuffer'
? Buffer.from(await response.arrayBuffer())
: await response.text();
return {
data: dataBuffer,
headers: Object.fromEntries(response.headers.entries()),
status: response.status
};
}
if (expectedResponseType === 'arraybuffer') {
return Buffer.from(await response.arrayBuffer());
}
if (expectedResponseType === 'text') {
return await response.text();
}
if (response.status === 204) {
return null;
}
const text = await response.text();
if (!text) {
return {};
}
try {
return JSON.parse(text);
} catch (error) {
throw new KintoneApiError('kintone API returned invalid JSON response', {
status: response.status,
responseBody: text
});
}
}
get(endpointName, params, options = {}) {
return this.request('get', endpointName, { ...options, params });
}
post(endpointName, data, options = {}) {
return this.request('post', endpointName, { ...options, data });
}
put(endpointName, data, options = {}) {
return this.request('put', endpointName, { ...options, data });
}
delete(endpointName, data, options = {}) {
return this.request('delete', endpointName, { ...options, data });
}
}