make-service 0.0.2 → 0.1.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 +14 -11
- package/dist/index.d.ts +46 -31
- package/dist/index.js +47 -16
- package/dist/index.mjs +46 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -40,9 +40,9 @@ This library exports the `makeService` function and some primitives used to buil
|
|
|
40
40
|
|
|
41
41
|
# makeService
|
|
42
42
|
|
|
43
|
-
The main function of this lib is built on top of the primitives described in the following sections. It allows you to create
|
|
43
|
+
The main function of this lib is built on top of the primitives described in the following sections. It allows you to create a service object with a `baseURL` and common `headers` for every request.
|
|
44
44
|
|
|
45
|
-
This
|
|
45
|
+
This service object can be called with every HTTP method and it will return a [`typedResponse`](#typedresponse) object as it uses the [`enhancedFetch`](#enhancedfetch) internally.
|
|
46
46
|
|
|
47
47
|
```ts
|
|
48
48
|
import { makeService } from 'make-service'
|
|
@@ -107,7 +107,7 @@ await api.options("/users")
|
|
|
107
107
|
## enhancedFetch
|
|
108
108
|
|
|
109
109
|
A wrapper around the `fetch` API.
|
|
110
|
-
It returns a [`
|
|
110
|
+
It returns a [`TypedResponse`](#typedresponse) instead of a `Response`.
|
|
111
111
|
|
|
112
112
|
```ts
|
|
113
113
|
import { enhancedFetch } from 'make-service'
|
|
@@ -123,6 +123,8 @@ const json = await response.json()
|
|
|
123
123
|
|
|
124
124
|
This function accepts the same arguments as the `fetch` API - with exception of [JSON-like body](/src/make-service.ts) -, and it also accepts an object-like [`query`](/src/make-service.ts) and a `trace` function that will be called with the `input` and `requestInit` arguments.
|
|
125
125
|
|
|
126
|
+
This slightly different `RequestInit` is typed as `EnhancedRequestInit`.
|
|
127
|
+
|
|
126
128
|
```ts
|
|
127
129
|
import { enhancedFetch } from 'make-service'
|
|
128
130
|
|
|
@@ -150,23 +152,24 @@ A type-safe wrapper around the `Response` object. It adds a `json` and `text` me
|
|
|
150
152
|
|
|
151
153
|
```ts
|
|
152
154
|
import { typedResponse } from 'make-service'
|
|
155
|
+
import type { TypedResponse } from 'make-service'
|
|
153
156
|
|
|
154
157
|
// With JSON
|
|
155
|
-
const response = new Response(JSON.stringify({ foo: "bar" }))
|
|
156
|
-
const json = await
|
|
158
|
+
const response: TypedResponse = typedResponse(new Response(JSON.stringify({ foo: "bar" })))
|
|
159
|
+
const json = await response.json()
|
|
157
160
|
// ^? unknown
|
|
158
|
-
const json = await
|
|
161
|
+
const json = await response.json<{ foo: string }>()
|
|
159
162
|
// ^? { foo: string }
|
|
160
|
-
const json = await
|
|
163
|
+
const json = await response.json(z.object({ foo: z.string() }))
|
|
161
164
|
// ^? { foo: string }
|
|
162
165
|
|
|
163
166
|
// With text
|
|
164
|
-
const response = new Response("foo")
|
|
165
|
-
const text = await
|
|
167
|
+
const response: TypedResponse = typedResponse(new Response("foo"))
|
|
168
|
+
const text = await response.text()
|
|
166
169
|
// ^? string
|
|
167
|
-
const text = await
|
|
170
|
+
const text = await response.text<`foo${string}`>()
|
|
168
171
|
// ^? `foo${string}`
|
|
169
|
-
const text = await
|
|
172
|
+
const text = await response.text(z.string().email())
|
|
170
173
|
// ^? string
|
|
171
174
|
```
|
|
172
175
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
parse: (d: unknown) => T;
|
|
3
|
-
};
|
|
4
|
-
type JSONValue = string | number | boolean | {
|
|
5
|
-
[x: string]: JSONValue;
|
|
6
|
-
} | Array<JSONValue>;
|
|
7
|
-
type SearchParams = ConstructorParameters<typeof URLSearchParams>[0];
|
|
1
|
+
declare const HTTP_METHODS: readonly ["get", "post", "put", "delete", "patch", "options", "head"];
|
|
8
2
|
|
|
9
3
|
/**
|
|
10
4
|
* It returns the JSON object or throws an error if the response is not ok.
|
|
@@ -18,6 +12,33 @@ declare function getJson(response: Response): <T = unknown>(schema?: Schema<T> |
|
|
|
18
12
|
*/
|
|
19
13
|
declare function getText(response: Response): <T extends string = string>(schema?: Schema<T> | undefined) => Promise<T>;
|
|
20
14
|
|
|
15
|
+
type Schema<T> = {
|
|
16
|
+
parse: (d: unknown) => T;
|
|
17
|
+
};
|
|
18
|
+
type JSONValue = string | number | boolean | {
|
|
19
|
+
[x: string]: JSONValue;
|
|
20
|
+
} | Array<JSONValue>;
|
|
21
|
+
type SearchParams = ConstructorParameters<typeof URLSearchParams>[0];
|
|
22
|
+
type TypedResponse = Omit<Response, 'json' | 'text'> & {
|
|
23
|
+
json: TypedResponseJson;
|
|
24
|
+
text: TypedResponseText;
|
|
25
|
+
};
|
|
26
|
+
type EnhancedRequestInit = Omit<RequestInit, 'body'> & {
|
|
27
|
+
body?: JSONValue;
|
|
28
|
+
query?: SearchParams;
|
|
29
|
+
trace?: (...args: Parameters<typeof fetch>) => void;
|
|
30
|
+
};
|
|
31
|
+
type ServiceRequestInit = Omit<EnhancedRequestInit, 'method'>;
|
|
32
|
+
type HTTPMethod = (typeof HTTP_METHODS)[number];
|
|
33
|
+
type TypedResponseJson = ReturnType<typeof getJson>;
|
|
34
|
+
type TypedResponseText = ReturnType<typeof getText>;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* It merges multiple HeadersInit objects into a single Headers object
|
|
38
|
+
* @param entries Any number of HeadersInit objects
|
|
39
|
+
* @returns a new Headers object with the merged headers
|
|
40
|
+
*/
|
|
41
|
+
declare function mergeHeaders(...entries: (HeadersInit | [string, undefined][] | Record<string, undefined>)[]): Headers;
|
|
21
42
|
/**
|
|
22
43
|
* @param input a string or URL to which the query parameters will be added
|
|
23
44
|
* @param searchParams the query parameters
|
|
@@ -28,7 +49,7 @@ declare function addQueryToInput(input: string | URL, searchParams?: SearchParam
|
|
|
28
49
|
* @param baseURL the base path to the API
|
|
29
50
|
* @returns a function that receives a path and an object of query parameters and returns a URL
|
|
30
51
|
*/
|
|
31
|
-
declare function makeGetApiUrl(baseURL: string): (path: string, searchParams?: SearchParams) => string | URL;
|
|
52
|
+
declare function makeGetApiUrl(baseURL: string | URL): (path: string, searchParams?: SearchParams) => string | URL;
|
|
32
53
|
/**
|
|
33
54
|
* It hacks the Response object to add typed json and text methods
|
|
34
55
|
* @param response the Response to be proxied
|
|
@@ -43,27 +64,19 @@ declare function makeGetApiUrl(baseURL: string): (path: string, searchParams?: S
|
|
|
43
64
|
* const typedJson = await response.json<User[]>();
|
|
44
65
|
* // ^? User[]
|
|
45
66
|
*/
|
|
46
|
-
declare function typedResponse(response: Response):
|
|
47
|
-
json: ReturnType<typeof getJson>;
|
|
48
|
-
text: ReturnType<typeof getText>;
|
|
49
|
-
};
|
|
67
|
+
declare function typedResponse(response: Response): TypedResponse;
|
|
50
68
|
/**
|
|
51
69
|
* @param body the JSON-like body of the request
|
|
52
70
|
* @returns the body stringified if it is not a string
|
|
53
71
|
*/
|
|
54
72
|
declare function ensureStringBody(body?: JSONValue): string | undefined;
|
|
55
|
-
type Options = Omit<RequestInit, 'body'> & {
|
|
56
|
-
body?: JSONValue;
|
|
57
|
-
query?: SearchParams;
|
|
58
|
-
trace?: (...args: Parameters<typeof fetch>) => void;
|
|
59
|
-
};
|
|
60
73
|
/**
|
|
61
74
|
*
|
|
62
75
|
* @param input a string or URL to be fetched
|
|
63
|
-
* @param
|
|
64
|
-
* @param
|
|
65
|
-
* @param
|
|
66
|
-
* @param
|
|
76
|
+
* @param requestInit the requestInit to be passed to the fetch request. It is the same as the `RequestInit` type, but it also accepts a JSON-like `body` and an object-like `query` parameter.
|
|
77
|
+
* @param requestInit.body the body of the request. It will be automatically stringified so you can send a JSON-like object
|
|
78
|
+
* @param requestInit.query the query parameters to be added to the URL
|
|
79
|
+
* @param requestInit.trace a function that receives the URL and the requestInit and can be used to log the request
|
|
67
80
|
* @returns a Response with typed json and text methods
|
|
68
81
|
* @example const response = await fetch("https://example.com/api/users");
|
|
69
82
|
* const users = await response.json(userSchema);
|
|
@@ -71,24 +84,26 @@ type Options = Omit<RequestInit, 'body'> & {
|
|
|
71
84
|
* const untyped = await response.json();
|
|
72
85
|
* // ^? unknown
|
|
73
86
|
*/
|
|
74
|
-
declare function enhancedFetch(input: string | URL,
|
|
75
|
-
json: <T = unknown>(schema?: Schema<T> | undefined) => Promise<T>;
|
|
76
|
-
text: <T_1 extends string = string>(schema?: Schema<T_1> | undefined) => Promise<T_1>;
|
|
77
|
-
}>;
|
|
87
|
+
declare function enhancedFetch(input: string | URL, requestInit?: EnhancedRequestInit): Promise<TypedResponse>;
|
|
78
88
|
/**
|
|
79
89
|
*
|
|
80
90
|
* @param baseURL the base URL to the API
|
|
81
91
|
* @param baseHeaders any headers that should be sent with every request
|
|
82
|
-
* @returns
|
|
92
|
+
* @returns a service object with HTTP methods that are functions that receive a path and requestInit and return a serialized json response that can be typed or not.
|
|
83
93
|
* @example const headers = { Authorization: "Bearer 123" }
|
|
84
94
|
* const api = makeService("https://example.com/api", headers);
|
|
85
95
|
* const response = await api.get("/users")
|
|
86
96
|
* const users = await response.json(userSchema);
|
|
87
97
|
* // ^? User[]
|
|
88
98
|
*/
|
|
89
|
-
declare function makeService(baseURL: string, baseHeaders?: HeadersInit):
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
99
|
+
declare function makeService(baseURL: string | URL, baseHeaders?: HeadersInit): {
|
|
100
|
+
get: (path: string, requestInit?: ServiceRequestInit) => Promise<TypedResponse>;
|
|
101
|
+
post: (path: string, requestInit?: ServiceRequestInit) => Promise<TypedResponse>;
|
|
102
|
+
put: (path: string, requestInit?: ServiceRequestInit) => Promise<TypedResponse>;
|
|
103
|
+
delete: (path: string, requestInit?: ServiceRequestInit) => Promise<TypedResponse>;
|
|
104
|
+
patch: (path: string, requestInit?: ServiceRequestInit) => Promise<TypedResponse>;
|
|
105
|
+
options: (path: string, requestInit?: ServiceRequestInit) => Promise<TypedResponse>;
|
|
106
|
+
head: (path: string, requestInit?: ServiceRequestInit) => Promise<TypedResponse>;
|
|
107
|
+
};
|
|
93
108
|
|
|
94
|
-
export { addQueryToInput, enhancedFetch, ensureStringBody, makeGetApiUrl, makeService, typedResponse };
|
|
109
|
+
export { EnhancedRequestInit, HTTPMethod, JSONValue, Schema, SearchParams, ServiceRequestInit, TypedResponse, TypedResponseJson, TypedResponseText, addQueryToInput, enhancedFetch, ensureStringBody, makeGetApiUrl, makeService, mergeHeaders, typedResponse };
|
package/dist/index.js
CHANGED
|
@@ -25,6 +25,7 @@ __export(src_exports, {
|
|
|
25
25
|
ensureStringBody: () => ensureStringBody,
|
|
26
26
|
makeGetApiUrl: () => makeGetApiUrl,
|
|
27
27
|
makeService: () => makeService,
|
|
28
|
+
mergeHeaders: () => mergeHeaders,
|
|
28
29
|
typedResponse: () => typedResponse
|
|
29
30
|
});
|
|
30
31
|
module.exports = __toCommonJS(src_exports);
|
|
@@ -61,20 +62,42 @@ function isHTTPMethod(method) {
|
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
// src/make-service.ts
|
|
65
|
+
function mergeHeaders(...entries) {
|
|
66
|
+
const result = /* @__PURE__ */ new Map();
|
|
67
|
+
for (const entry of entries) {
|
|
68
|
+
const headers = new Headers(entry);
|
|
69
|
+
for (const [key, value] of headers.entries()) {
|
|
70
|
+
if (value === void 0 || value === "undefined") {
|
|
71
|
+
result.delete(key);
|
|
72
|
+
} else {
|
|
73
|
+
result.set(key, value);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return new Headers(Array.from(result.entries()));
|
|
78
|
+
}
|
|
64
79
|
function addQueryToInput(input, searchParams) {
|
|
65
80
|
if (!searchParams)
|
|
66
81
|
return input;
|
|
67
|
-
if (
|
|
82
|
+
if (typeof input === "string") {
|
|
68
83
|
const separator = input.includes("?") ? "&" : "?";
|
|
69
84
|
return `${input}${separator}${new URLSearchParams(searchParams)}`;
|
|
70
85
|
}
|
|
71
86
|
if (searchParams && input instanceof URL) {
|
|
72
|
-
|
|
87
|
+
for (const [key, value] of Object.entries(
|
|
88
|
+
new URLSearchParams(searchParams)
|
|
89
|
+
)) {
|
|
90
|
+
input.searchParams.set(key, value);
|
|
91
|
+
}
|
|
73
92
|
}
|
|
74
93
|
return input;
|
|
75
94
|
}
|
|
76
95
|
function makeGetApiUrl(baseURL) {
|
|
77
|
-
|
|
96
|
+
const base = baseURL instanceof URL ? baseURL.toString() : baseURL;
|
|
97
|
+
return (path, searchParams) => {
|
|
98
|
+
const url = `${base}${path}`.replace(/([^https?:]\/)\/+/g, "$1");
|
|
99
|
+
return addQueryToInput(url, searchParams);
|
|
100
|
+
};
|
|
78
101
|
}
|
|
79
102
|
function typedResponse(response) {
|
|
80
103
|
return new Proxy(response, {
|
|
@@ -94,24 +117,31 @@ function ensureStringBody(body) {
|
|
|
94
117
|
return body;
|
|
95
118
|
return JSON.stringify(body);
|
|
96
119
|
}
|
|
97
|
-
async function enhancedFetch(input,
|
|
98
|
-
|
|
99
|
-
const
|
|
120
|
+
async function enhancedFetch(input, requestInit) {
|
|
121
|
+
var _a;
|
|
122
|
+
const { query, trace, ...reqInit } = requestInit != null ? requestInit : {};
|
|
123
|
+
const headers = mergeHeaders(
|
|
124
|
+
{
|
|
125
|
+
"content-type": "application/json"
|
|
126
|
+
},
|
|
127
|
+
(_a = reqInit.headers) != null ? _a : {}
|
|
128
|
+
);
|
|
100
129
|
const url = addQueryToInput(input, query);
|
|
101
130
|
const body = ensureStringBody(reqInit.body);
|
|
102
|
-
const
|
|
103
|
-
trace == null ? void 0 : trace(url,
|
|
104
|
-
const
|
|
105
|
-
const response = await fetch(request);
|
|
131
|
+
const enhancedReqInit = { ...reqInit, headers, body };
|
|
132
|
+
trace == null ? void 0 : trace(url, enhancedReqInit);
|
|
133
|
+
const response = await fetch(url, enhancedReqInit);
|
|
106
134
|
return typedResponse(response);
|
|
107
135
|
}
|
|
108
136
|
function makeService(baseURL, baseHeaders) {
|
|
109
|
-
const
|
|
110
|
-
return async (path,
|
|
111
|
-
|
|
112
|
-
|
|
137
|
+
const service = (method) => {
|
|
138
|
+
return async (path, requestInit = {}) => {
|
|
139
|
+
var _a;
|
|
140
|
+
const url = makeGetApiUrl(baseURL)(path);
|
|
141
|
+
const response = await enhancedFetch(url, {
|
|
142
|
+
...requestInit,
|
|
113
143
|
method,
|
|
114
|
-
headers:
|
|
144
|
+
headers: mergeHeaders(baseHeaders != null ? baseHeaders : {}, (_a = requestInit == null ? void 0 : requestInit.headers) != null ? _a : {})
|
|
115
145
|
});
|
|
116
146
|
return response;
|
|
117
147
|
};
|
|
@@ -119,7 +149,7 @@ function makeService(baseURL, baseHeaders) {
|
|
|
119
149
|
return new Proxy({}, {
|
|
120
150
|
get(_target, prop) {
|
|
121
151
|
if (isHTTPMethod(prop))
|
|
122
|
-
return
|
|
152
|
+
return service(prop.toUpperCase());
|
|
123
153
|
throw new Error(`Invalid HTTP method: ${prop.toString()}`);
|
|
124
154
|
}
|
|
125
155
|
});
|
|
@@ -131,5 +161,6 @@ function makeService(baseURL, baseHeaders) {
|
|
|
131
161
|
ensureStringBody,
|
|
132
162
|
makeGetApiUrl,
|
|
133
163
|
makeService,
|
|
164
|
+
mergeHeaders,
|
|
134
165
|
typedResponse
|
|
135
166
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -30,20 +30,42 @@ function isHTTPMethod(method) {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
// src/make-service.ts
|
|
33
|
+
function mergeHeaders(...entries) {
|
|
34
|
+
const result = /* @__PURE__ */ new Map();
|
|
35
|
+
for (const entry of entries) {
|
|
36
|
+
const headers = new Headers(entry);
|
|
37
|
+
for (const [key, value] of headers.entries()) {
|
|
38
|
+
if (value === void 0 || value === "undefined") {
|
|
39
|
+
result.delete(key);
|
|
40
|
+
} else {
|
|
41
|
+
result.set(key, value);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return new Headers(Array.from(result.entries()));
|
|
46
|
+
}
|
|
33
47
|
function addQueryToInput(input, searchParams) {
|
|
34
48
|
if (!searchParams)
|
|
35
49
|
return input;
|
|
36
|
-
if (
|
|
50
|
+
if (typeof input === "string") {
|
|
37
51
|
const separator = input.includes("?") ? "&" : "?";
|
|
38
52
|
return `${input}${separator}${new URLSearchParams(searchParams)}`;
|
|
39
53
|
}
|
|
40
54
|
if (searchParams && input instanceof URL) {
|
|
41
|
-
|
|
55
|
+
for (const [key, value] of Object.entries(
|
|
56
|
+
new URLSearchParams(searchParams)
|
|
57
|
+
)) {
|
|
58
|
+
input.searchParams.set(key, value);
|
|
59
|
+
}
|
|
42
60
|
}
|
|
43
61
|
return input;
|
|
44
62
|
}
|
|
45
63
|
function makeGetApiUrl(baseURL) {
|
|
46
|
-
|
|
64
|
+
const base = baseURL instanceof URL ? baseURL.toString() : baseURL;
|
|
65
|
+
return (path, searchParams) => {
|
|
66
|
+
const url = `${base}${path}`.replace(/([^https?:]\/)\/+/g, "$1");
|
|
67
|
+
return addQueryToInput(url, searchParams);
|
|
68
|
+
};
|
|
47
69
|
}
|
|
48
70
|
function typedResponse(response) {
|
|
49
71
|
return new Proxy(response, {
|
|
@@ -63,24 +85,31 @@ function ensureStringBody(body) {
|
|
|
63
85
|
return body;
|
|
64
86
|
return JSON.stringify(body);
|
|
65
87
|
}
|
|
66
|
-
async function enhancedFetch(input,
|
|
67
|
-
|
|
68
|
-
const
|
|
88
|
+
async function enhancedFetch(input, requestInit) {
|
|
89
|
+
var _a;
|
|
90
|
+
const { query, trace, ...reqInit } = requestInit != null ? requestInit : {};
|
|
91
|
+
const headers = mergeHeaders(
|
|
92
|
+
{
|
|
93
|
+
"content-type": "application/json"
|
|
94
|
+
},
|
|
95
|
+
(_a = reqInit.headers) != null ? _a : {}
|
|
96
|
+
);
|
|
69
97
|
const url = addQueryToInput(input, query);
|
|
70
98
|
const body = ensureStringBody(reqInit.body);
|
|
71
|
-
const
|
|
72
|
-
trace == null ? void 0 : trace(url,
|
|
73
|
-
const
|
|
74
|
-
const response = await fetch(request);
|
|
99
|
+
const enhancedReqInit = { ...reqInit, headers, body };
|
|
100
|
+
trace == null ? void 0 : trace(url, enhancedReqInit);
|
|
101
|
+
const response = await fetch(url, enhancedReqInit);
|
|
75
102
|
return typedResponse(response);
|
|
76
103
|
}
|
|
77
104
|
function makeService(baseURL, baseHeaders) {
|
|
78
|
-
const
|
|
79
|
-
return async (path,
|
|
80
|
-
|
|
81
|
-
|
|
105
|
+
const service = (method) => {
|
|
106
|
+
return async (path, requestInit = {}) => {
|
|
107
|
+
var _a;
|
|
108
|
+
const url = makeGetApiUrl(baseURL)(path);
|
|
109
|
+
const response = await enhancedFetch(url, {
|
|
110
|
+
...requestInit,
|
|
82
111
|
method,
|
|
83
|
-
headers:
|
|
112
|
+
headers: mergeHeaders(baseHeaders != null ? baseHeaders : {}, (_a = requestInit == null ? void 0 : requestInit.headers) != null ? _a : {})
|
|
84
113
|
});
|
|
85
114
|
return response;
|
|
86
115
|
};
|
|
@@ -88,7 +117,7 @@ function makeService(baseURL, baseHeaders) {
|
|
|
88
117
|
return new Proxy({}, {
|
|
89
118
|
get(_target, prop) {
|
|
90
119
|
if (isHTTPMethod(prop))
|
|
91
|
-
return
|
|
120
|
+
return service(prop.toUpperCase());
|
|
92
121
|
throw new Error(`Invalid HTTP method: ${prop.toString()}`);
|
|
93
122
|
}
|
|
94
123
|
});
|
|
@@ -99,5 +128,6 @@ export {
|
|
|
99
128
|
ensureStringBody,
|
|
100
129
|
makeGetApiUrl,
|
|
101
130
|
makeService,
|
|
131
|
+
mergeHeaders,
|
|
102
132
|
typedResponse
|
|
103
133
|
};
|