accessio 1.1.2 → 1.3.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/README.md +109 -14
- package/cjs/accessio.cjs +59 -3
- package/cjs/accessio.cjs.map +1 -1
- package/cjs/core/accessioError.cjs +28 -3
- package/cjs/core/accessioError.cjs.map +1 -1
- package/cjs/core/buildURL.cjs +15 -1
- package/cjs/core/buildURL.cjs.map +1 -1
- package/cjs/core/fetchAdapter.cjs +54 -10
- package/cjs/core/fetchAdapter.cjs.map +1 -1
- package/cjs/core/request.cjs +107 -31
- package/cjs/core/request.cjs.map +1 -1
- package/cjs/core/retry.cjs +48 -4
- package/cjs/core/retry.cjs.map +1 -1
- package/cjs/helpers/flattenHeaders.cjs +37 -0
- package/cjs/helpers/flattenHeaders.cjs.map +1 -1
- package/cjs/helpers/memoryCache.cjs +51 -0
- package/cjs/helpers/memoryCache.cjs.map +1 -0
- package/cjs/helpers/rateLimiter.cjs +11 -22
- package/cjs/helpers/rateLimiter.cjs.map +1 -1
- package/cjs/helpers/toFormData.cjs +50 -0
- package/cjs/helpers/toFormData.cjs.map +1 -0
- package/cjs/helpers/transformData.cjs +2 -2
- package/cjs/helpers/transformData.cjs.map +1 -1
- package/cjs/index.cjs +4 -1
- package/cjs/index.cjs.map +1 -1
- package/index.d.ts +7 -0
- package/package.json +3 -2
- package/src/accessio.ts +81 -3
- package/src/core/accessioError.ts +26 -1
- package/src/core/buildURL.ts +16 -1
- package/src/core/fetchAdapter.ts +62 -10
- package/src/core/request.ts +134 -44
- package/src/core/retry.ts +44 -6
- package/src/helpers/flattenHeaders.ts +30 -0
- package/src/helpers/memoryCache.ts +30 -0
- package/src/helpers/rateLimiter.ts +11 -24
- package/src/helpers/toFormData.ts +25 -0
- package/src/helpers/transformData.ts +2 -1
- package/src/index.ts +4 -1
- package/src/types.ts +27 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/helpers/transformData.ts"],"sourcesContent":["import AccessioError from '../core/accessioError';\nimport type { TransformFunction, AccessioRequestConfig } from '../types';\n\nexport default async function transformData(\n transforms: TransformFunction | TransformFunction[] | undefined,\n data: unknown,\n headers: Record<string, string | string[]>,\n config?: AccessioRequestConfig,\n): Promise<unknown> {\n if (!transforms || !Array.isArray(transforms)) {\n return data;\n }\n\n let result = data;\n\n for (const transform of transforms) {\n if (typeof transform === 'function') {\n try {\n result = await transform(result, headers);\n } catch (err) {\n throw AccessioError.from(\n err instanceof Error ? err : new Error(String(err)),\n AccessioError.ERR_BAD_REQUEST,\n config ?? null,\n null,\n null,\n );\n }\n }\n }\n\n return result;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,2BAA0B;AAG1B,eAAO,cACL,YACA,MACA,SACA,
|
|
1
|
+
{"version":3,"sources":["../../src/helpers/transformData.ts"],"sourcesContent":["import AccessioError from '../core/accessioError';\nimport type { TransformFunction, AccessioRequestConfig } from '../types';\n\nexport default async function transformData(\n transforms: TransformFunction | TransformFunction[] | undefined,\n data: unknown,\n headers: Record<string, string | string[]>,\n config?: AccessioRequestConfig,\n direction: 'request' | 'response' = 'request',\n): Promise<unknown> {\n if (!transforms || !Array.isArray(transforms)) {\n return data;\n }\n\n let result = data;\n\n for (const transform of transforms) {\n if (typeof transform === 'function') {\n try {\n result = await transform(result, headers);\n } catch (err) {\n throw AccessioError.from(\n err instanceof Error ? err : new Error(String(err)),\n direction === 'response' ? AccessioError.ERR_BAD_RESPONSE : AccessioError.ERR_BAD_REQUEST,\n config ?? null,\n null,\n null,\n );\n }\n }\n }\n\n return result;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,2BAA0B;AAG1B,eAAO,cACL,YACA,MACA,SACA,QACA,YAAoC,WAClB;AAClB,MAAI,CAAC,cAAc,CAAC,MAAM,QAAQ,UAAU,GAAG;AAC7C,WAAO;AAAA,EACT;AAEA,MAAI,SAAS;AAEb,aAAW,aAAa,YAAY;AAClC,QAAI,OAAO,cAAc,YAAY;AACnC,UAAI;AACF,iBAAS,MAAM,UAAU,QAAQ,OAAO;AAAA,MAC1C,SAAS,KAAK;AACZ,cAAM,qBAAAA,QAAc;AAAA,UAClB,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC;AAAA,UAClD,cAAc,aAAa,qBAAAA,QAAc,mBAAmB,qBAAAA,QAAc;AAAA,UAC1E,UAAU;AAAA,UACV;AAAA,UACA;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;","names":["AccessioError"]}
|
package/cjs/index.cjs
CHANGED
package/cjs/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import Accessio from './accessio';\nimport defaults from './defaults';\nimport AccessioError from './core/accessioError';\nimport mergeConfig from './core/mergeConfig';\nimport buildURL from './core/buildURL';\nimport InterceptorManager from './interceptors/interceptorManager';\nimport { createRateLimiter } from './helpers/rateLimiter';\nimport { logRequest, logResponse, logError } from './helpers/debug';\nimport { ERR_CANCELED } from './constants/errorCodes';\nimport type { AccessioRequestConfig
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import Accessio from './accessio';\nimport defaults from './defaults';\nimport AccessioError from './core/accessioError';\nimport mergeConfig from './core/mergeConfig';\nimport buildURL from './core/buildURL';\nimport InterceptorManager from './interceptors/interceptorManager';\nimport { createRateLimiter } from './helpers/rateLimiter';\nimport { logRequest, logResponse, logError } from './helpers/debug';\nimport { ERR_CANCELED } from './constants/errorCodes';\nimport type { AccessioRequestConfig } from './types';\n\nconst PUBLIC_METHODS = [\n 'request',\n 'getUri',\n 'get',\n 'delete',\n 'head',\n 'options',\n 'post',\n 'put',\n 'patch',\n 'postForm',\n 'putForm',\n 'patchForm',\n 'stream',\n 'autoPaginate',\n 'gql',\n];\n\nfunction createInstance(defaultConfig: AccessioRequestConfig) {\n const context = new Accessio(defaultConfig);\n\n const instance: any = function accessio(\n configOrUrl: string | AccessioRequestConfig,\n config?: AccessioRequestConfig,\n ) {\n return context.request(configOrUrl, config);\n };\n\n for (const key of PUBLIC_METHODS) {\n const method: any = (context as any)[key];\n if (typeof method === 'function') {\n instance[key] = method.bind(context);\n }\n }\n\n instance.defaults = context.defaults;\n instance.interceptors = context.interceptors;\n instance.all = function all(promises: any[]): Promise<any[]> {\n return Promise.all(promises);\n };\n instance.spread = function spread<T>(callback: (...args: any[]) => T): (arr: any[]) => T {\n return function wrap(arr: any[]): T {\n return callback(...arr);\n };\n };\n instance.isCancel = function isCancel(value: any): boolean {\n return !!(value && value.isAccessioError && value.code === ERR_CANCELED);\n };\n instance.isAccessioError = function isAccessioError(value: any): boolean {\n return (\n value instanceof AccessioError ||\n !!(value && typeof value === 'object' && value.isAccessioError === true)\n );\n };\n instance.AccessioError = AccessioError;\n instance.Accessio = Accessio;\n instance.mergeConfig = mergeConfig;\n instance.buildURL = buildURL;\n instance.InterceptorManager = InterceptorManager;\n instance.createRateLimiter = createRateLimiter;\n\n return instance;\n}\n\nconst accessio = createInstance(defaults);\n\nfunction create(instanceConfig?: AccessioRequestConfig) {\n return createInstance(mergeConfig(defaults, instanceConfig));\n}\n\naccessio.create = create;\n\nexport default accessio;\n\nexport {\n Accessio,\n AccessioError,\n mergeConfig,\n buildURL,\n InterceptorManager,\n createInstance,\n createRateLimiter,\n logRequest,\n logResponse,\n logError,\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA,kCAAAA;AAAA,EAAA,0CAAAC;AAAA,EAAA,oDAAAC;AAAA,EAAA,gCAAAC;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,wCAAAC;AAAA;AAAA;AAAA,sBAAqB;AACrB,sBAAqB;AACrB,2BAA0B;AAC1B,yBAAwB;AACxB,sBAAqB;AACrB,gCAA+B;AAC/B,yBAAkC;AAClC,mBAAkD;AAClD,wBAA6B;AAG7B,MAAM,iBAAiB;AAAA,EACrB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,SAAS,eAAe,eAAsC;AAC5D,QAAM,UAAU,IAAI,gBAAAJ,QAAS,aAAa;AAE1C,QAAM,WAAgB,SAASK,UAC7B,aACA,QACA;AACA,WAAO,QAAQ,QAAQ,aAAa,MAAM;AAAA,EAC5C;AAEA,aAAW,OAAO,gBAAgB;AAChC,UAAM,SAAe,QAAgB,GAAG;AACxC,QAAI,OAAO,WAAW,YAAY;AAChC,eAAS,GAAG,IAAI,OAAO,KAAK,OAAO;AAAA,IACrC;AAAA,EACF;AAEA,WAAS,WAAW,QAAQ;AAC5B,WAAS,eAAe,QAAQ;AAChC,WAAS,MAAM,SAAS,IAAI,UAAiC;AAC3D,WAAO,QAAQ,IAAI,QAAQ;AAAA,EAC7B;AACA,WAAS,SAAS,SAAS,OAAU,UAAoD;AACvF,WAAO,SAAS,KAAK,KAAe;AAClC,aAAO,SAAS,GAAG,GAAG;AAAA,IACxB;AAAA,EACF;AACA,WAAS,WAAW,SAAS,SAAS,OAAqB;AACzD,WAAO,CAAC,EAAE,SAAS,MAAM,mBAAmB,MAAM,SAAS;AAAA,EAC7D;AACA,WAAS,kBAAkB,SAAS,gBAAgB,OAAqB;AACvE,WACE,iBAAiB,qBAAAJ,WACjB,CAAC,EAAE,SAAS,OAAO,UAAU,YAAY,MAAM,oBAAoB;AAAA,EAEvE;AACA,WAAS,gBAAgB,qBAAAA;AACzB,WAAS,WAAW,gBAAAD;AACpB,WAAS,cAAc,mBAAAI;AACvB,WAAS,WAAW,gBAAAD;AACpB,WAAS,qBAAqB,0BAAAD;AAC9B,WAAS,oBAAoB;AAE7B,SAAO;AACT;AAEA,MAAM,WAAW,eAAe,gBAAAI,OAAQ;AAExC,SAAS,OAAO,gBAAwC;AACtD,SAAO,mBAAe,mBAAAF,SAAY,gBAAAE,SAAU,cAAc,CAAC;AAC7D;AAEA,SAAS,SAAS;AAElB,IAAO,cAAQ;","names":["Accessio","AccessioError","InterceptorManager","buildURL","mergeConfig","accessio","defaults"]}
|
package/index.d.ts
CHANGED
|
@@ -55,6 +55,13 @@ export interface AccessioRequestConfig {
|
|
|
55
55
|
/** Include credentials in cross-site requests */
|
|
56
56
|
withCredentials?: boolean;
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* URL protocols accepted by the client. Defaults to `["http:", "https:"]`.
|
|
60
|
+
* Pass an extended array to allow more (e.g. `["http:", "https:", "ws:"]`),
|
|
61
|
+
* or `null` to disable the check entirely.
|
|
62
|
+
*/
|
|
63
|
+
allowedProtocols?: string[] | null;
|
|
64
|
+
|
|
58
65
|
/** Expected response data type */
|
|
59
66
|
responseType?: "json" | "text" | "blob" | "arraybuffer" | "stream";
|
|
60
67
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "accessio",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Fast, flexible HTTP client — simple, modular, and dependency-free",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./cjs/index.cjs",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"./core/buildURL": {
|
|
30
30
|
"types": "./index.d.ts",
|
|
31
31
|
"import": "./src/core/buildURL.ts",
|
|
32
|
-
"require": "./cjs/
|
|
32
|
+
"require": "./cjs/core/buildURL.cjs"
|
|
33
33
|
},
|
|
34
34
|
"./core/mergeConfig": {
|
|
35
35
|
"types": "./index.d.ts",
|
|
@@ -103,6 +103,7 @@
|
|
|
103
103
|
"prettier": "^3.8.3",
|
|
104
104
|
"tsup": "^8.0.0",
|
|
105
105
|
"typescript": "^5.0.0",
|
|
106
|
+
"typescript-eslint": "^8.59.3",
|
|
106
107
|
"vitest": "^3.1.0"
|
|
107
108
|
}
|
|
108
109
|
}
|
package/src/accessio.ts
CHANGED
|
@@ -6,6 +6,7 @@ import buildURL from './core/buildURL';
|
|
|
6
6
|
import retryRequest from './core/retry';
|
|
7
7
|
import { logRequest, logResponse, logError } from './helpers/debug';
|
|
8
8
|
import { rateLimitedRequest } from './helpers/rateLimiter';
|
|
9
|
+
import { toFormData } from './helpers/toFormData';
|
|
9
10
|
import type {
|
|
10
11
|
AccessioRequestConfig,
|
|
11
12
|
AccessioResponse,
|
|
@@ -201,11 +202,12 @@ export class Accessio {
|
|
|
201
202
|
data?: any,
|
|
202
203
|
config?: AccessioRequestConfig,
|
|
203
204
|
): Promise<AccessioResponse<T>> {
|
|
205
|
+
const formData = data && !(data instanceof FormData) ? toFormData(data) : data;
|
|
204
206
|
return this.request<T>(
|
|
205
207
|
mergeConfig(config || {}, {
|
|
206
208
|
method: 'post',
|
|
207
209
|
url,
|
|
208
|
-
data,
|
|
210
|
+
data: formData,
|
|
209
211
|
headers: { 'Content-Type': 'multipart/form-data' },
|
|
210
212
|
}),
|
|
211
213
|
);
|
|
@@ -216,11 +218,12 @@ export class Accessio {
|
|
|
216
218
|
data?: any,
|
|
217
219
|
config?: AccessioRequestConfig,
|
|
218
220
|
): Promise<AccessioResponse<T>> {
|
|
221
|
+
const formData = data && !(data instanceof FormData) ? toFormData(data) : data;
|
|
219
222
|
return this.request<T>(
|
|
220
223
|
mergeConfig(config || {}, {
|
|
221
224
|
method: 'put',
|
|
222
225
|
url,
|
|
223
|
-
data,
|
|
226
|
+
data: formData,
|
|
224
227
|
headers: { 'Content-Type': 'multipart/form-data' },
|
|
225
228
|
}),
|
|
226
229
|
);
|
|
@@ -231,15 +234,90 @@ export class Accessio {
|
|
|
231
234
|
data?: any,
|
|
232
235
|
config?: AccessioRequestConfig,
|
|
233
236
|
): Promise<AccessioResponse<T>> {
|
|
237
|
+
const formData = data && !(data instanceof FormData) ? toFormData(data) : data;
|
|
234
238
|
return this.request<T>(
|
|
235
239
|
mergeConfig(config || {}, {
|
|
236
240
|
method: 'patch',
|
|
237
241
|
url,
|
|
238
|
-
data,
|
|
242
|
+
data: formData,
|
|
239
243
|
headers: { 'Content-Type': 'multipart/form-data' },
|
|
240
244
|
}),
|
|
241
245
|
);
|
|
242
246
|
}
|
|
247
|
+
|
|
248
|
+
async *stream<T = any>(
|
|
249
|
+
url: string,
|
|
250
|
+
config?: AccessioRequestConfig,
|
|
251
|
+
): AsyncGenerator<T, void, unknown> {
|
|
252
|
+
const response = await this.request<ReadableStream<Uint8Array>>(
|
|
253
|
+
mergeConfig(config || {}, { method: 'get', url, responseType: 'stream' }),
|
|
254
|
+
);
|
|
255
|
+
if (!response.data) return;
|
|
256
|
+
|
|
257
|
+
const reader = response.data.getReader();
|
|
258
|
+
const decoder = new TextDecoder();
|
|
259
|
+
let buffer = '';
|
|
260
|
+
|
|
261
|
+
while (true) {
|
|
262
|
+
const { done, value } = await reader.read();
|
|
263
|
+
if (done) break;
|
|
264
|
+
|
|
265
|
+
buffer += decoder.decode(value, { stream: true });
|
|
266
|
+
const lines = buffer.split('\n');
|
|
267
|
+
buffer = lines.pop() || '';
|
|
268
|
+
|
|
269
|
+
for (const line of lines) {
|
|
270
|
+
if (line.trim().startsWith('data:')) {
|
|
271
|
+
const dataStr = line.replace(/^data:\s*/, '');
|
|
272
|
+
if (dataStr === '[DONE]') return;
|
|
273
|
+
try {
|
|
274
|
+
yield JSON.parse(dataStr);
|
|
275
|
+
} catch (e) {
|
|
276
|
+
yield dataStr as any;
|
|
277
|
+
}
|
|
278
|
+
} else if (line.trim().startsWith('{') || line.trim().startsWith('[')) {
|
|
279
|
+
try {
|
|
280
|
+
yield JSON.parse(line);
|
|
281
|
+
} catch (e) {
|
|
282
|
+
// ignore partial json
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async *autoPaginate<T = any>(
|
|
290
|
+
url: string,
|
|
291
|
+
config?: AccessioRequestConfig,
|
|
292
|
+
): AsyncGenerator<T, void, unknown> {
|
|
293
|
+
let nextUrl: string | null = url;
|
|
294
|
+
let currentConfig = config || {};
|
|
295
|
+
|
|
296
|
+
while (nextUrl) {
|
|
297
|
+
const response: AccessioResponse<any> = await this.get(nextUrl, currentConfig);
|
|
298
|
+
|
|
299
|
+
const items = Array.isArray(response.data) ? response.data : response.data.data;
|
|
300
|
+
if (Array.isArray(items)) {
|
|
301
|
+
for (const item of items) {
|
|
302
|
+
yield item;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
nextUrl = response.data.next || response.data.links?.next || null;
|
|
307
|
+
if (nextUrl) {
|
|
308
|
+
currentConfig = mergeConfig(currentConfig, { url: nextUrl, params: {} });
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
gql<T = any>(
|
|
314
|
+
url: string,
|
|
315
|
+
query: string,
|
|
316
|
+
variables?: Record<string, any>,
|
|
317
|
+
config?: AccessioRequestConfig,
|
|
318
|
+
): Promise<AccessioResponse<T>> {
|
|
319
|
+
return this.post<T>(url, { query, variables }, config);
|
|
320
|
+
}
|
|
243
321
|
}
|
|
244
322
|
|
|
245
323
|
export default Accessio;
|
|
@@ -1,6 +1,30 @@
|
|
|
1
1
|
import ErrorCodes from '../constants/errorCodes';
|
|
2
2
|
import type { AccessioRequestConfig, AccessioResponse } from '../types';
|
|
3
3
|
|
|
4
|
+
function redactHeaders(headers: unknown): unknown {
|
|
5
|
+
if (!headers || typeof headers !== 'object') return headers;
|
|
6
|
+
const out: Record<string, unknown> = {};
|
|
7
|
+
for (const key of Object.keys(headers as Record<string, unknown>)) {
|
|
8
|
+
const value = (headers as Record<string, unknown>)[key];
|
|
9
|
+
if (/^authorization$/i.test(key)) {
|
|
10
|
+
out[key] = '[REDACTED]';
|
|
11
|
+
} else if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
12
|
+
out[key] = redactHeaders(value);
|
|
13
|
+
} else {
|
|
14
|
+
out[key] = value;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return out;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function redactConfig(config: AccessioRequestConfig | null): AccessioRequestConfig | null {
|
|
21
|
+
if (!config) return config;
|
|
22
|
+
const clone = { ...config } as AccessioRequestConfig & { auth?: unknown };
|
|
23
|
+
if ('auth' in clone) delete clone.auth;
|
|
24
|
+
if (clone.headers) clone.headers = redactHeaders(clone.headers) as typeof clone.headers;
|
|
25
|
+
return clone;
|
|
26
|
+
}
|
|
27
|
+
|
|
4
28
|
export class AccessioError extends Error {
|
|
5
29
|
static ERR_BAD_OPTION_VALUE: string = ErrorCodes.ERR_BAD_OPTION_VALUE;
|
|
6
30
|
static ERR_BAD_OPTION: string = ErrorCodes.ERR_BAD_OPTION;
|
|
@@ -20,6 +44,7 @@ export class AccessioError extends Error {
|
|
|
20
44
|
readonly response: AccessioResponse | null;
|
|
21
45
|
readonly isAccessioError: true;
|
|
22
46
|
cause?: Error;
|
|
47
|
+
override name = 'AccessioError' as const;
|
|
23
48
|
|
|
24
49
|
constructor(
|
|
25
50
|
message: string,
|
|
@@ -31,7 +56,7 @@ export class AccessioError extends Error {
|
|
|
31
56
|
super(message);
|
|
32
57
|
this.name = 'AccessioError';
|
|
33
58
|
this.code = code ?? null;
|
|
34
|
-
this.config = config ?? null;
|
|
59
|
+
this.config = redactConfig(config ?? null);
|
|
35
60
|
this.request = request ?? null;
|
|
36
61
|
this.response = response ?? null;
|
|
37
62
|
this.isAccessioError = true;
|
package/src/core/buildURL.ts
CHANGED
|
@@ -75,7 +75,22 @@ export default function buildURL(
|
|
|
75
75
|
): string {
|
|
76
76
|
let fullURL = baseURL && !isAbsoluteURL(url) ? combineURLs(baseURL, url) : url || '';
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
let finalParams = params;
|
|
79
|
+
if (params && typeof params === 'object') {
|
|
80
|
+
const unusedParams = { ...params };
|
|
81
|
+
fullURL = fullURL.replace(/(?::([a-zA-Z0-9_]+))|(?:{([a-zA-Z0-9_]+)})/g, (match, p1, p2) => {
|
|
82
|
+
const key = p1 || p2;
|
|
83
|
+
if (key && unusedParams[key] !== undefined) {
|
|
84
|
+
const val = unusedParams[key];
|
|
85
|
+
delete unusedParams[key];
|
|
86
|
+
return encodeURIComponent(String(val));
|
|
87
|
+
}
|
|
88
|
+
return match;
|
|
89
|
+
});
|
|
90
|
+
finalParams = unusedParams;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const serialized = serializeParams(finalParams as Record<string, unknown>, paramsSerializer);
|
|
79
94
|
if (serialized) {
|
|
80
95
|
const hashIndex = fullURL.indexOf('#');
|
|
81
96
|
if (hashIndex !== -1) {
|
package/src/core/fetchAdapter.ts
CHANGED
|
@@ -2,7 +2,11 @@ import AccessioError from './accessioError';
|
|
|
2
2
|
import parseHeaders from '../helpers/parseHeaders';
|
|
3
3
|
import type { AccessioRequestConfig, AccessioResponse } from '../types';
|
|
4
4
|
|
|
5
|
-
async function readResponseData(
|
|
5
|
+
async function readResponseData(
|
|
6
|
+
fetchResponse: Response,
|
|
7
|
+
config: AccessioRequestConfig,
|
|
8
|
+
): Promise<unknown> {
|
|
9
|
+
const responseType = config.responseType || 'json';
|
|
6
10
|
switch (responseType) {
|
|
7
11
|
case 'arraybuffer':
|
|
8
12
|
return await fetchResponse.arrayBuffer();
|
|
@@ -14,12 +18,20 @@ async function readResponseData(fetchResponse: Response, responseType: string):
|
|
|
14
18
|
default: {
|
|
15
19
|
const contentType = fetchResponse.headers.get('content-type') || '';
|
|
16
20
|
if (contentType.includes('application/json')) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
const text = await fetchResponse.text();
|
|
22
|
+
if (!text) return '';
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(text);
|
|
25
|
+
} catch (err) {
|
|
26
|
+
throw new AccessioError(
|
|
27
|
+
`Failed to parse JSON response: ${(err as Error).message}. Raw body: ${
|
|
28
|
+
text.length > 500 ? text.slice(0, 500) + '…' : text
|
|
29
|
+
}`,
|
|
30
|
+
AccessioError.ERR_BAD_RESPONSE,
|
|
31
|
+
config,
|
|
32
|
+
fetchResponse,
|
|
33
|
+
null,
|
|
34
|
+
);
|
|
23
35
|
}
|
|
24
36
|
}
|
|
25
37
|
return await fetchResponse.text();
|
|
@@ -94,10 +106,42 @@ export default async function fetchAdapter(
|
|
|
94
106
|
}
|
|
95
107
|
|
|
96
108
|
try {
|
|
97
|
-
const
|
|
109
|
+
const fetchImpl = config.fetch || fetch;
|
|
110
|
+
let fetchResponse = await fetchImpl(fullURL, fetchOptions);
|
|
111
|
+
|
|
112
|
+
if (config.onDownloadProgress && fetchResponse.body && config.responseType !== 'stream') {
|
|
113
|
+
const contentLength = fetchResponse.headers.get('content-length');
|
|
114
|
+
const total = contentLength ? parseInt(contentLength, 10) : 0;
|
|
115
|
+
let loaded = 0;
|
|
116
|
+
|
|
117
|
+
const reader = fetchResponse.body.getReader();
|
|
118
|
+
const stream = new ReadableStream({
|
|
119
|
+
async start(controller) {
|
|
120
|
+
try {
|
|
121
|
+
while (true) {
|
|
122
|
+
const { done, value } = await reader.read();
|
|
123
|
+
if (done) {
|
|
124
|
+
controller.close();
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
loaded += value.byteLength;
|
|
128
|
+
config.onDownloadProgress!({ loaded, total });
|
|
129
|
+
controller.enqueue(value);
|
|
130
|
+
}
|
|
131
|
+
} catch (e) {
|
|
132
|
+
controller.error(e);
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
fetchResponse = new Response(stream, {
|
|
138
|
+
headers: fetchResponse.headers,
|
|
139
|
+
status: fetchResponse.status,
|
|
140
|
+
statusText: fetchResponse.statusText,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
98
143
|
|
|
99
144
|
let responseData: unknown;
|
|
100
|
-
const responseType = config.responseType || 'json';
|
|
101
145
|
|
|
102
146
|
const contentLength = fetchResponse.headers.get('content-length');
|
|
103
147
|
if (
|
|
@@ -115,8 +159,16 @@ export default async function fetchAdapter(
|
|
|
115
159
|
}
|
|
116
160
|
|
|
117
161
|
try {
|
|
118
|
-
responseData = await readResponseData(fetchResponse,
|
|
162
|
+
responseData = await readResponseData(fetchResponse, config);
|
|
163
|
+
if (config.schema) {
|
|
164
|
+
if (typeof config.schema.parseAsync === 'function') {
|
|
165
|
+
responseData = await config.schema.parseAsync(responseData);
|
|
166
|
+
} else {
|
|
167
|
+
responseData = config.schema.parse(responseData);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
119
170
|
} catch (readError) {
|
|
171
|
+
if (readError instanceof AccessioError) throw readError;
|
|
120
172
|
throw AccessioError.from(
|
|
121
173
|
readError as Error,
|
|
122
174
|
AccessioError.ERR_BAD_RESPONSE,
|
package/src/core/request.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import buildURL from './buildURL';
|
|
2
|
-
import AccessioError from './accessioError';
|
|
2
|
+
import AccessioError, { redactConfig } from './accessioError';
|
|
3
|
+
import { ERR_BAD_OPTION } from '../constants/errorCodes';
|
|
3
4
|
import transformData from '../helpers/transformData';
|
|
4
5
|
import settle from '../helpers/settle';
|
|
5
6
|
import { flattenHeaders, removeContentType, buildFetchHeaders } from '../helpers/flattenHeaders';
|
|
6
7
|
import { setBasicAuth } from '../helpers/auth';
|
|
7
8
|
import fetchAdapter from './fetchAdapter';
|
|
9
|
+
import { defaultMemoryCache } from '../helpers/memoryCache';
|
|
8
10
|
import type { AccessioRequestConfig, AccessioResponse, TransformFunction } from '../types';
|
|
9
11
|
|
|
10
12
|
type HeadersConfig = Record<string, Record<string, string | string[]>>;
|
|
@@ -17,6 +19,31 @@ function buildTransformArray(
|
|
|
17
19
|
return [transform];
|
|
18
20
|
}
|
|
19
21
|
|
|
22
|
+
const DEFAULT_ALLOWED_PROTOCOLS = ['http:', 'https:'];
|
|
23
|
+
|
|
24
|
+
function assertAllowedProtocol(fullURL: string, config: AccessioRequestConfig): void {
|
|
25
|
+
if (config.allowedProtocols === null) return;
|
|
26
|
+
const allowed = config.allowedProtocols ?? DEFAULT_ALLOWED_PROTOCOLS;
|
|
27
|
+
|
|
28
|
+
let scheme: string | null = null;
|
|
29
|
+
const match = /^([a-z][a-z\d+\-.]*):/i.exec(fullURL);
|
|
30
|
+
if (match) scheme = match[1].toLowerCase() + ':';
|
|
31
|
+
if (!scheme) return;
|
|
32
|
+
|
|
33
|
+
if (!allowed.includes(scheme)) {
|
|
34
|
+
throw new AccessioError(
|
|
35
|
+
`URL protocol "${scheme}" is not allowed. Allowed: ${allowed.join(', ')}. ` +
|
|
36
|
+
'Set config.allowedProtocols to extend, or null to disable the check.',
|
|
37
|
+
ERR_BAD_OPTION,
|
|
38
|
+
config,
|
|
39
|
+
null,
|
|
40
|
+
null,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const activeRequests = new Map<string, Promise<AccessioResponse>>();
|
|
46
|
+
|
|
20
47
|
export default async function dispatchRequest(
|
|
21
48
|
config: AccessioRequestConfig,
|
|
22
49
|
): Promise<AccessioResponse> {
|
|
@@ -29,61 +56,124 @@ export default async function dispatchRequest(
|
|
|
29
56
|
config.paramsSerializer,
|
|
30
57
|
);
|
|
31
58
|
|
|
32
|
-
|
|
59
|
+
assertAllowedProtocol(fullURL, config);
|
|
33
60
|
|
|
34
|
-
|
|
61
|
+
if (config.hooks?.onBeforeRequest) {
|
|
62
|
+
await config.hooks.onBeforeRequest(config);
|
|
63
|
+
}
|
|
35
64
|
|
|
36
|
-
const
|
|
65
|
+
const isGet = (config.method || 'GET').toUpperCase() === 'GET';
|
|
66
|
+
const cacheKey = isGet ? `GET:${fullURL}` : '';
|
|
67
|
+
|
|
68
|
+
if (isGet && config.cache) {
|
|
69
|
+
const cacheProvider = typeof config.cache === 'object' ? config.cache : defaultMemoryCache;
|
|
70
|
+
const cached = await cacheProvider.get(cacheKey);
|
|
71
|
+
if (cached) {
|
|
72
|
+
if (config.hooks?.onRequestResponse) {
|
|
73
|
+
await config.hooks.onRequestResponse(cached);
|
|
74
|
+
}
|
|
75
|
+
return cached;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
37
78
|
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
) {
|
|
43
|
-
removeContentType(flatHeaders);
|
|
79
|
+
if (isGet && config.dedupe) {
|
|
80
|
+
if (activeRequests.has(cacheKey)) {
|
|
81
|
+
return activeRequests.get(cacheKey)!;
|
|
82
|
+
}
|
|
44
83
|
}
|
|
45
84
|
|
|
46
|
-
|
|
85
|
+
const performRequest = async () => {
|
|
86
|
+
const flatHeaders = flattenHeaders(config.headers as HeadersConfig | undefined, config.method);
|
|
87
|
+
const requestTransforms = buildTransformArray(config.transformRequest);
|
|
88
|
+
const requestData = await transformData(requestTransforms, config.data, flatHeaders, config);
|
|
89
|
+
|
|
90
|
+
if (
|
|
91
|
+
requestData === null ||
|
|
92
|
+
requestData === undefined ||
|
|
93
|
+
(typeof FormData !== 'undefined' && requestData instanceof FormData)
|
|
94
|
+
) {
|
|
95
|
+
removeContentType(flatHeaders);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
setBasicAuth(config, flatHeaders);
|
|
99
|
+
|
|
100
|
+
const fetchOptions: RequestInit = {
|
|
101
|
+
method: (config.method || 'GET').toUpperCase(),
|
|
102
|
+
headers: buildFetchHeaders(flatHeaders),
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const methodsWithBody = ['POST', 'PUT', 'PATCH', 'DELETE'];
|
|
106
|
+
if (
|
|
107
|
+
methodsWithBody.includes(fetchOptions.method!) &&
|
|
108
|
+
requestData !== undefined &&
|
|
109
|
+
requestData !== null
|
|
110
|
+
) {
|
|
111
|
+
fetchOptions.body = requestData as BodyInit;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (config.withCredentials) {
|
|
115
|
+
fetchOptions.credentials = 'include';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (config.dispatcher) {
|
|
119
|
+
(fetchOptions as any).dispatcher = config.dispatcher;
|
|
120
|
+
}
|
|
121
|
+
if (config.agent) {
|
|
122
|
+
(fetchOptions as any).agent = config.agent;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const requestStartTime = Date.now();
|
|
126
|
+
|
|
127
|
+
const response = await fetchAdapter(config, fullURL, fetchOptions, requestStartTime);
|
|
128
|
+
response.config = redactConfig(response.config) as typeof response.config;
|
|
129
|
+
|
|
130
|
+
const responseTransforms = buildTransformArray(config.transformResponse);
|
|
131
|
+
|
|
132
|
+
response.data = await transformData(
|
|
133
|
+
responseTransforms,
|
|
134
|
+
response.data,
|
|
135
|
+
response.headers,
|
|
136
|
+
config,
|
|
137
|
+
'response',
|
|
138
|
+
);
|
|
47
139
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
140
|
+
return new Promise<AccessioResponse>((resolve, reject) => {
|
|
141
|
+
settle(
|
|
142
|
+
resolve as (value: AccessioResponse) => void,
|
|
143
|
+
reject as (reason: AccessioError) => void,
|
|
144
|
+
response,
|
|
145
|
+
config,
|
|
146
|
+
);
|
|
147
|
+
});
|
|
51
148
|
};
|
|
52
149
|
|
|
53
|
-
const
|
|
54
|
-
if (
|
|
55
|
-
methodsWithBody.includes(fetchOptions.method!) &&
|
|
56
|
-
requestData !== undefined &&
|
|
57
|
-
requestData !== null
|
|
58
|
-
) {
|
|
59
|
-
fetchOptions.body = requestData as BodyInit;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (config.withCredentials) {
|
|
63
|
-
fetchOptions.credentials = 'include';
|
|
64
|
-
}
|
|
150
|
+
const promise = performRequest();
|
|
65
151
|
|
|
66
|
-
if (config.
|
|
67
|
-
(
|
|
152
|
+
if (isGet && config.dedupe) {
|
|
153
|
+
activeRequests.set(cacheKey, promise);
|
|
154
|
+
const cleanup = () => {
|
|
155
|
+
activeRequests.delete(cacheKey);
|
|
156
|
+
};
|
|
157
|
+
promise.then(cleanup, cleanup);
|
|
68
158
|
}
|
|
69
|
-
if (config.agent) {
|
|
70
|
-
(fetchOptions as any).agent = config.agent;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const requestStartTime = Date.now();
|
|
74
159
|
|
|
75
|
-
|
|
160
|
+
try {
|
|
161
|
+
const response = await promise;
|
|
76
162
|
|
|
77
|
-
|
|
163
|
+
if (isGet && config.cache) {
|
|
164
|
+
const cacheProvider = typeof config.cache === 'object' ? config.cache : defaultMemoryCache;
|
|
165
|
+
await cacheProvider.set(cacheKey, response, config.cacheTTL);
|
|
166
|
+
}
|
|
78
167
|
|
|
79
|
-
|
|
168
|
+
if (config.hooks?.onRequestResponse) {
|
|
169
|
+
await config.hooks.onRequestResponse(response);
|
|
170
|
+
}
|
|
80
171
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
});
|
|
172
|
+
return response;
|
|
173
|
+
} catch (error) {
|
|
174
|
+
if (config.hooks?.onRequestError && error instanceof AccessioError) {
|
|
175
|
+
await config.hooks.onRequestError(error);
|
|
176
|
+
}
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
89
179
|
}
|