1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
5 import { camelCase, isPlainObject, isArray, snakeCase } from "lodash";
6 import { AxiosInstance, AxiosPromise, AxiosRequestConfig } from "axios";
7 import uuid from "uuid/v4";
8 import { ApiActions } from "services/api/api-actions";
9 import QueryString from "query-string";
10 import { Session } from "models/session";
18 export interface ListArguments {
26 includeOldVersions?: boolean;
29 export interface ListResults<T> {
35 itemsAvailable: number;
38 export class CommonService<T> {
39 protected serverApi: AxiosInstance;
40 protected resourceType: string;
41 protected actions: ApiActions;
42 protected readOnlyFields: string[];
44 constructor(serverApi: AxiosInstance, resourceType: string, actions: ApiActions, readOnlyFields: string[] = []) {
45 this.serverApi = serverApi;
46 this.resourceType = resourceType;
47 this.actions = actions;
48 this.readOnlyFields = readOnlyFields;
51 static mapResponseKeys = (response: { data: any }) =>
52 CommonService.mapKeys(camelCase)(response.data)
54 static mapKeys = (mapFn: (key: string) => string) =>
55 (value: any): any => {
57 case isPlainObject(value):
60 .map(key => [key, mapFn(key)])
61 .reduce((newValue, [key, newKey]) => ({
63 [newKey]: (key === 'items') ? CommonService.mapKeys(mapFn)(value[key]) : value[key]
66 return value.map(CommonService.mapKeys(mapFn));
72 protected validateUuid(uuid: string) {
74 throw new Error('UUID cannot be empty string');
78 static defaultResponse<R>(promise: AxiosPromise<R>, actions: ApiActions, mapKeys = true, showErrors = true): Promise<R> {
80 actions.progressFn(reqId, true);
83 actions.progressFn(reqId, false);
86 .then((response: { data: any }) => {
87 return mapKeys ? CommonService.mapResponseKeys(response) : response.data;
89 .catch(({ response }) => {
91 actions.progressFn(reqId, false);
92 const errors = CommonService.mapResponseKeys(response) as Errors;
93 errors.status = response.status;
94 actions.errorFn(reqId, errors, showErrors);
100 create(data?: Partial<T>, showErrors?: boolean) {
101 return CommonService.defaultResponse(
103 .post<T>(`/${this.resourceType}`, data && CommonService.mapKeys(snakeCase)(data)),
110 delete(uuid: string, showErrors?: boolean): Promise<T> {
111 this.validateUuid(uuid);
112 return CommonService.defaultResponse(
114 .delete(`/${this.resourceType}/${uuid}`),
121 get(uuid: string, showErrors?: boolean, select?: string[], session?: Session) {
122 this.validateUuid(uuid);
124 const cfg: AxiosRequestConfig = {
127 ? `[${select.map(snakeCase).map(s => `"${s}"`).join(',')}]`
132 cfg.baseURL = session.baseUrl;
133 cfg.headers = { 'Authorization': 'Bearer ' + session.token };
136 return CommonService.defaultResponse(
138 .get<T>(`/${this.resourceType}/${uuid}`, cfg),
145 list(args: ListArguments = {}, showErrors?: boolean): Promise<ListResults<T>> {
146 const { filters, select, ...other } = args;
148 ...CommonService.mapKeys(snakeCase)(other),
149 filters: filters ? `[${filters}]` : undefined,
151 ? `[${select.map(snakeCase).map(s => `"${s}"`).join(', ')}]`
155 if (QueryString.stringify(params).length <= 1500) {
156 return CommonService.defaultResponse(
157 this.serverApi.get(`/${this.resourceType}`, { params }),
163 // Using the POST special case to avoid URI length 414 errors.
164 // We must use urlencoded post body since api doesn't support form data
165 // const formData = new FormData();
166 const formData = new URLSearchParams();
167 formData.append("_method", "GET");
168 Object.keys(params).forEach(key => {
169 if (params[key] !== undefined) {
170 formData.append(key, params[key]);
173 return CommonService.defaultResponse(
174 this.serverApi.post(`/${this.resourceType}`, formData, {}),
182 update(uuid: string, data: Partial<T>, showErrors?: boolean) {
183 this.validateUuid(uuid);
184 return CommonService.defaultResponse(
186 .put<T>(`/${this.resourceType}/${uuid}`, data && CommonService.mapKeys(snakeCase)(data)),
188 undefined, // mapKeys