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