@equinor/fusion-framework-module-http 0.1.0-beta.9 → 0.2.0

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/src/client.ts CHANGED
@@ -3,15 +3,25 @@ import { switchMap, takeUntil, tap } from 'rxjs/operators';
3
3
  import { fromFetch } from 'rxjs/fetch';
4
4
 
5
5
  import { ProcessOperators } from './process-operators';
6
+ import { jsonSelector } from './selector';
6
7
 
7
- export type HttpRequestInit = RequestInit & { uri: string; path: string };
8
+ export type FetchRequest = RequestInit & {
9
+ uri: string;
10
+ path: string;
11
+ };
12
+
13
+ export type FetchRequestInit<
14
+ TReturn = unknown,
15
+ TRequest = FetchRequest,
16
+ TResponse = Response
17
+ > = Omit<TRequest, 'uri' | 'path'> & {
18
+ selector?: (response: TResponse) => ObservableInput<TReturn>;
19
+ };
8
20
 
9
21
  /**
10
22
  * Extends @see {ProcessOperators} for pre-processing requests.
11
23
  */
12
- export class HttpRequestHandler<
13
- T extends HttpRequestInit = HttpRequestInit
14
- > extends ProcessOperators<T> {
24
+ export class HttpRequestHandler<T extends FetchRequest = FetchRequest> extends ProcessOperators<T> {
15
25
  /**
16
26
  * Set header that will apply on all requests done by consumer @see {HttpClient}
17
27
  * @param key - name of header
@@ -26,16 +36,17 @@ export class HttpRequestHandler<
26
36
  }
27
37
  }
28
38
 
29
- export type HttpClientCreateOptions<T extends HttpRequestInit = HttpRequestInit> = {
39
+ export type HttpClientCreateOptions<T extends FetchRequest = FetchRequest> = {
30
40
  requestHandler: HttpRequestHandler<T>;
31
41
  };
32
42
 
33
43
  export type HttpResponseHandler<T> = (response: Response) => Promise<T>;
34
44
 
35
- export interface IHttpClient<
36
- TRequest extends HttpRequestInit = HttpRequestInit,
37
- TResponse = Response
38
- > {
45
+ /**
46
+ * @template TRequest request arguments @see {@link https://developer.mozilla.org/en-US/docs/Web/API/request|request}
47
+ * @template TResponse request arguments @see {@link https://developer.mozilla.org/en-US/docs/Web/API/response|response}
48
+ */
49
+ export interface IHttpClient<TRequest extends FetchRequest = FetchRequest, TResponse = Response> {
39
50
  uri: string;
40
51
  /** pre-processor of requests */
41
52
  readonly requestHandler: HttpRequestHandler<TRequest>;
@@ -52,11 +63,13 @@ export interface IHttpClient<
52
63
  * Observable request.
53
64
  * Simplifies execution of request and
54
65
  * note: request will not be executed until subscribe!
66
+ *
55
67
  * @see {@link https://rxjs.dev/api/fetch/fromFetch|RXJS}
68
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch|fetch}
56
69
  * @example
57
70
  * ```ts
58
71
  * // Observer changes of a input field
59
- * const client = window.Fusion.createClient('my-client');
72
+ * const client = modules.http.createClient('my-client');
60
73
  * const input$ = fromEvent(document.getElementById('input'), 'input');
61
74
  * input$.pipe(
62
75
  * // only call after no key input in .5s
@@ -80,10 +93,14 @@ export interface IHttpClient<
80
93
  * ).subscribe(console.log);
81
94
  * ```
82
95
  */
83
- fetch(init: Omit<TRequest, 'uri'> | string): Observable<TResponse>;
96
+ fetch<T = TResponse>(
97
+ path: string,
98
+ init?: FetchRequestInit<T, TRequest, TResponse>
99
+ ): Observable<T>;
84
100
 
85
101
  /**
86
102
  * Fetch a resource as an promise
103
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch|fetch}
87
104
  * @example
88
105
  * ```ts
89
106
  * let controller: AbortController;
@@ -110,7 +127,20 @@ export interface IHttpClient<
110
127
  * });
111
128
  * ```
112
129
  */
113
- fetchAsync(init: Omit<TRequest, 'uri'> | string): Promise<TResponse>;
130
+ fetchAsync<T = TResponse>(
131
+ path: string,
132
+ init?: FetchRequestInit<T, TRequest, TResponse>
133
+ ): Promise<T>;
134
+
135
+ json<T = TResponse>(
136
+ path: string,
137
+ init?: FetchRequestInit<T, TRequest, TResponse>
138
+ ): Observable<T>;
139
+
140
+ jsonAsync<T = TResponse>(
141
+ path: string,
142
+ init?: FetchRequestInit<T, TRequest, TResponse>
143
+ ): Promise<T>;
114
144
 
115
145
  /**
116
146
  * Abort all ongoing request for current client
@@ -119,7 +149,7 @@ export interface IHttpClient<
119
149
  }
120
150
 
121
151
  /** Base http client for executing requests */
122
- export class HttpClient<TRequest extends HttpRequestInit = HttpRequestInit, TResponse = Response>
152
+ export class HttpClient<TRequest extends FetchRequest = FetchRequest, TResponse = Response>
123
153
  implements IHttpClient<TRequest, TResponse>
124
154
  {
125
155
  readonly requestHandler: HttpRequestHandler<TRequest>;
@@ -153,9 +183,57 @@ export class HttpClient<TRequest extends HttpRequestInit = HttpRequestInit, TRes
153
183
  // called by children for constructor setup
154
184
  }
155
185
 
156
- public fetch(init: Omit<TRequest, 'uri'> | string): Observable<TResponse> {
157
- const options = typeof init === 'string' ? { path: init } : init;
158
- return of({ ...options, uri: this._resolveUrl(options.path) } as TRequest).pipe(
186
+ public fetch<T = TResponse>(
187
+ path: string,
188
+ args?: FetchRequestInit<T, TRequest, TResponse>
189
+ ): Observable<T> {
190
+ return this._fetch(path, args);
191
+ }
192
+
193
+ public fetchAsync<T = TResponse>(
194
+ path: string,
195
+ args?: FetchRequestInit<T, TRequest, TResponse>
196
+ ): Promise<T> {
197
+ return firstValueFrom(this.fetch<T>(path, args));
198
+ }
199
+
200
+ public json<T = TResponse>(
201
+ path: string,
202
+ args?: FetchRequestInit<T, TRequest, TResponse>
203
+ ): Observable<T> {
204
+ const body = typeof args?.body === 'object' ? JSON.stringify(args?.body) : args?.body;
205
+ const selector = args?.selector ?? jsonSelector;
206
+ const header = new Headers(args?.headers);
207
+ header.append('Content-Type', 'application/json');
208
+ return this.fetch(path, {
209
+ ...args,
210
+ body,
211
+ selector,
212
+ } as FetchRequestInit<T, TRequest, TResponse>);
213
+ }
214
+
215
+ public jsonAsync<T = TResponse>(
216
+ path: string,
217
+ args?: FetchRequestInit<T, TRequest, TResponse>
218
+ ): Promise<T> {
219
+ return firstValueFrom(this.json<T>(path, args));
220
+ }
221
+
222
+ public abort(): void {
223
+ this._abort$.next();
224
+ }
225
+
226
+ protected _fetch<T = TResponse>(
227
+ path: string,
228
+ args?: FetchRequestInit<T, TRequest, TResponse>
229
+ ): Observable<T> {
230
+ const { selector, ...options } = Object.assign({}, args || { selector: undefined }, {
231
+ path,
232
+ });
233
+ const response$ = of({
234
+ ...options,
235
+ uri: this._resolveUrl(options.path),
236
+ } as TRequest).pipe(
159
237
  /** prepare request, allow extensions to modify request */
160
238
  switchMap((x) => this._prepareRequest(x)),
161
239
  /** push request to event buss */
@@ -166,17 +244,12 @@ export class HttpClient<TRequest extends HttpRequestInit = HttpRequestInit, TRes
166
244
  switchMap((x) => this._prepareResponse(x)),
167
245
  /** push response to event buss */
168
246
  tap((x) => this._response$.next(x)),
247
+
248
+ switchMap((x) => (selector ? selector(x) : Promise.resolve(x))),
169
249
  /** cancel request on abort signal */
170
250
  takeUntil(this._abort$)
171
251
  );
172
- }
173
-
174
- public fetchAsync(init: Omit<TRequest, 'uri'> | string): Promise<TResponse> {
175
- return firstValueFrom(this.fetch(init));
176
- }
177
-
178
- public abort(): void {
179
- this._abort$.next();
252
+ return response$ as unknown as Observable<T>;
180
253
  }
181
254
 
182
255
  protected _prepareRequest(init: TRequest): ObservableInput<TRequest> {
@@ -1,6 +1,6 @@
1
- import { HttpRequestHandler, HttpRequestInit, IHttpClient } from './client';
1
+ import { HttpRequestHandler, FetchRequest, IHttpClient } from './client';
2
2
 
3
- interface HttpClientConstructorOptions<TInit extends HttpRequestInit> {
3
+ interface HttpClientConstructorOptions<TInit extends FetchRequest> {
4
4
  requestHandler: HttpRequestHandler<TInit>;
5
5
  }
6
6
 
@@ -11,8 +11,9 @@ interface HttpClientConstructor<TClient extends IHttpClient> {
11
11
  ): TClient;
12
12
  }
13
13
 
14
- interface HttpClientOptions<TClient extends IHttpClient> {
14
+ export interface HttpClientOptions<TClient extends IHttpClient> {
15
15
  baseUri?: string;
16
+ defaultScopes?: string[];
16
17
  ctor?: HttpClientConstructor<TClient>;
17
18
  onCreate?: (client: TClient) => void;
18
19
  requestHandler?: HttpRequestHandler<HttpClientRequestInitType<TClient>>;
@@ -20,49 +21,93 @@ interface HttpClientOptions<TClient extends IHttpClient> {
20
21
 
21
22
  type HttpClientRequestInitType<T extends IHttpClient> = T extends IHttpClient<infer U> ? U : never;
22
23
 
24
+ /**
25
+ * Instance for configuring http client
26
+ * @template TClient base type of client that the provider will create
27
+ */
23
28
  export interface IHttpClientConfigurator<TClient extends IHttpClient = IHttpClient> {
24
29
  readonly clients: Record<string, HttpClientOptions<TClient>>;
25
30
  readonly defaultHttpClientCtor: HttpClientConstructor<TClient>;
26
31
  readonly defaultHttpRequestHandler: HttpRequestHandler<HttpClientRequestInitType<TClient>>;
27
32
 
28
- configureClient(name: string, uri: string): HttpClientConfigurator<TClient>;
29
-
33
+ /**
34
+ * Configure a client with arguments
35
+ * @param name name of the client
36
+ * @param args option that are used cor creating a client
37
+ * @example
38
+ * ```ts
39
+ * configurator.http.configureClient('foo',{
40
+ * baseUri: 'https://foo.bar',
41
+ * defaultScopes: ['foobar/.default']
42
+ * });
43
+ * ```
44
+ */
30
45
  configureClient<T extends TClient>(
31
46
  name: string,
32
47
  args: HttpClientOptions<T>
33
48
  ): HttpClientConfigurator<TClient>;
34
49
 
50
+ /**
51
+ * Configure a simple client by name to an endpoint
52
+ * @param name name of the client
53
+ * @param uri base endpoint for the client
54
+ */
55
+ configureClient(name: string, uri: string): HttpClientConfigurator<TClient>;
56
+
57
+ /**
58
+ * Creates a client with callback configuration
59
+ * @param name name of the client
60
+ * @param onCreate callback when a client is created
61
+ * ```ts
62
+ * configurator.http.configureClient('foo',(client) => {
63
+ * client.requestHandler.add('logger', (request) => console.log(request));
64
+ * });
65
+ * ```
66
+ */
35
67
  configureClient<T extends TClient>(
36
68
  name: string,
37
69
  onCreate: (client: T) => void
38
70
  ): HttpClientConfigurator<TClient>;
39
71
 
72
+ /**
73
+ * Check if there is a configuration for provided name
74
+ */
40
75
  hasClient(name: string): boolean;
41
76
  }
42
77
 
78
+ /** @inheritdoc */
43
79
  export class HttpClientConfigurator<TClient extends IHttpClient>
44
80
  implements IHttpClientConfigurator<TClient>
45
81
  {
46
82
  protected _clients: Record<string, HttpClientOptions<TClient>> = {};
47
83
 
84
+ /** Get a clone of all configured clients */
48
85
  public get clients(): Record<string, HttpClientOptions<TClient>> {
49
86
  return { ...this._clients };
50
87
  }
51
88
 
89
+ /** default class for creation of http clients */
52
90
  readonly defaultHttpClientCtor: HttpClientConstructor<TClient>;
53
91
 
92
+ /** default request handler for http clients, applied on creation */
54
93
  readonly defaultHttpRequestHandler = new HttpRequestHandler<
55
94
  HttpClientRequestInitType<TClient>
56
95
  >();
57
96
 
97
+ /**
98
+ * Create a instance of http configuration
99
+ * @param client defaultHttpRequestHandler
100
+ */
58
101
  constructor(client: HttpClientConstructor<TClient>) {
59
102
  this.defaultHttpClientCtor = client;
60
103
  }
61
104
 
105
+ /** @inheritdoc */
62
106
  hasClient(name: string): boolean {
63
107
  return Object.keys(this._clients).includes(name);
64
108
  }
65
109
 
110
+ /** @inheritdoc */
66
111
  configureClient<T extends TClient>(
67
112
  name: string,
68
113
  args: string | HttpClientOptions<T> | HttpClientOptions<T>['onCreate']
package/src/index.ts CHANGED
@@ -1,8 +1,14 @@
1
- export { HttpClient, IHttpClient } from './client';
1
+ /**
2
+ * [[include:module-http/README.MD]]
3
+ * @module
4
+ */
5
+
6
+ export { HttpClient, IHttpClient, FetchRequestInit } from './client';
2
7
  export { HttpClientMsal } from './client-msal';
3
8
 
4
9
  export * from './configurator';
5
10
  export * from './provider';
6
11
  export * from './module';
12
+ export * from './selector';
7
13
 
8
14
  export { default } from './module';
package/src/provider.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { HttpClient } from './client';
2
- import { IHttpClientConfigurator } from './configurator';
2
+ import { HttpClientOptions, IHttpClientConfigurator } from './configurator';
3
3
 
4
4
  export class ClientNotFoundException extends Error {
5
5
  constructor(message: string) {
@@ -12,6 +12,7 @@ export interface IHttpClientProvider<TClient extends HttpClient = HttpClient> {
12
12
  hasClient(key: string): boolean;
13
13
  /** create a new http client */
14
14
  createClient(key: string): TClient;
15
+ createClient(key: HttpClientOptions<TClient>): TClient;
15
16
  /**
16
17
  * Class cast creation of custom client
17
18
  * @example
@@ -46,21 +47,18 @@ export class HttpClientProvider<TClient extends HttpClient>
46
47
  return Object.keys(this.config.clients).includes(key);
47
48
  }
48
49
 
49
- public createClient(key: string): TClient {
50
- let config = this.config.clients[key];
51
- if (!config && isURL(key)) {
52
- config = { baseUri: key };
53
- } else if (!config) {
54
- throw new ClientNotFoundException(`No registered http client for key [${key}]`);
55
- }
50
+ public createClient(keyOrConfig: string | HttpClientOptions<TClient>): TClient {
51
+ const config = this._resolveConfig(keyOrConfig);
56
52
  const {
57
53
  baseUri,
54
+ defaultScopes = [],
58
55
  onCreate,
59
56
  ctor = this.config.defaultHttpClientCtor,
60
57
  requestHandler = this.config.defaultHttpRequestHandler,
61
- } = config;
58
+ } = config as HttpClientOptions<TClient>;
62
59
  const options = { requestHandler };
63
60
  const instance = new ctor(baseUri || '', options) as TClient;
61
+ Object.assign(instance, { defaultScopes });
64
62
  onCreate && onCreate(instance as TClient);
65
63
  return instance as TClient;
66
64
  }
@@ -68,4 +66,20 @@ export class HttpClientProvider<TClient extends HttpClient>
68
66
  public createCustomClient<T extends HttpClient>(key: string): T {
69
67
  return this.createClient(key) as unknown as T;
70
68
  }
69
+
70
+ protected _resolveConfig(
71
+ keyOrConfig: string | HttpClientOptions<TClient>
72
+ ): HttpClientOptions<TClient> {
73
+ if (typeof keyOrConfig === 'string') {
74
+ const config = this.config.clients[keyOrConfig];
75
+ if (!config && isURL(keyOrConfig)) {
76
+ return { baseUri: keyOrConfig };
77
+ } else if (!config) {
78
+ throw new ClientNotFoundException(
79
+ `No registered http client for key [${keyOrConfig}]`
80
+ );
81
+ }
82
+ }
83
+ return keyOrConfig as HttpClientOptions<TClient>;
84
+ }
71
85
  }
@@ -0,0 +1,12 @@
1
+ export const jsonSelector = <TType = unknown, TResponse extends Response = Response>(
2
+ response: TResponse
3
+ ): Promise<TType> => {
4
+ if (!response.ok) {
5
+ throw new Error('Network response was not OK');
6
+ }
7
+ try {
8
+ return response.json();
9
+ } catch (err) {
10
+ throw Error('failed to parse response', { cause: err as Error });
11
+ }
12
+ };