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