18484: Improves CollectionService.get() to support "select".
[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 { camelCase, isPlainObject, isArray, snakeCase } from "lodash";
6 import { AxiosInstance, AxiosPromise } from "axios";
7 import uuid from "uuid/v4";
8 import { ApiActions } from "services/api/api-actions";
9 import 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     includeOldVersions?: boolean;
26 }
27
28 export interface ListResults<T> {
29     clusterId?: string;
30     kind: string;
31     offset: number;
32     limit: number;
33     items: T[];
34     itemsAvailable: number;
35 }
36
37 export class CommonService<T> {
38     protected serverApi: AxiosInstance;
39     protected resourceType: string;
40     protected actions: ApiActions;
41     protected readOnlyFields: string[];
42
43     constructor(serverApi: AxiosInstance, resourceType: string, actions: ApiActions, readOnlyFields: string[] = []) {
44         this.serverApi = serverApi;
45         this.resourceType = resourceType;
46         this.actions = actions;
47         this.readOnlyFields = readOnlyFields;
48     }
49
50     static mapResponseKeys = (response: { data: any }) =>
51         CommonService.mapKeys(camelCase)(response.data)
52
53     static mapKeys = (mapFn: (key: string) => string) =>
54         (value: any): any => {
55             switch (true) {
56                 case isPlainObject(value):
57                     return Object
58                         .keys(value)
59                         .map(key => [key, mapFn(key)])
60                         .reduce((newValue, [key, newKey]) => ({
61                             ...newValue,
62                             [newKey]: (key === 'items') ? CommonService.mapKeys(mapFn)(value[key]) : value[key]
63                         }), {});
64                 case isArray(value):
65                     return value.map(CommonService.mapKeys(mapFn));
66                 default:
67                     return value;
68             }
69         }
70
71     protected validateUuid(uuid: string) {
72         if (uuid === "") {
73             throw new Error('UUID cannot be empty string');
74         }
75     }
76
77     static defaultResponse<R>(promise: AxiosPromise<R>, actions: ApiActions, mapKeys = true, showErrors = true): Promise<R> {
78         const reqId = uuid();
79         actions.progressFn(reqId, true);
80         return promise
81             .then(data => {
82                 actions.progressFn(reqId, false);
83                 return data;
84             })
85             .then((response: { data: any }) => {
86                 return mapKeys ? CommonService.mapResponseKeys(response) : response.data;
87             })
88             .catch(({ response }) => {
89                 actions.progressFn(reqId, false);
90                 const errors = CommonService.mapResponseKeys(response) as Errors;
91                 errors.status = response.status;
92                 actions.errorFn(reqId, errors, showErrors);
93                 throw errors;
94             });
95     }
96
97     create(data?: Partial<T>, showErrors?: boolean) {
98         return CommonService.defaultResponse(
99             this.serverApi
100                 .post<T>(`/${this.resourceType}`, data && CommonService.mapKeys(snakeCase)(data)),
101             this.actions,
102             true, // mapKeys
103             showErrors
104         );
105     }
106
107     delete(uuid: string): Promise<T> {
108         this.validateUuid(uuid);
109         return CommonService.defaultResponse(
110             this.serverApi
111                 .delete(`/${this.resourceType}/${uuid}`),
112             this.actions
113         );
114     }
115
116     get(uuid: string, showErrors?: boolean) {
117         this.validateUuid(uuid);
118         return CommonService.defaultResponse(
119             this.serverApi
120                 .get<T>(`/${this.resourceType}/${uuid}`),
121             this.actions,
122             true, // mapKeys
123             showErrors
124         );
125     }
126
127     list(args: ListArguments = {}, showErrors?: boolean): Promise<ListResults<T>> {
128         const { filters, select, ...other } = args;
129         const params = {
130             ...CommonService.mapKeys(snakeCase)(other),
131             filters: filters ? `[${filters}]` : undefined,
132             select: select
133                 ? `[${select.map(snakeCase).map(s => `"${s}"`).join(', ')}]`
134                 : undefined
135         };
136
137         if (QueryString.stringify(params).length <= 1500) {
138             return CommonService.defaultResponse(
139                 this.serverApi.get(`/${this.resourceType}`, { params }),
140                 this.actions,
141                 showErrors
142             );
143         } else {
144             // Using the POST special case to avoid URI length 414 errors.
145             const formData = new FormData();
146             formData.append("_method", "GET");
147             Object.keys(params).forEach(key => {
148                 if (params[key] !== undefined) {
149                     formData.append(key, params[key]);
150                 }
151             });
152             return CommonService.defaultResponse(
153                 this.serverApi.post(`/${this.resourceType}`, formData, {
154                     params: {
155                         _method: 'GET'
156                     }
157                 }),
158                 this.actions,
159                 showErrors
160             );
161         }
162     }
163
164     update(uuid: string, data: Partial<T>) {
165         this.validateUuid(uuid);
166         return CommonService.defaultResponse(
167             this.serverApi
168                 .put<T>(`/${this.resourceType}/${uuid}`, data && CommonService.mapKeys(snakeCase)(data)),
169             this.actions
170         );
171     }
172 }