@verii/http-client 1.1.0-pre.1763452964 → 1.1.0-pre.1764142377

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.
Files changed (2) hide show
  1. package/package.json +2 -2
  2. package/src/client.js +120 -89
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@verii/http-client",
3
- "version": "1.1.0-pre.1763452964",
3
+ "version": "1.1.0-pre.1764142377",
4
4
  "description": "HTTP client for Verii network",
5
5
  "repository": "https://github.com/LFDT-Verii/core",
6
6
  "main": "index.js",
@@ -42,5 +42,5 @@
42
42
  "lib"
43
43
  ]
44
44
  },
45
- "gitHead": "18943a1a15dc61b19d05b8ec0b0112234817e1dc"
45
+ "gitHead": "39d0123fb7911d1a2814777ad0e0a25e35bc13c8"
46
46
  }
package/src/client.js CHANGED
@@ -22,74 +22,28 @@ const {
22
22
  getGlobalDispatcher,
23
23
  } = require('undici');
24
24
  const { createOidcInterceptor } = require('undici-oidc-interceptor');
25
- const { map } = require('lodash/fp');
26
25
  const { isObject } = require('lodash');
27
26
  const pkg = require('../package.json');
28
27
 
29
28
  const USER_AGENT_HEADER = `${pkg.name}/${pkg.version}`;
30
- const registeredPrefixUrls = new Map();
31
29
 
32
30
  const initCache = () => new cacheStores.MemoryCacheStore();
33
31
 
