Merge branch 'master'
[arvados-workbench2.git] / src / services / common-service / common-resource-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 { Resource } from "src/models/resource";
8 import * as uuid from "uuid/v4";
9 import { ApiActions } from "~/services/api/api-actions";
10
11 export interface ListArguments {
12     limit?: number;
13     offset?: number;
14     filters?: string;
15     order?: string;
16     select?: string[];
17     distinct?: boolean;
18     count?: string;
19 }
20
21 export interface ListResults<T> {
22     kind: string;
23     offset: number;
24     limit: number;
25     items: T[];
26     itemsAvailable: number;
27 }
28
29 export interface Errors {
30     errors: string[];
31     errorToken: string;
32 }
33
34 export enum CommonResourceServiceError {
35     UNIQUE_VIOLATION = 'UniqueViolation',
36     OWNERSHIP_CYCLE = 'OwnershipCycle',
37     MODIFYING_CONTAINER_REQUEST_FINAL_STATE = 'ModifyingContainerRequestFinalState',
38     NAME_HAS_ALREADY_BEEN_TAKEN = 'NameHasAlreadyBeenTaken',
39     UNKNOWN = 'Unknown',
40     NONE = 'None'
41 }
42
43 export class CommonResourceService<T extends Resource> {
44
45     static mapResponseKeys = (response: { data: any }) =>
46         CommonResourceService.mapKeys(_.camelCase)(response.data)
47
48     static mapKeys = (mapFn: (key: string) => string) =>
49         (value: any): any => {
50             switch (true) {
51                 case _.isPlainObject(value):
52                     return Object
53                         .keys(value)
54                         .map(key => [key, mapFn(key)])
55                         .reduce((newValue, [key, newKey]) => ({
56                             ...newValue,
57                             [newKey]: CommonResourceService.mapKeys(mapFn)(value[key])
58                         }), {});
59                 case _.isArray(value):
60                     return value.map(CommonResourceService.mapKeys(mapFn));
61                 default:
62                     return value;
63             }
64         }
65
66     static defaultResponse<R>(promise: AxiosPromise<R>, actions: ApiActions, mapKeys = true): Promise<R> {
67         const reqId = uuid();
68         actions.progressFn(reqId, true);
69         return promise
70             .then(data => {
71                 actions.progressFn(reqId, false);
72                 return data;
73             })
74             .then((response: { data: any }) => {
75                 return mapKeys ? CommonResourceService.mapResponseKeys(response) : response.data;
76             })
77             .catch(({ response }) => {
78                 actions.progressFn(reqId, false);
79                 const errors = CommonResourceService.mapResponseKeys(response) as Errors;
80                 actions.errorFn(reqId, errors);
81                 throw errors;
82             });
83     }
84
85     protected serverApi: AxiosInstance;
86     protected resourceType: string;
87     protected actions: ApiActions;
88
89     constructor(serverApi: AxiosInstance, resourceType: string, actions: ApiActions) {
90         this.serverApi = serverApi;
91         this.resourceType = '/' + resourceType + '/';
92         this.actions = actions;
93     }
94
95     create(data?: Partial<T>) {
96         return CommonResourceService.defaultResponse(
97             this.serverApi
98                 .post<T>(this.resourceType, data && CommonResourceService.mapKeys(_.snakeCase)(data)),
99             this.actions
100         );
101     }
102
103     delete(uuid: string): Promise<T> {
104         return CommonResourceService.defaultResponse(
105             this.serverApi
106                 .delete(this.resourceType + uuid),
107             this.actions
108         );
109     }
110
111     get(uuid: string) {
112         return CommonResourceService.defaultResponse(
113             this.serverApi
114                 .get<T>(this.resourceType + uuid),
115             this.actions
116         );
117     }
118
119     list(args: ListArguments = {}): Promise<ListResults<T>> {
120         const { filters, order, ...other } = args;
121         const params = {
122             ...other,
123             filters: filters ? `[${filters}]` : undefined,
124             order: order ? order : undefined
125         };
126         return CommonResourceService.defaultResponse(
127             this.serverApi
128                 .get(this.resourceType, {
129                     params: CommonResourceService.mapKeys(_.snakeCase)(params)
130                 }),
131             this.actions
132         );
133     }
134
135     update(uuid: string, data: Partial<T>) {
136         return CommonResourceService.defaultResponse(
137             this.serverApi
138                 .put<T>(this.resourceType + uuid, data && CommonResourceService.mapKeys(_.snakeCase)(data)),
139             this.actions
140         );
141     }
142 }
143
144 export const getCommonResourceServiceError = (errorResponse: any) => {
145     if ('errors' in errorResponse && 'errorToken' in errorResponse) {
146         const error = errorResponse.errors.join('');
147         switch (true) {
148             case /UniqueViolation/.test(error):
149                 return CommonResourceServiceError.UNIQUE_VIOLATION;
150             case /ownership cycle/.test(error):
151                 return CommonResourceServiceError.OWNERSHIP_CYCLE;
152             case /Mounts cannot be modified in state 'Final'/.test(error):
153                 return CommonResourceServiceError.MODIFYING_CONTAINER_REQUEST_FINAL_STATE;
154             case /Name has already been taken/.test(error):
155                 return CommonResourceServiceError.NAME_HAS_ALREADY_BEEN_TAKEN;
156             default:
157                 return CommonResourceServiceError.UNKNOWN;
158         }
159     }
160     return CommonResourceServiceError.NONE;
161 };
162
163