@scm-manager/ui-api 2.30.2-20220101-185531 → 2.30.2-20220102-104837

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scm-manager/ui-api",
3
- "version": "2.30.2-20220101-185531",
3
+ "version": "2.30.2-20220102-104837",
4
4
  "description": "React hook api for the SCM-Manager backend",
5
5
  "main": "src/index.ts",
6
6
  "files": [
@@ -25,7 +25,7 @@
25
25
  "react-test-renderer": "^17.0.1"
26
26
  },
27
27
  "dependencies": {
28
- "@scm-manager/ui-types": "^2.30.2-20220101-185531",
28
+ "@scm-manager/ui-types": "^2.30.2-20220102-104837",
29
29
  "fetch-mock-jest": "^1.5.1",
30
30
  "gitdiff-parser": "^0.1.2",
31
31
  "query-string": "6.14.1",
@@ -24,7 +24,7 @@
24
24
 
25
25
  import { apiClient, createUrl, extractXsrfTokenFromCookie } from "./apiclient";
26
26
  import fetchMock from "fetch-mock";
27
- import { BackendError } from "./errors";
27
+ import { BackendError, BadGatewayError } from "./errors";
28
28
 
29
29
  describe("create url", () => {
30
30
  it("should not change absolute urls", () => {
@@ -45,9 +45,9 @@ describe("error handling tests", () => {
45
45
  context: [
46
46
  {
47
47
  type: "planet",
48
- id: "earth",
49
- },
50
- ],
48
+ id: "earth"
49
+ }
50
+ ]
51
51
  };
52
52
 
53
53
  afterEach(() => {
@@ -55,9 +55,9 @@ describe("error handling tests", () => {
55
55
  fetchMock.restore();
56
56
  });
57
57
 
58
- it("should create a normal error, if the content type is not scmm-error", (done) => {
58
+ it("should create a normal error, if the content type is not scmm-error", done => {
59
59
  fetchMock.getOnce("/api/v2/error", {
60
- status: 404,
60
+ status: 404
61
61
  });
62
62
 
63
63
  apiClient.get("/error").catch((err: Error) => {
@@ -67,13 +67,24 @@ describe("error handling tests", () => {
67
67
  });
68
68
  });
69
69
 
70
- it("should create an backend error, if the content type is scmm-error", (done) => {
70
+ it("should create a bad gateway error", done => {
71
+ fetchMock.getOnce("/api/v2/error", {
72
+ status: 502
73
+ });
74
+
75
+ apiClient.get("/error").catch((err: Error) => {
76
+ expect(err).toBeInstanceOf(BadGatewayError);
77
+ done();
78
+ });
79
+ });
80
+
81
+ it("should create an backend error, if the content type is scmm-error", done => {
71
82
  fetchMock.getOnce("/api/v2/error", {
72
83
  status: 404,
73
84
  headers: {
74
- "Content-Type": "application/vnd.scmm-error+json;v=2",
85
+ "Content-Type": "application/vnd.scmm-error+json;v=2"
75
86
  },
76
- body: earthNotFoundError,
87
+ body: earthNotFoundError
77
88
  });
78
89
 
79
90
  apiClient.get("/error").catch((err: BackendError) => {
@@ -87,8 +98,8 @@ describe("error handling tests", () => {
87
98
  expect(err.context).toEqual([
88
99
  {
89
100
  type: "planet",
90
- id: "earth",
91
- },
101
+ id: "earth"
102
+ }
92
103
  ]);
93
104
  done();
94
105
  });
package/src/apiclient.ts CHANGED
@@ -25,12 +25,13 @@
25
25
  import { contextPath } from "./urls";
26
26
  import {
27
27
  BackendErrorContent,
28
+ BadGatewayError,
28
29
  createBackendError,
29
30
  ForbiddenError,
30
31
  isBackendError,
31
32
  TOKEN_EXPIRED_ERROR_CODE,
32
33
  TokenExpiredError,
33
- UnauthorizedError,
34
+ UnauthorizedError
34
35
  } from "./errors";
35
36
 
36
37
  type SubscriptionEvent = {
@@ -62,7 +63,12 @@ type SubscriptionArgument = MessageListeners | SubscriptionContext;
62
63
 
63
64
  type Cancel = () => void;
64
65
 
65
- const sessionId = (Date.now().toString(36) + Math.random().toString(36).substr(2, 5)).toUpperCase();
66
+ const sessionId = (
67
+ Date.now().toString(36) +
68
+ Math.random()
69
+ .toString(36)
70
+ .substr(2, 5)
71
+ ).toUpperCase();
66
72
 
67
73
  const extractXsrfTokenFromJwt = (jwt: string) => {
68
74
  const parts = jwt.split(".");
@@ -97,7 +103,7 @@ const createRequestHeaders = () => {
97
103
  // identify the web interface
98
104
  "X-SCM-Client": "WUI",
99
105
  // identify the window session
100
- "X-SCM-Session-ID": sessionId,
106
+ "X-SCM-Session-ID": sessionId
101
107
  };
102
108
 
103
109
  const xsrf = extractXsrfToken();
@@ -107,10 +113,10 @@ const createRequestHeaders = () => {
107
113
  return headers;
108
114
  };
109
115
 
110
- const applyFetchOptions: (p: RequestInit) => RequestInit = (o) => {
116
+ const applyFetchOptions: (p: RequestInit) => RequestInit = o => {
111
117
  if (o.headers) {
112
118
  o.headers = {
113
- ...createRequestHeaders(),
119
+ ...createRequestHeaders()
114
120
  };
115
121
  } else {
116
122
  o.headers = createRequestHeaders();
@@ -134,6 +140,8 @@ function handleFailure(response: Response) {
134
140
  throw new UnauthorizedError("Unauthorized", 401);
135
141
  } else if (response.status === 403) {
136
142
  throw new ForbiddenError("Forbidden", 403);
143
+ } else if (response.status === 502) {
144
+ throw new BadGatewayError("Bad Gateway", 502);
137
145
  } else if (isBackendError(response)) {
138
146
  return response.json().then((content: BackendErrorContent) => {
139
147
  throw createBackendError(content, response.status);
@@ -169,7 +177,9 @@ class ApiClient {
169
177
  requestListeners: RequestListener[] = [];
170
178
 
171
179
  get = (url: string): Promise<Response> => {
172
- return this.request(url, applyFetchOptions({})).then(handleFailure).catch(this.notifyAndRethrow);
180
+ return this.request(url, applyFetchOptions({}))
181
+ .then(handleFailure)
182
+ .catch(this.notifyAndRethrow);
173
183
  };
174
184
 
175
185
  post = (
@@ -196,7 +206,7 @@ class ApiClient {
196
206
  const options: RequestInit = {
197
207
  method: "POST",
198
208
  body: formData,
199
- headers: additionalHeaders,
209
+ headers: additionalHeaders
200
210
  };
201
211
  return this.httpRequestWithBinaryBody(options, url);
202
212
  };
@@ -207,18 +217,22 @@ class ApiClient {
207
217
 
208
218
  head = (url: string) => {
209
219
  let options: RequestInit = {
210
- method: "HEAD",
220
+ method: "HEAD"
211
221
  };
212
222
  options = applyFetchOptions(options);
213
- return this.request(url, options).then(handleFailure).catch(this.notifyAndRethrow);
223
+ return this.request(url, options)
224
+ .then(handleFailure)
225
+ .catch(this.notifyAndRethrow);
214
226
  };
215
227
 
216
228
  delete = (url: string): Promise<Response> => {
217
229
  let options: RequestInit = {
218
- method: "DELETE",
230
+ method: "DELETE"
219
231
  };
220
232
  options = applyFetchOptions(options);
221
- return this.request(url, options).then(handleFailure).catch(this.notifyAndRethrow);
233
+ return this.request(url, options)
234
+ .then(handleFailure)
235
+ .catch(this.notifyAndRethrow);
222
236
  };
223
237
 
224
238
  httpRequestWithJSONBody = (
@@ -230,7 +244,7 @@ class ApiClient {
230
244
  ): Promise<Response> => {
231
245
  const options: RequestInit = {
232
246
  method: method,
233
- headers: additionalHeaders,
247
+ headers: additionalHeaders
234
248
  };
235
249
  if (payload) {
236
250
  options.body = JSON.stringify(payload);
@@ -246,7 +260,7 @@ class ApiClient {
246
260
  ) => {
247
261
  const options: RequestInit = {
248
262
  method: method,
249
- headers: additionalHeaders,
263
+ headers: additionalHeaders
250
264
  };
251
265
  options.body = payload;
252
266
  return this.httpRequestWithBinaryBody(options, url, "text/plain");
@@ -262,12 +276,14 @@ class ApiClient {
262
276
  options.headers["Content-Type"] = contentType;
263
277
  }
264
278
 
265
- return this.request(url, options).then(handleFailure).catch(this.notifyAndRethrow);
279
+ return this.request(url, options)
280
+ .then(handleFailure)
281
+ .catch(this.notifyAndRethrow);
266
282
  };
267
283
 
268
284
  subscribe(url: string, argument: SubscriptionArgument): Cancel {
269
285
  const es = new EventSource(createUrlWithIdentifiers(url), {
270
- withCredentials: true,
286
+ withCredentials: true
271
287
  });
272
288
 
273
289
  let listeners: MessageListeners;
@@ -308,11 +324,11 @@ class ApiClient {
308
324
  };
309
325
 
310
326
  private notifyRequestListeners = (url: string, options: RequestInit) => {
311
- this.requestListeners.forEach((requestListener) => requestListener(url, options));
327
+ this.requestListeners.forEach(requestListener => requestListener(url, options));
312
328
  };
313
329
 
314
330
  private notifyAndRethrow = (error: Error): never => {
315
- this.errorListeners.forEach((errorListener) => errorListener(error));
331
+ this.errorListeners.forEach(errorListener => errorListener(error));
316
332
  throw error;
317
333
  };
318
334
  }
package/src/errors.ts CHANGED
@@ -79,6 +79,15 @@ export class UnauthorizedError extends Error {
79
79
  }
80
80
  }
81
81
 
82
+ export class BadGatewayError extends Error {
83
+ statusCode: number;
84
+
85
+ constructor(message: string, statusCode: number) {
86
+ super(message);
87
+ this.statusCode = statusCode;
88
+ }
89
+ }
90
+
82
91
  export class TokenExpiredError extends UnauthorizedError {}
83
92
 
84
93
  export class ForbiddenError extends Error {
package/src/plugins.ts CHANGED
@@ -27,6 +27,7 @@ import { isPluginCollection, PendingPlugins, Plugin, PluginCollection } from "@s
27
27
  import { useMutation, useQuery, useQueryClient } from "react-query";
28
28
  import { apiClient } from "./apiclient";
29
29
  import { requiredLink } from "./links";
30
+ import { BadGatewayError } from "./errors";
30
31
 
31
32
  type WaitForRestartOptions = {
32
33
  initialDelay?: number;
@@ -37,31 +38,38 @@ const waitForRestartAfter = (
37
38
  promise: Promise<any>,
38
39
  { initialDelay = 1000, timeout = 500 }: WaitForRestartOptions = {}
39
40
  ): Promise<void> => {
40
- const endTime = Number(new Date()) + 60000;
41
+ const endTime = Number(new Date()) + 4 * 60 * 1000;
41
42
  let started = false;
42
43
 
43
- const executor =
44
- <T = any>(data: T) =>
45
- (resolve: (result: T) => void, reject: (error: Error) => void) => {
46
- // we need some initial delay
47
- if (!started) {
48
- started = true;
49
- setTimeout(executor(data), initialDelay, resolve, reject);
50
- } else {
51
- apiClient
52
- .get("")
53
- .then(() => resolve(data))
54
- .catch(() => {
55
- if (Number(new Date()) < endTime) {
56
- setTimeout(executor(data), timeout, resolve, reject);
57
- } else {
58
- reject(new Error("timeout reached"));
59
- }
60
- });
61
- }
62
- };
44
+ const executor = <T = any>(data: T) => (resolve: (result: T) => void, reject: (error: Error) => void) => {
45
+ // we need some initial delay
46
+ if (!started) {
47
+ started = true;
48
+ setTimeout(executor(data), initialDelay, resolve, reject);
49
+ } else {
50
+ apiClient
51
+ .get("")
52
+ .then(() => resolve(data))
53
+ .catch(() => {
54
+ if (Number(new Date()) < endTime) {
55
+ setTimeout(executor(data), timeout, resolve, reject);
56
+ } else {
57
+ reject(new Error("timeout reached"));
58
+ }
59
+ });
60
+ }
61
+ };
63
62
 
64
- return promise.then((data) => new Promise<void>(executor(data)));
63
+ return promise
64
+ .catch(err => {
65
+ if (err instanceof BadGatewayError) {
66
+ // in some rare cases the reverse proxy stops forwarding traffic to scm before the response is returned
67
+ // in such a case the reverse proxy returns 502 (bad gateway), so we treat 502 not as error
68
+ return "ok";
69
+ }
70
+ throw err;
71
+ })
72
+ .then(data => new Promise<void>(executor(data)));
65
73
  };
66
74
 
67
75
  export type UseAvailablePluginsOptions = {
@@ -72,10 +80,10 @@ export const useAvailablePlugins = ({ enabled }: UseAvailablePluginsOptions = {}
72
80
  const indexLink = useRequiredIndexLink("availablePlugins");
73
81
  return useQuery<PluginCollection, Error>(
74
82
  ["plugins", "available"],
75
- () => apiClient.get(indexLink).then((response) => response.json()),
83
+ () => apiClient.get(indexLink).then(response => response.json()),
76
84
  {
77
85
  enabled,
78
- retry: 3,
86
+ retry: 3
79
87
  }
80
88
  );
81
89
  };
@@ -88,10 +96,10 @@ export const useInstalledPlugins = ({ enabled }: UseInstalledPluginsOptions = {}
88
96
  const indexLink = useRequiredIndexLink("installedPlugins");
89
97
  return useQuery<PluginCollection, Error>(
90
98
  ["plugins", "installed"],
91
- () => apiClient.get(indexLink).then((response) => response.json()),
99
+ () => apiClient.get(indexLink).then(response => response.json()),
92
100
  {
93
101
  enabled,
94
- retry: 3,
102
+ retry: 3
95
103
  }
96
104
  );
97
105
  };
@@ -100,10 +108,10 @@ export const usePendingPlugins = (): ApiResult<PendingPlugins> => {
100
108
  const indexLink = useIndexLink("pendingPlugins");
101
109
  return useQuery<PendingPlugins, Error>(
102
110
  ["plugins", "pending"],
103
- () => apiClient.get(indexLink!).then((response) => response.json()),
111
+ () => apiClient.get(indexLink!).then(response => response.json()),
104
112
  {
105
113
  enabled: !!indexLink,
106
- retry: 3,
114
+ retry: 3
107
115
  }
108
116
  );
109
117
  };
@@ -135,19 +143,19 @@ export const useInstallPlugin = () => {
135
143
  return promise;
136
144
  },
137
145
  {
138
- onSuccess: () => queryClient.invalidateQueries("plugins"),
146
+ onSuccess: () => queryClient.invalidateQueries("plugins")
139
147
  }
140
148
  );
141
149
  return {
142
150
  install: (plugin: Plugin, restartOptions: RestartOptions = {}) =>
143
151
  mutate({
144
152
  plugin,
145
- restartOptions,
153
+ restartOptions
146
154
  }),
147
155
  isLoading,
148
156
  error,
149
157
  data,
150
- isInstalled: !!data,
158
+ isInstalled: !!data
151
159
  };
152
160
  };
153
161
 
@@ -162,18 +170,18 @@ export const useUninstallPlugin = () => {
162
170
  return promise;
163
171
  },
164
172
  {
165
- onSuccess: () => queryClient.invalidateQueries("plugins"),
173
+ onSuccess: () => queryClient.invalidateQueries("plugins")
166
174
  }
167
175
  );
168
176
  return {
169
177
  uninstall: (plugin: Plugin, restartOptions: RestartOptions = {}) =>
170
178
  mutate({
171
179
  plugin,
172
- restartOptions,
180
+ restartOptions
173
181
  }),
174
182
  isLoading,
175
183
  error,
176
- isUninstalled: !!data,
184
+ isUninstalled: !!data
177
185
  };
178
186
  };
179
187
 
@@ -196,18 +204,18 @@ export const useUpdatePlugins = () => {
196
204
  return promise;
197
205
  },
198
206
  {
199
- onSuccess: () => queryClient.invalidateQueries("plugins"),
207
+ onSuccess: () => queryClient.invalidateQueries("plugins")
200
208
  }
201
209
  );
202
210
  return {
203
211
  update: (plugin: Plugin | PluginCollection, restartOptions: RestartOptions = {}) =>
204
212
  mutate({
205
213
  plugins: plugin,
206
- restartOptions,
214
+ restartOptions
207
215
  }),
208
216
  isLoading,
209
217
  error,
210
- isUpdated: !!data,
218
+ isUpdated: !!data
211
219
  };
212
220
  };
213
221
 
@@ -222,7 +230,7 @@ export const useExecutePendingPlugins = () => {
222
230
  ({ pending, restartOptions }) =>
223
231
  waitForRestartAfter(apiClient.post(requiredLink(pending, "execute")), restartOptions),
224
232
  {
225
- onSuccess: () => queryClient.invalidateQueries("plugins"),
233
+ onSuccess: () => queryClient.invalidateQueries("plugins")
226
234
  }
227
235
  );
228
236
  return {
@@ -230,22 +238,22 @@ export const useExecutePendingPlugins = () => {
230
238
  mutate({ pending, restartOptions }),
231
239
  isLoading,
232
240
  error,
233
- isExecuted: !!data,
241
+ isExecuted: !!data
234
242
  };
235
243
  };
236
244
 
237
245
  export const useCancelPendingPlugins = () => {
238
246
  const queryClient = useQueryClient();
239
247
  const { mutate, isLoading, error, data } = useMutation<unknown, Error, PendingPlugins>(
240
- (pending) => apiClient.post(requiredLink(pending, "cancel")),
248
+ pending => apiClient.post(requiredLink(pending, "cancel")),
241
249
  {
242
- onSuccess: () => queryClient.invalidateQueries("plugins"),
250
+ onSuccess: () => queryClient.invalidateQueries("plugins")
243
251
  }
244
252
  );
245
253
  return {
246
254
  update: (pending: PendingPlugins) => mutate(pending),
247
255
  isLoading,
248
256
  error,
249
- isCancelled: !!data,
257
+ isCancelled: !!data
250
258
  };
251
259
  };