34
- const buildInterceptors = ({
35
- isTest,
36
- cache,
37
- tokensEndpoint,
38
- clientId,
39
- clientSecret,
40
- scopes,
41
- audience,
42
- }) => {
43
- const requiredInterceptors = [
44
- interceptors.responseError(),
45
- ...addCache(cache),
46
- ];
47
-
48
- if (tokensEndpoint) {
49
- const origins = map(
50
- (url) => url.origin,
51
- registeredPrefixUrls.values().toArray()
52
- );
53
- const oidcInterceptor = createOidcInterceptor({
54
- idpTokenUrl: tokensEndpoint,
55
- clientId,
56
- clientSecret,
57
- retryOnStatusCodes: [401],
58
- scope: scopes,
59
- audience,
60
- urls: origins,
61
- });
62
- requiredInterceptors.push(oidcInterceptor);
63
- }
64
-
65
- if (!isTest) {
66
- requiredInterceptors.push(
67
- interceptors.dns({ maxTTL: 300000, maxItems: 2000, dualStack: false })
68
- );
69
- }
70
-
71
- return requiredInterceptors;
72
- };
73
-
74
32
  const initHttpClient = (options) => {
75
33
  const { prefixUrl, isTest, bearerToken } = options;
76
34
 
77
35
  const { clientOptions, traceIdHeader, customHeaders } = parseOptions(options);
78
36
 
79
- // register prefixUrl
80
- if (prefixUrl) {
81
- const parsedPrefixUrl = parsePrefixUrl(prefixUrl);
82
- registeredPrefixUrls.set(prefixUrl, parsedPrefixUrl);
83
- }
37
+ const presetHost = prefixUrl != null ? parsePrefixUrl(prefixUrl) : undefined;
84
38
 
85
- let agent;
39
+ const baseAgent = isTest ? getGlobalDispatcher() : new Agent(clientOptions);
40
+ const agent = baseAgent.compose(buildInterceptorChain(presetHost, options));
86
41
 
87
- if (isTest) {
88
- const existingAgent = getGlobalDispatcher();
89
- agent = existingAgent.compose(buildInterceptors(options));
90
- } else {
91
- agent = new Agent(clientOptions).compose(buildInterceptors(options));
92
- }
42
+ const defaultReqHeaders = {
43
+ 'user-agent': USER_AGENT_HEADER,
44
+ ...customHeaders,
45
+ ...buildBearerAuthorizationHeader(bearerToken),
46
+ };
93
47
 
94
48
  const request = async (
95
49
  url,
@@ -97,16 +51,11 @@ const initHttpClient = (options) => {
97
51
  method,
98
52
  host,
99
53
  { traceId, log },
100
- body
54
+ body,
55
+ contentType
101
56
  ) => {
102
57
  const reqId = nanoid();
103
- const reqHeaders = {
104
- 'user-agent': USER_AGENT_HEADER,
105
- [traceIdHeader]: traceId,
106
- ...customHeaders,
107
- ...reqOptions?.headers,
108
- ...buildBearerAuthorizationHeader(bearerToken),
109
- };
58
+ const reqHeaders = buildReqHeaders(reqOptions, contentType, traceId);
110
59
  const [origin, path] = buildUrl(host, url, reqOptions);
111
60
 
112
61
  log.info({ origin, path, url, reqId, reqHeaders }, 'HttpClient request');
@@ -161,33 +110,57 @@ const initHttpClient = (options) => {
161
110
  }
162
111
  };
163
112
 
164
- return (...args) => {
165
- let host;
166
- let context = args[0];
167
- if (args.length === 2) {
168
- host = registeredPrefixUrls.get(args[0]) ?? parsePrefixUrl(args[0]);
169
- context = args[1];
113
+ const buildReqHeaders = (reqOptions, contentType, traceId) => {
114
+ const reqHeaders = {
115
+ ...defaultReqHeaders,
116
+ ...(reqOptions.headers ?? {}),
117
+ [traceIdHeader]: traceId,
118
+ };
119
+ if (contentType) {
120
+ reqHeaders['content-type'] = contentType;
170
121
  }
122
+ return reqHeaders;
123
+ };
171
124
 
125
+ return (...args) => {
126
+ const { host, context } = parseArgs(presetHost, args);
172
127
  return {
173
- get: (url, reqOptions) =>
128
+ get: (url, reqOptions = {}) =>
174
129
  request(url, reqOptions, HTTP_VERBS.GET, host, context),
175
- post: (url, payload, reqOptions) =>
130
+ post: (url, payload, reqOptions = {}) =>
176
131
  request(
177
132
  url,
178
133
  reqOptions,
179
134
  HTTP_VERBS.POST,
180
135
  host,
181
136
  context,
182
- isObject(payload) ? JSON.stringify(payload) : payload
137
+ isObject(payload) ? JSON.stringify(payload) : payload,
138
+ calcContentType(payload, reqOptions?.headers)
183
139
  ),
184
- delete: (url, reqOptions) =>
140
+ delete: (url, reqOptions = {}) =>
185
141
  request(url, reqOptions, HTTP_VERBS.DELETE, host, context),
186
142
  responseType: 'promise',
187
143
  };
188
144
  };
189
145
  };
190
146
 
147
+ const calcContentType = (payload, headers = {}) =>
148
+ isObject(payload) && !headers['content-type']
149
+ ? 'application/json'
150
+ : headers['content-type'];
151
+
152
+ const parseArgs = (presetHost, args) => {
153
+ if (args.length === 1) {
154
+ return { host: presetHost, context: args[0] };
155
+ }
156
+ if (args.length === 2) {
157
+ return { host: parsePrefixUrl(args[0]), context: args[1] };
158
+ }
159
+ throw new Error(
160
+ `HttpClient: Expected 1 or 2 arguments, received ${args.length}`
161
+ );
162
+ };
163
+
191
164
  const parseOptions = (options) => {
192
165
  const clientOptions = {
193
166
  connect: {
@@ -205,6 +178,47 @@ const parseOptions = (options) => {
205
178
  };
206
179
  };
207
180
 
181
+ const buildInterceptorChain = (
182
+ host,
183
+ {
184
+ isTest,
185
+ cache: cacheStore,
186
+ tokensEndpoint,
187
+ clientId,
188
+ clientSecret,
189
+ scopes,
190
+ audience,
191
+ }
192
+ ) => {
193
+ const chain = [];
194
+ if (!isTest) {
195
+ chain.push(
196
+ interceptors.dns({ maxTTL: 300000, maxItems: 2000, dualStack: false })
197
+ );
198
+ }
199
+
200
+ chain.push(interceptors.responseError());
201
+
202
+ if (cacheStore != null) {
203
+ chain.push(interceptors.cache({ store: cacheStore, methods: ['GET'] }));
204
+ }
205
+
206
+ if (tokensEndpoint && host != null) {
207
+ const oidcInterceptor = createOidcInterceptor({
208
+ idpTokenUrl: tokensEndpoint,
209
+ clientId,
210
+ clientSecret,
211
+ retryOnStatusCodes: [401],
212
+ scope: scopes,
213
+ audience,
214
+ urls: [host.origin],
215
+ });
216
+ chain.push(oidcInterceptor);
217
+ }
218
+
219
+ return chain;
220
+ };
221
+
208
222
  const parsePrefixUrl = (prefixUrl) => {
209
223
  const url = new URL(prefixUrl);
210
224
  return {
@@ -215,34 +229,45 @@ const parsePrefixUrl = (prefixUrl) => {
215
229
  };
216
230
 
217
231
  const buildUrl = (host, url, reqOptions) => {
218
- const fullUrl = reqOptions?.prefixUrl
219
- ? new URL(url, reqOptions.prefixUrl).toString()
220
- : url;
221
-
222
- return host && !reqOptions?.prefixUrl
223
- ? [host.origin, buildRelativePath(host.rootPath, url, reqOptions)]
224
- : parseFullURL(fullUrl, reqOptions);
232
+ if (/https?:\/\//.test(url)) {
233
+ return parseFullURL(url, reqOptions);
234
+ }
235
+ if (!host) {
236
+ throw new Error(
237
+ 'HttpClient: Cannot build URL without prefixUrl or full url'
238
+ );
239
+ }
240
+ return [host.origin, buildRelativePath(host.rootPath ?? '', url, reqOptions)];
225
241
  };
226
242
 
227
243
  const parseFullURL = (url, reqOptions) => {
228
244
  const { origin, pathname, searchParams } = new URL(url);
229
245
  return [
230
246
  origin,
231
- addSearchParams(pathname, reqOptions?.searchParams || `${searchParams}`),
247
+ addSearchParams(pathname, searchParams, reqOptions?.searchParams),
232
248
  ];
233
249
  };
234
250
 
235
- const buildRelativePath = (rootPath, url, reqOptions) =>
236
- addSearchParams(
237
- `${rootPath}${url.charAt[0] === '/' ? '' : '/'}${url}`,
251
+ const buildRelativePath = (rootPath, url, reqOptions) => {
252
+ const relativePath = `${rootPath}${url.charAt(0) === '/' ? '' : '/'}${url}`;
253
+ const [pathname, searchParamsString] = relativePath.split('?');
254
+
255
+ return addSearchParams(
256
+ pathname,
257
+ new URLSearchParams(searchParamsString),
238
258
  reqOptions?.searchParams
239
259
  );
260
+ };
240
261
 
241
- const addSearchParams = (path, searchParams) =>
242
- searchParams?.size || searchParams?.length ? `${path}?${searchParams}` : path;
243
-
244
- const addCache = (store) =>
245
- store ? [interceptors.cache({ store, methods: ['GET'] })] : [];
262
+ const addSearchParams = (path, searchParams, additionalParams) => {
263
+ const params = additionalParams?.size
264
+ ? new URLSearchParams([
265
+ ...Array.from(searchParams.entries()),
266
+ ...Array.from(additionalParams.entries()),
267
+ ])
268
+ : searchParams;
269
+ return params?.size ? `${path}?${params}` : path;
270
+ };
246
271
 
247
272
  const buildBearerAuthorizationHeader = (token) =>
248
273
  token ? { Authorization: `Bearer ${token}` } : {};
@@ -253,4 +278,10 @@ const HTTP_VERBS = {
253
278
  DELETE: 'DELETE',
254
279
  };
255
280
 
256
- module.exports = { initHttpClient, parseOptions, parsePrefixUrl, initCache };
281
+ module.exports = {
282
+ initHttpClient,
283
+ parseOptions,
284
+ parsePrefixUrl,
285
+ initCache,
286
+ buildUrl,
287
+ };