@vertz/fetch 0.1.0 → 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/dist/index.d.ts +55 -16
- package/dist/index.js +295 -153
- package/package.json +5 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { EntityErrorType, FetchErrorType, Result as Result3 } from "@vertz/errors";
|
|
2
|
+
import { err, isErr, isOk, matchError, ok, unwrap, unwrapOr } from "@vertz/errors";
|
|
3
|
+
import { FetchError as BaseFetchError, Result } from "@vertz/errors";
|
|
1
4
|
type AuthStrategy = {
|
|
2
5
|
type: "bearer";
|
|
3
6
|
token: string | (() => string | Promise<string>);
|
|
@@ -47,19 +50,39 @@ interface RequestOptions {
|
|
|
47
50
|
body?: unknown;
|
|
48
51
|
signal?: AbortSignal;
|
|
49
52
|
}
|
|
50
|
-
|
|
53
|
+
type FetchResponse<T> = Result<{
|
|
51
54
|
data: T;
|
|
52
55
|
status: number;
|
|
53
56
|
headers: Headers;
|
|
54
|
-
}
|
|
57
|
+
}, BaseFetchError>;
|
|
55
58
|
interface StreamingRequestOptions extends RequestOptions {
|
|
56
59
|
format: StreamingFormat;
|
|
57
60
|
}
|
|
61
|
+
/** Paginated list response envelope returned by entity list endpoints. */
|
|
62
|
+
interface ListResponse<T> {
|
|
63
|
+
items: T[];
|
|
64
|
+
total: number;
|
|
65
|
+
limit: number;
|
|
66
|
+
nextCursor: string | null;
|
|
67
|
+
hasNextPage: boolean;
|
|
68
|
+
}
|
|
58
69
|
declare class FetchClient {
|
|
59
70
|
private readonly config;
|
|
60
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Custom fetch provided via config, bound to globalThis.
|
|
73
|
+
* When not provided, globalThis.fetch is read at call time — this allows
|
|
74
|
+
* SSR handlers to patch globalThis.fetch for relative URL resolution
|
|
75
|
+
* without the client capturing a stale reference at construction time.
|
|
76
|
+
*/
|
|
77
|
+
private readonly customFetchFn;
|
|
61
78
|
constructor(config: FetchClientConfig);
|
|
79
|
+
private get fetchFn();
|
|
62
80
|
request<T>(method: string, path: string, options?: RequestOptions): Promise<FetchResponse<T>>;
|
|
81
|
+
get<T>(path: string, options?: RequestOptions): Promise<FetchResponse<T>>;
|
|
82
|
+
post<T>(path: string, body?: unknown, options?: RequestOptions): Promise<FetchResponse<T>>;
|
|
83
|
+
put<T>(path: string, body?: unknown, options?: RequestOptions): Promise<FetchResponse<T>>;
|
|
84
|
+
patch<T>(path: string, body?: unknown, options?: RequestOptions): Promise<FetchResponse<T>>;
|
|
85
|
+
delete<T>(path: string, options?: RequestOptions): Promise<FetchResponse<T>>;
|
|
63
86
|
requestStream<T>(options: StreamingRequestOptions & {
|
|
64
87
|
method: string;
|
|
65
88
|
path: string;
|
|
@@ -75,40 +98,56 @@ declare class FetchClient {
|
|
|
75
98
|
private applyStrategy;
|
|
76
99
|
private safeParseJSON;
|
|
77
100
|
}
|
|
78
|
-
|
|
101
|
+
import { FetchError, Result as Result2 } from "@vertz/errors";
|
|
102
|
+
interface QueryDescriptor<
|
|
103
|
+
T,
|
|
104
|
+
E = FetchError
|
|
105
|
+
> extends PromiseLike<Result2<T, E>> {
|
|
106
|
+
readonly _tag: "QueryDescriptor";
|
|
107
|
+
readonly _key: string;
|
|
108
|
+
readonly _fetch: () => Promise<Result2<T, E>>;
|
|
109
|
+
/** Phantom field to carry the error type through generics. Never set at runtime. */
|
|
110
|
+
readonly _error?: E;
|
|
111
|
+
}
|
|
112
|
+
declare function isQueryDescriptor<
|
|
113
|
+
T,
|
|
114
|
+
E = FetchError
|
|
115
|
+
>(value: unknown): value is QueryDescriptor<T, E>;
|
|
116
|
+
declare function createDescriptor<T>(method: string, path: string, fetchFn: () => Promise<FetchResponse<T>>, query?: Record<string, unknown>): QueryDescriptor<T>;
|
|
117
|
+
declare class FetchError2 extends Error {
|
|
79
118
|
readonly status: number;
|
|
80
119
|
readonly body?: unknown;
|
|
81
120
|
constructor(message: string, status: number, body?: unknown);
|
|
82
121
|
}
|
|
83
|
-
declare class BadRequestError extends
|
|
122
|
+
declare class BadRequestError extends FetchError2 {
|
|
84
123
|
constructor(message: string, body?: unknown);
|
|
85
124
|
}
|
|
86
|
-
declare class UnauthorizedError extends
|
|
125
|
+
declare class UnauthorizedError extends FetchError2 {
|
|
87
126
|
constructor(message: string, body?: unknown);
|
|
88
127
|
}
|
|
89
|
-
declare class ForbiddenError extends
|
|
128
|
+
declare class ForbiddenError extends FetchError2 {
|
|
90
129
|
constructor(message: string, body?: unknown);
|
|
91
130
|
}
|
|
92
|
-
declare class NotFoundError extends
|
|
131
|
+
declare class NotFoundError extends FetchError2 {
|
|
93
132
|
constructor(message: string, body?: unknown);
|
|
94
133
|
}
|
|
95
|
-
declare class ConflictError extends
|
|
134
|
+
declare class ConflictError extends FetchError2 {
|
|
96
135
|
constructor(message: string, body?: unknown);
|
|
97
136
|
}
|
|
98
|
-
declare class GoneError extends
|
|
137
|
+
declare class GoneError extends FetchError2 {
|
|
99
138
|
constructor(message: string, body?: unknown);
|
|
100
139
|
}
|
|
101
|
-
declare class UnprocessableEntityError extends
|
|
140
|
+
declare class UnprocessableEntityError extends FetchError2 {
|
|
102
141
|
constructor(message: string, body?: unknown);
|
|
103
142
|
}
|
|
104
|
-
declare class RateLimitError extends
|
|
143
|
+
declare class RateLimitError extends FetchError2 {
|
|
105
144
|
constructor(message: string, body?: unknown);
|
|
106
145
|
}
|
|
107
|
-
declare class InternalServerError extends
|
|
146
|
+
declare class InternalServerError extends FetchError2 {
|
|
108
147
|
constructor(message: string, body?: unknown);
|
|
109
148
|
}
|
|
110
|
-
declare class ServiceUnavailableError extends
|
|
149
|
+
declare class ServiceUnavailableError extends FetchError2 {
|
|
111
150
|
constructor(message: string, body?: unknown);
|
|
112
151
|
}
|
|
113
|
-
declare function createErrorFromStatus(status: number, message: string, body?: unknown):
|
|
114
|
-
export { createErrorFromStatus, UnprocessableEntityError, UnauthorizedError, StreamingRequestOptions, StreamingFormat, ServiceUnavailableError, RetryConfig, RequestOptions, RateLimitError, NotFoundError, InternalServerError, HooksConfig, GoneError, ForbiddenError, FetchResponse, FetchError, FetchClientConfig, FetchClient, ConflictError, BadRequestError, AuthStrategy };
|
|
152
|
+
declare function createErrorFromStatus(status: number, message: string, body?: unknown): FetchError2;
|
|
153
|
+
export { unwrapOr, unwrap, ok, matchError, isQueryDescriptor, isOk, isErr, err, createErrorFromStatus, createDescriptor, UnprocessableEntityError, UnauthorizedError, StreamingRequestOptions, StreamingFormat, ServiceUnavailableError, RetryConfig, Result3 as Result, RequestOptions, RateLimitError, QueryDescriptor, NotFoundError, ListResponse, InternalServerError, HooksConfig, GoneError, ForbiddenError, FetchResponse, FetchErrorType, FetchError2 as FetchError, FetchClientConfig, FetchClient, EntityErrorType, ConflictError, BadRequestError, AuthStrategy };
|
package/dist/index.js
CHANGED
|
@@ -1,162 +1,128 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
|
|
3
|
-
status;
|
|
4
|
-
body;
|
|
5
|
-
constructor(message, status, body) {
|
|
6
|
-
super(message);
|
|
7
|
-
this.name = "FetchError";
|
|
8
|
-
this.status = status;
|
|
9
|
-
this.body = body;
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
class BadRequestError extends FetchError {
|
|
14
|
-
constructor(message, body) {
|
|
15
|
-
super(message, 400, body);
|
|
16
|
-
this.name = "BadRequestError";
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
class UnauthorizedError extends FetchError {
|
|
21
|
-
constructor(message, body) {
|
|
22
|
-
super(message, 401, body);
|
|
23
|
-
this.name = "UnauthorizedError";
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
class ForbiddenError extends FetchError {
|
|
28
|
-
constructor(message, body) {
|
|
29
|
-
super(message, 403, body);
|
|
30
|
-
this.name = "ForbiddenError";
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
class NotFoundError extends FetchError {
|
|
35
|
-
constructor(message, body) {
|
|
36
|
-
super(message, 404, body);
|
|
37
|
-
this.name = "NotFoundError";
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
class ConflictError extends FetchError {
|
|
42
|
-
constructor(message, body) {
|
|
43
|
-
super(message, 409, body);
|
|
44
|
-
this.name = "ConflictError";
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
class GoneError extends FetchError {
|
|
49
|
-
constructor(message, body) {
|
|
50
|
-
super(message, 410, body);
|
|
51
|
-
this.name = "GoneError";
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
class UnprocessableEntityError extends FetchError {
|
|
56
|
-
constructor(message, body) {
|
|
57
|
-
super(message, 422, body);
|
|
58
|
-
this.name = "UnprocessableEntityError";
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
class RateLimitError extends FetchError {
|
|
63
|
-
constructor(message, body) {
|
|
64
|
-
super(message, 429, body);
|
|
65
|
-
this.name = "RateLimitError";
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
class InternalServerError extends FetchError {
|
|
70
|
-
constructor(message, body) {
|
|
71
|
-
super(message, 500, body);
|
|
72
|
-
this.name = "InternalServerError";
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
class ServiceUnavailableError extends FetchError {
|
|
77
|
-
constructor(message, body) {
|
|
78
|
-
super(message, 503, body);
|
|
79
|
-
this.name = "ServiceUnavailableError";
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
var errorMap = {
|
|
83
|
-
400: BadRequestError,
|
|
84
|
-
401: UnauthorizedError,
|
|
85
|
-
403: ForbiddenError,
|
|
86
|
-
404: NotFoundError,
|
|
87
|
-
409: ConflictError,
|
|
88
|
-
410: GoneError,
|
|
89
|
-
422: UnprocessableEntityError,
|
|
90
|
-
429: RateLimitError,
|
|
91
|
-
500: InternalServerError,
|
|
92
|
-
503: ServiceUnavailableError
|
|
93
|
-
};
|
|
94
|
-
function createErrorFromStatus(status, message, body) {
|
|
95
|
-
const ErrorClass = errorMap[status];
|
|
96
|
-
if (ErrorClass) {
|
|
97
|
-
return new ErrorClass(message, body);
|
|
98
|
-
}
|
|
99
|
-
return new FetchError(message, status, body);
|
|
100
|
-
}
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { err as err2, isErr, isOk, matchError, ok as ok3, unwrap, unwrapOr } from "@vertz/errors";
|
|
101
3
|
|
|
102
4
|
// src/client.ts
|
|
5
|
+
import {
|
|
6
|
+
createHttpError,
|
|
7
|
+
err,
|
|
8
|
+
FetchNetworkError,
|
|
9
|
+
FetchTimeoutError,
|
|
10
|
+
FetchValidationError,
|
|
11
|
+
ok,
|
|
12
|
+
ParseError
|
|
13
|
+
} from "@vertz/errors";
|
|
103
14
|
var DEFAULT_RETRY_ON = [429, 500, 502, 503, 504];
|
|
104
15
|
|
|
105
16
|
class FetchClient {
|
|
106
17
|
config;
|
|
107
|
-
|
|
18
|
+
customFetchFn;
|
|
108
19
|
constructor(config) {
|
|
109
20
|
this.config = config;
|
|
110
|
-
this.
|
|
21
|
+
this.customFetchFn = config.fetch ? config.fetch.bind(globalThis) : null;
|
|
22
|
+
}
|
|
23
|
+
get fetchFn() {
|
|
24
|
+
return this.customFetchFn ?? globalThis.fetch.bind(globalThis);
|
|
111
25
|
}
|
|
112
26
|
async request(method, path, options) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const url = this.buildURL(path, options?.query);
|
|
121
|
-
const headers = new Headers(this.config.headers);
|
|
122
|
-
if (options?.headers) {
|
|
123
|
-
for (const [key, value] of Object.entries(options.headers)) {
|
|
124
|
-
headers.set(key, value);
|
|
27
|
+
try {
|
|
28
|
+
const retryConfig = this.resolveRetryConfig();
|
|
29
|
+
let lastError;
|
|
30
|
+
for (let attempt = 0;attempt <= retryConfig.retries; attempt++) {
|
|
31
|
+
if (attempt > 0) {
|
|
32
|
+
const delay = this.calculateBackoff(attempt, retryConfig);
|
|
33
|
+
await this.sleep(delay);
|
|
125
34
|
}
|
|
35
|
+
const url = this.buildURL(path, options?.query);
|
|
36
|
+
const headers = new Headers(this.config.headers);
|
|
37
|
+
if (options?.headers) {
|
|
38
|
+
for (const [key, value] of Object.entries(options.headers)) {
|
|
39
|
+
headers.set(key, value);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const signal = this.buildSignal(options?.signal);
|
|
43
|
+
const isRelativeUrl = url.startsWith("/");
|
|
44
|
+
const requestUrl = isRelativeUrl ? `http://localhost${url}` : url;
|
|
45
|
+
const serializedBody = options?.body !== undefined ? JSON.stringify(options.body) : undefined;
|
|
46
|
+
const request = new Request(requestUrl, {
|
|
47
|
+
method,
|
|
48
|
+
headers,
|
|
49
|
+
body: serializedBody,
|
|
50
|
+
signal
|
|
51
|
+
});
|
|
52
|
+
if (options?.body !== undefined) {
|
|
53
|
+
request.headers.set("Content-Type", "application/json");
|
|
54
|
+
}
|
|
55
|
+
const authedRequest = await this.applyAuth(request);
|
|
56
|
+
await this.config.hooks?.beforeRequest?.(authedRequest);
|
|
57
|
+
const response = isRelativeUrl ? await this.fetchFn(url, {
|
|
58
|
+
method: authedRequest.method,
|
|
59
|
+
headers: authedRequest.headers,
|
|
60
|
+
body: serializedBody,
|
|
61
|
+
signal: authedRequest.signal
|
|
62
|
+
}) : await this.fetchFn(authedRequest);
|
|
63
|
+
if (!response.ok) {
|
|
64
|
+
const body = await this.safeParseJSON(response);
|
|
65
|
+
let serverCode;
|
|
66
|
+
if (body && typeof body === "object" && "error" in body) {
|
|
67
|
+
const errorObj = body.error;
|
|
68
|
+
if (errorObj && typeof errorObj.code === "string") {
|
|
69
|
+
serverCode = errorObj.code;
|
|
70
|
+
}
|
|
71
|
+
if (errorObj?.code === "ValidationError" && Array.isArray(errorObj.errors)) {
|
|
72
|
+
return err(new FetchValidationError("Validation failed", errorObj.errors));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const httpError = createHttpError(response.status, response.statusText, serverCode);
|
|
76
|
+
if (attempt < retryConfig.retries && retryConfig.retryOn.includes(response.status)) {
|
|
77
|
+
lastError = httpError;
|
|
78
|
+
await this.config.hooks?.beforeRetry?.(attempt + 1, httpError);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
await this.config.hooks?.onError?.(httpError);
|
|
82
|
+
return err(httpError);
|
|
83
|
+
}
|
|
84
|
+
await this.config.hooks?.afterResponse?.(response);
|
|
85
|
+
if (response.status === 204 || response.status === 205) {
|
|
86
|
+
return ok({ data: undefined, status: response.status, headers: response.headers });
|
|
87
|
+
}
|
|
88
|
+
let data;
|
|
89
|
+
try {
|
|
90
|
+
data = await response.json();
|
|
91
|
+
} catch (parseError) {
|
|
92
|
+
return err(new ParseError("", "Failed to parse response JSON", parseError));
|
|
93
|
+
}
|
|
94
|
+
return ok({
|
|
95
|
+
data,
|
|
96
|
+
status: response.status,
|
|
97
|
+
headers: response.headers
|
|
98
|
+
});
|
|
126
99
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
});
|
|
134
|
-
if (options?.body !== undefined) {
|
|
135
|
-
request.headers.set("Content-Type", "application/json");
|
|
136
|
-
}
|
|
137
|
-
const authedRequest = await this.applyAuth(request);
|
|
138
|
-
await this.config.hooks?.beforeRequest?.(authedRequest);
|
|
139
|
-
const response = await this.fetchFn(authedRequest);
|
|
140
|
-
if (!response.ok) {
|
|
141
|
-
const body = await this.safeParseJSON(response);
|
|
142
|
-
const error = createErrorFromStatus(response.status, response.statusText, body);
|
|
143
|
-
if (attempt < retryConfig.retries && retryConfig.retryOn.includes(response.status)) {
|
|
144
|
-
lastError = error;
|
|
145
|
-
await this.config.hooks?.beforeRetry?.(attempt + 1, error);
|
|
146
|
-
continue;
|
|
100
|
+
return err(lastError ?? new FetchNetworkError("All retries exhausted"));
|
|
101
|
+
} catch (error) {
|
|
102
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
103
|
+
const abortSignal = error instanceof DOMException ? error.cause : error;
|
|
104
|
+
if (abortSignal instanceof Error && abortSignal.name === "TimeoutError") {
|
|
105
|
+
return err(new FetchTimeoutError);
|
|
147
106
|
}
|
|
148
|
-
|
|
149
|
-
throw error;
|
|
107
|
+
return err(new FetchNetworkError("Request aborted"));
|
|
150
108
|
}
|
|
151
|
-
|
|
152
|
-
const data = await response.json();
|
|
153
|
-
return {
|
|
154
|
-
data,
|
|
155
|
-
status: response.status,
|
|
156
|
-
headers: response.headers
|
|
157
|
-
};
|
|
109
|
+
return err(new FetchNetworkError("Network request failed"));
|
|
158
110
|
}
|
|
159
|
-
|
|
111
|
+
}
|
|
112
|
+
async get(path, options) {
|
|
113
|
+
return this.request("GET", path, options);
|
|
114
|
+
}
|
|
115
|
+
async post(path, body, options) {
|
|
116
|
+
return this.request("POST", path, { ...options, body });
|
|
117
|
+
}
|
|
118
|
+
async put(path, body, options) {
|
|
119
|
+
return this.request("PUT", path, { ...options, body });
|
|
120
|
+
}
|
|
121
|
+
async patch(path, body, options) {
|
|
122
|
+
return this.request("PATCH", path, { ...options, body });
|
|
123
|
+
}
|
|
124
|
+
async delete(path, options) {
|
|
125
|
+
return this.request("DELETE", path, options);
|
|
160
126
|
}
|
|
161
127
|
async* requestStream(options) {
|
|
162
128
|
const url = this.buildURL(options.path, options.query);
|
|
@@ -172,20 +138,35 @@ class FetchClient {
|
|
|
172
138
|
headers.set("Accept", "application/x-ndjson");
|
|
173
139
|
}
|
|
174
140
|
const signal = this.buildSignal(options.signal);
|
|
175
|
-
const
|
|
141
|
+
const isRelativeUrl = url.startsWith("/");
|
|
142
|
+
const requestUrl = isRelativeUrl ? `http://localhost${url}` : url;
|
|
143
|
+
const serializedBody = options.body !== undefined ? JSON.stringify(options.body) : undefined;
|
|
144
|
+
const request = new Request(requestUrl, {
|
|
176
145
|
method: options.method,
|
|
177
146
|
headers,
|
|
178
|
-
body:
|
|
147
|
+
body: serializedBody,
|
|
179
148
|
signal
|
|
180
149
|
});
|
|
181
150
|
const authedRequest = await this.applyAuth(request);
|
|
182
151
|
await this.config.hooks?.beforeRequest?.(authedRequest);
|
|
183
|
-
const response = await this.fetchFn(
|
|
152
|
+
const response = isRelativeUrl ? await this.fetchFn(url, {
|
|
153
|
+
method: authedRequest.method,
|
|
154
|
+
headers: authedRequest.headers,
|
|
155
|
+
body: serializedBody,
|
|
156
|
+
signal: authedRequest.signal
|
|
157
|
+
}) : await this.fetchFn(authedRequest);
|
|
184
158
|
if (!response.ok) {
|
|
185
159
|
const body = await this.safeParseJSON(response);
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
160
|
+
let serverCode;
|
|
161
|
+
if (body && typeof body === "object" && "error" in body) {
|
|
162
|
+
const errorObj = body.error;
|
|
163
|
+
if (errorObj && typeof errorObj.code === "string") {
|
|
164
|
+
serverCode = errorObj.code;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const httpError = createHttpError(response.status, response.statusText, serverCode);
|
|
168
|
+
await this.config.hooks?.onError?.(httpError);
|
|
169
|
+
throw httpError;
|
|
189
170
|
}
|
|
190
171
|
if (!response.body) {
|
|
191
172
|
return;
|
|
@@ -290,15 +271,32 @@ class FetchClient {
|
|
|
290
271
|
}
|
|
291
272
|
buildURL(path, query) {
|
|
292
273
|
const base = this.config.baseURL;
|
|
293
|
-
const
|
|
274
|
+
const isAbsoluteBase = base && /^https?:\/\//.test(base);
|
|
275
|
+
let urlString;
|
|
276
|
+
if (base && isAbsoluteBase) {
|
|
277
|
+
const relativePath = path.startsWith("/") ? path.slice(1) : path;
|
|
278
|
+
const normalizedBase = base.endsWith("/") ? base : `${base}/`;
|
|
279
|
+
urlString = new URL(relativePath, normalizedBase).toString();
|
|
280
|
+
} else if (base) {
|
|
281
|
+
const normalizedBase = base.endsWith("/") ? base.slice(0, -1) : base;
|
|
282
|
+
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
283
|
+
urlString = `${normalizedBase}${normalizedPath}`;
|
|
284
|
+
} else {
|
|
285
|
+
urlString = path;
|
|
286
|
+
}
|
|
294
287
|
if (query) {
|
|
288
|
+
const params = new URLSearchParams;
|
|
295
289
|
for (const [key, value] of Object.entries(query)) {
|
|
296
290
|
if (value !== undefined && value !== null) {
|
|
297
|
-
|
|
291
|
+
params.set(key, String(value));
|
|
298
292
|
}
|
|
299
293
|
}
|
|
294
|
+
const qs = params.toString();
|
|
295
|
+
if (qs) {
|
|
296
|
+
urlString += `${urlString.includes("?") ? "&" : "?"}${qs}`;
|
|
297
|
+
}
|
|
300
298
|
}
|
|
301
|
-
return
|
|
299
|
+
return urlString;
|
|
302
300
|
}
|
|
303
301
|
async applyAuth(request) {
|
|
304
302
|
const strategies = this.config.authStrategies;
|
|
@@ -346,8 +344,152 @@ class FetchClient {
|
|
|
346
344
|
}
|
|
347
345
|
}
|
|
348
346
|
}
|
|
347
|
+
// src/descriptor.ts
|
|
348
|
+
import { ok as ok2 } from "@vertz/errors";
|
|
349
|
+
function isQueryDescriptor(value) {
|
|
350
|
+
return value !== null && typeof value === "object" && "_tag" in value && value._tag === "QueryDescriptor";
|
|
351
|
+
}
|
|
352
|
+
function createDescriptor(method, path, fetchFn, query) {
|
|
353
|
+
const key = `${method}:${path}${serializeQuery(query)}`;
|
|
354
|
+
const fetchResult = async () => {
|
|
355
|
+
const response = await fetchFn();
|
|
356
|
+
if (!response.ok)
|
|
357
|
+
return response;
|
|
358
|
+
return ok2(response.data.data);
|
|
359
|
+
};
|
|
360
|
+
return {
|
|
361
|
+
_tag: "QueryDescriptor",
|
|
362
|
+
_key: key,
|
|
363
|
+
_fetch: fetchResult,
|
|
364
|
+
then(onFulfilled, onRejected) {
|
|
365
|
+
return fetchResult().then(onFulfilled, onRejected);
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
function serializeQuery(query) {
|
|
370
|
+
if (!query)
|
|
371
|
+
return "";
|
|
372
|
+
const params = new URLSearchParams;
|
|
373
|
+
for (const key of Object.keys(query).sort()) {
|
|
374
|
+
const value = query[key];
|
|
375
|
+
if (value !== undefined && value !== null) {
|
|
376
|
+
params.set(key, String(value));
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
const str = params.toString();
|
|
380
|
+
return str ? `?${str}` : "";
|
|
381
|
+
}
|
|
382
|
+
// src/errors.ts
|
|
383
|
+
class FetchError extends Error {
|
|
384
|
+
status;
|
|
385
|
+
body;
|
|
386
|
+
constructor(message, status, body) {
|
|
387
|
+
super(message);
|
|
388
|
+
this.name = "FetchError";
|
|
389
|
+
this.status = status;
|
|
390
|
+
this.body = body;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
class BadRequestError extends FetchError {
|
|
395
|
+
constructor(message, body) {
|
|
396
|
+
super(message, 400, body);
|
|
397
|
+
this.name = "BadRequestError";
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
class UnauthorizedError extends FetchError {
|
|
402
|
+
constructor(message, body) {
|
|
403
|
+
super(message, 401, body);
|
|
404
|
+
this.name = "UnauthorizedError";
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
class ForbiddenError extends FetchError {
|
|
409
|
+
constructor(message, body) {
|
|
410
|
+
super(message, 403, body);
|
|
411
|
+
this.name = "ForbiddenError";
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
class NotFoundError extends FetchError {
|
|
416
|
+
constructor(message, body) {
|
|
417
|
+
super(message, 404, body);
|
|
418
|
+
this.name = "NotFoundError";
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
class ConflictError extends FetchError {
|
|
423
|
+
constructor(message, body) {
|
|
424
|
+
super(message, 409, body);
|
|
425
|
+
this.name = "ConflictError";
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
class GoneError extends FetchError {
|
|
430
|
+
constructor(message, body) {
|
|
431
|
+
super(message, 410, body);
|
|
432
|
+
this.name = "GoneError";
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
class UnprocessableEntityError extends FetchError {
|
|
437
|
+
constructor(message, body) {
|
|
438
|
+
super(message, 422, body);
|
|
439
|
+
this.name = "UnprocessableEntityError";
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
class RateLimitError extends FetchError {
|
|
444
|
+
constructor(message, body) {
|
|
445
|
+
super(message, 429, body);
|
|
446
|
+
this.name = "RateLimitError";
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
class InternalServerError extends FetchError {
|
|
451
|
+
constructor(message, body) {
|
|
452
|
+
super(message, 500, body);
|
|
453
|
+
this.name = "InternalServerError";
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
class ServiceUnavailableError extends FetchError {
|
|
458
|
+
constructor(message, body) {
|
|
459
|
+
super(message, 503, body);
|
|
460
|
+
this.name = "ServiceUnavailableError";
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
var errorMap = {
|
|
464
|
+
400: BadRequestError,
|
|
465
|
+
401: UnauthorizedError,
|
|
466
|
+
403: ForbiddenError,
|
|
467
|
+
404: NotFoundError,
|
|
468
|
+
409: ConflictError,
|
|
469
|
+
410: GoneError,
|
|
470
|
+
422: UnprocessableEntityError,
|
|
471
|
+
429: RateLimitError,
|
|
472
|
+
500: InternalServerError,
|
|
473
|
+
503: ServiceUnavailableError
|
|
474
|
+
};
|
|
475
|
+
function createErrorFromStatus(status, message, body) {
|
|
476
|
+
const ErrorClass = errorMap[status];
|
|
477
|
+
if (ErrorClass) {
|
|
478
|
+
return new ErrorClass(message, body);
|
|
479
|
+
}
|
|
480
|
+
return new FetchError(message, status, body);
|
|
481
|
+
}
|
|
349
482
|
export {
|
|
483
|
+
unwrapOr,
|
|
484
|
+
unwrap,
|
|
485
|
+
ok3 as ok,
|
|
486
|
+
matchError,
|
|
487
|
+
isQueryDescriptor,
|
|
488
|
+
isOk,
|
|
489
|
+
isErr,
|
|
490
|
+
err2 as err,
|
|
350
491
|
createErrorFromStatus,
|
|
492
|
+
createDescriptor,
|
|
351
493
|
UnprocessableEntityError,
|
|
352
494
|
UnauthorizedError,
|
|
353
495
|
ServiceUnavailableError,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vertz/fetch",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Type-safe HTTP client for Vertz",
|
|
@@ -24,6 +24,9 @@
|
|
|
24
24
|
"files": [
|
|
25
25
|
"dist"
|
|
26
26
|
],
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@vertz/errors": "workspace:*"
|
|
29
|
+
},
|
|
27
30
|
"scripts": {
|
|
28
31
|
"build": "bunup",
|
|
29
32
|
"test": "vitest run",
|
|
@@ -31,7 +34,7 @@
|
|
|
31
34
|
"typecheck": "tsc --noEmit"
|
|
32
35
|
},
|
|
33
36
|
"devDependencies": {
|
|
34
|
-
"@types/node": "^
|
|
37
|
+
"@types/node": "^25.3.1",
|
|
35
38
|
"@vitest/coverage-v8": "^4.0.18",
|
|
36
39
|
"bunup": "latest",
|
|
37
40
|
"typescript": "^5.7.0",
|