// Copyright (C) The Arvados Authors. All rights reserved. // // SPDX-License-Identifier: AGPL-3.0 import { camelCase, isPlainObject, isArray, snakeCase } from "lodash"; import { AxiosInstance, AxiosPromise, AxiosRequestConfig } from "axios"; import uuid from "uuid/v4"; import { ApiActions } from "services/api/api-actions"; import QueryString from "query-string"; import { Session } from "models/session"; interface Errors { status: number; errors: string[]; errorToken: string; } export interface ListArguments { limit?: number; offset?: number; filters?: string; order?: string; select?: string[]; distinct?: boolean; count?: string; includeOldVersions?: boolean; } export interface ListResults { clusterId?: string; kind: string; offset: number; limit: number; items: T[]; itemsAvailable: number; } export class CommonService { protected serverApi: AxiosInstance; protected resourceType: string; protected actions: ApiActions; protected readOnlyFields: string[]; constructor(serverApi: AxiosInstance, resourceType: string, actions: ApiActions, readOnlyFields: string[] = []) { this.serverApi = serverApi; this.resourceType = resourceType; this.actions = actions; this.readOnlyFields = readOnlyFields; } static mapResponseKeys = (response: { data: any }) => CommonService.mapKeys(camelCase)(response.data) static mapKeys = (mapFn: (key: string) => string) => (value: any): any => { switch (true) { case isPlainObject(value): return Object .keys(value) .map(key => [key, mapFn(key)]) .reduce((newValue, [key, newKey]) => ({ ...newValue, [newKey]: (key === 'items') ? CommonService.mapKeys(mapFn)(value[key]) : value[key] }), {}); case isArray(value): return value.map(CommonService.mapKeys(mapFn)); default: return value; } } protected validateUuid(uuid: string) { if (uuid === "") { throw new Error('UUID cannot be empty string'); } } static defaultResponse(promise: AxiosPromise, actions: ApiActions, mapKeys = true, showErrors = true): Promise { const reqId = uuid(); actions.progressFn(reqId, true); return promise .then(data => { actions.progressFn(reqId, false); return data; }) .then((response: { data: any }) => { return mapKeys ? CommonService.mapResponseKeys(response) : response.data; }) .catch(({ response }) => { if (response) { actions.progressFn(reqId, false); const errors = CommonService.mapResponseKeys(response) as Errors; errors.status = response.status; actions.errorFn(reqId, errors, showErrors); throw errors; } }); } create(data?: Partial, showErrors?: boolean) { return CommonService.defaultResponse( this.serverApi .post(`/${this.resourceType}`, data && CommonService.mapKeys(snakeCase)(data)), this.actions, true, // mapKeys showErrors ); } delete(uuid: string, showErrors?: boolean): Promise { this.validateUuid(uuid); return CommonService.defaultResponse( this.serverApi .delete(`/${this.resourceType}/${uuid}`), this.actions, true, // mapKeys showErrors ); } get(uuid: string, showErrors?: boolean, select?: string[], session?: Session) { this.validateUuid(uuid); const cfg: AxiosRequestConfig = { params: { select: select ? `[${select.map(snakeCase).map(s => `"${s}"`).join(',')}]` : undefined } }; if (session) { cfg.baseURL = session.baseUrl; cfg.headers = { 'Authorization': 'Bearer ' + session.token }; } return CommonService.defaultResponse( this.serverApi .get(`/${this.resourceType}/${uuid}`, cfg), this.actions, true, // mapKeys showErrors ); } list(args: ListArguments = {}, showErrors?: boolean): Promise> { const { filters, select, ...other } = args; const params = { ...CommonService.mapKeys(snakeCase)(other), filters: filters ? `[${filters}]` : undefined, select: select ? `[${select.map(snakeCase).map(s => `"${s}"`).join(', ')}]` : undefined }; if (QueryString.stringify(params).length <= 1500) { return CommonService.defaultResponse( this.serverApi.get(`/${this.resourceType}`, { params }), this.actions, true, showErrors ); } else { // Using the POST special case to avoid URI length 414 errors. // We must use urlencoded post body since api doesn't support form data // const formData = new FormData(); const formData = new URLSearchParams(); formData.append("_method", "GET"); Object.keys(params).forEach(key => { if (params[key] !== undefined) { formData.append(key, params[key]); } }); return CommonService.defaultResponse( this.serverApi.post(`/${this.resourceType}`, formData, {}), this.actions, true, showErrors ); } } update(uuid: string, data: Partial, showErrors?: boolean) { this.validateUuid(uuid); return CommonService.defaultResponse( this.serverApi .put(`/${this.resourceType}/${uuid}`, data && CommonService.mapKeys(snakeCase)(data)), this.actions, undefined, // mapKeys showErrors ); } }