e00a3d7d43b6ece7be78e55d1e67a0cd5dfe7fda
[arvados-workbench2.git] / src / services / common-service / common-service.ts
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import * as _ from "lodash";
6 import { AxiosInstance, AxiosPromise } from "axios";
7 import * as uuid from "uuid/v4";
8 import { ApiActions } from "~/services/api/api-actions";
9 import * as QueryString from "query-string";
10
11 interface Errors {
12     errors: string[];
13     errorToken: string;
14 }
15
16 export interface ListArguments {
17     limit?: number;
18     offset?: number;
19     filters?: string;
20     order?: string;
21     select?: string[];
22     distinct?: boolean;
23     count?: string;
24 }
25
26 export interface ListResults<T> {
27     clusterId?: string;
28     kind: string;
29     offset: number;
30     limit: number;
31     items: T[];
32     itemsAvailable: number;
33 }
34
35 export class CommonService<T> {
36     protected serverApi: AxiosInstance;
37     protected resourceType: string;
38     protected actions: ApiActions;
39     protected readOnlyFields: string[];
40
41     constructor(serverApi: AxiosInstance, resourceType: string, actions: ApiActions, readOnlyFields: string[] = []) {
42         this.serverApi = serverApi;
43         this.resourceType = '/' + resourceType;
44         this.actions = actions;
45         this.readOnlyFields = readOnlyFields;
46     }
47
48     static mapResponseKeys = (response: { data: any }) =>
49         CommonService.mapKeys(_.camelCase)(response.data)
50
51     static mapKeys = (mapFn: (key: string) => string) =>
52         (value: any): any => {
53             switch (true) {
54                 case _.isPlainObject(value):
55                     return Object
56                         .keys(value)
57                         .map(key => [key, mapFn(key)])
58                         .reduce((newValue, [key, newKey]) => ({
59                             ...newValue,
60                             [newKey]: (key === 'items') ? CommonService.mapKeys(mapFn)(value[key]) : value[key]
61                         }), {});
62                 case _.isArray(value):
63                     return value.map(CommonService.mapKeys(mapFn));
64                 default:
65                     return value;
66             }
67         }
68
69     static defaultResponse<R>(promise: AxiosPromise<R>, actions: ApiActions, mapKeys = true, showErrors = true): Promise<R> {
70         const reqId = uuid();
71         actions.progressFn(reqId, true);
72         return promise
73             .then(data => {
74                 actions.progressFn(reqId, false);
75                 return data;
76             })
77             .then((response: { data: any }) => {
78                 return mapKeys ? CommonService.mapResponseKeys(response) : response.data;
79             })
80             .catch(({ response }) => {
81                 actions.progressFn(reqId, false);
82                 const errors = CommonService.mapResponseKeys(response) as Errors;
83                 actions.errorFn(reqId, errors, showErrors);
84                 throw errors;
85             });
86     }
87
88     create(data?: Partial<T>) {
89         return CommonService.defaultResponse(
90             this.serverApi
91                 .post<T>(this.resourceType, data && CommonService.mapKeys(_.snakeCase)(data)),
92             this.actions
93         );
94     }
95
96     delete(uuid: string): Promise<T> {
97         return CommonService.defaultResponse(
98             this.serverApi
99                 .delete(this.resourceType + '/' + uuid),
100             this.actions
101         );
102     }
103
104     get(uuid: string, showErrors?: boolean) {
105         return CommonService.defaultResponse(
106             this.serverApi
107                 .get<T>(this.resourceType + '/' + uuid),
108             this.actions,
109             true, // mapKeys
110             showErrors
111         );
112     }
113
114     list(args: ListArguments = {}): Promise<ListResults<T>> {
115         const { filters, order, ...other } = args;
116         const params = {
117             ...other,
118             filters: filters ? `[${filters}]` : undefined,
119             order: order ? order : undefined
120         };
121
122         if (QueryString.stringify(params).length <= 1500) {
123             return CommonService.defaultResponse(
124                 this.serverApi.get(this.resourceType, { params }),
125                 this.actions
126             );
127         } else {
128             // Using the POST special case to avoid URI length 414 errors.
129             const formData = new FormData();
130             formData.append("_method", "GET");
131             Object.keys(params).map(key => {
132                 if (params[key] !== undefined) {
133                     formData.append(key, params[key]);
134                 }
135             });
136             return CommonService.defaultResponse(
137                 this.serverApi.post(this.resourceType, formData, {
138                     params: {
139                         _method: 'GET'
140                     }
141                 }),
142                 this.actions
143             );
144         }
145     }
146
147     update(uuid: string, data: Partial<T>) {
148         return CommonService.defaultResponse(
149             this.serverApi
150                 .put<T>(this.resourceType + '/' + uuid, data && CommonService.mapKeys(_.snakeCase)(data)),
151             this.actions
152         );
153     }
154 }