Skip to content

Commit a186ebe

Browse files
authored
feat: Add Request object and options to adjust requests before sending (#6)
1 parent fa47450 commit a186ebe

File tree

3 files changed

+182
-59
lines changed

3 files changed

+182
-59
lines changed

index.ts

Lines changed: 177 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ type RequestMethod =
2222
| 'UNLOCK'
2323
| 'PROPFIND'
2424
| 'VIEW';
25+
type Options = {url:string, init:RequestInit};
2526
export interface RequestArgument {
2627
name: string;
2728
value: any;
@@ -47,99 +48,221 @@ const JSONStringifyReplacer = function(this:any, key:string, value:any) {
4748
return value;
4849
}
4950

50-
export class RestClient {
51+
export class RestClientRequest<ReturnType = any> {
5152
private readonly _baseUrl: string;
53+
private readonly _path: string;
54+
private readonly _method: RequestMethod;
55+
private readonly _requestArguments: RequestArgument[];
56+
private readonly _headers: { [key: string]: string } = {};
5257

53-
/**
54-
* Initialise rest client
55-
*
56-
* @param {string} baseUrl
57-
*/
58-
constructor(baseUrl: string) {
59-
if (!baseUrl) {
60-
baseUrl = '/';
61-
}
62-
63-
if (!baseUrl.endsWith('/')) {
64-
baseUrl += '/';
58+
constructor(baseUrl: string, method: RequestMethod, path: string, requestArguments: RequestArgument[]) {
59+
while (path.startsWith('/')) {
60+
path = path.substring(1);
6561
}
6662

6763
this._baseUrl = baseUrl;
64+
this._path = path;
65+
this._method = method;
66+
this._requestArguments = requestArguments;
6867
}
6968

70-
/**
71-
* Executes a request to the specified path using the specified method.
72-
*
73-
* @param {RequestMethod} method The HTTP method to use for the request.
74-
* @param {string} path The path of the resource to request.
75-
* @param {RequestArgument[]} requestArguments An array of request arguments.
76-
* @return {Promise<ReturnData | null>} The result of the request, or null if the response status is 404.
77-
*/
78-
async execute<ReturnData = any>(method: RequestMethod, path: string, requestArguments: RequestArgument[] = []) {
79-
while (path.startsWith('/')) {
80-
path = path.substring(1);
69+
public get url() {
70+
return this._baseUrl + this._path;
71+
}
72+
73+
public get method() {
74+
return this._method;
75+
}
76+
77+
public get arguments() {
78+
return [...this._requestArguments];
79+
}
80+
81+
public get headers() {
82+
return {
83+
...this._headers,
84+
};
85+
}
86+
87+
public withHeader(name: string, value: string) {
88+
this._headers[name] = value;
89+
return this;
90+
}
91+
92+
public withAuthorization(auth: string) {
93+
return this.withHeader('Authorization', auth);
94+
}
95+
96+
public withBearerToken(token: string) {
97+
return this.withAuthorization(`Bearer ${token}`);
98+
}
99+
100+
public withContentType(contentType: string) {
101+
return this.withHeader('Content-Type', contentType);
102+
}
103+
104+
public async call():Promise<ReturnType|null> {
105+
const opts = this.createOptions();
106+
const result = await fetch(opts.url, opts.init);
107+
108+
if (result.status === 404) {
109+
return null;
110+
}
111+
112+
let output: ReturnType | null = null;
113+
if (result.headers.get('content-type')?.startsWith('application/json')) {
114+
//Only parse json if content-type is application/json
115+
const text = await result.text();
116+
output = text ? (JSON.parse(text) as ReturnType) : null;
81117
}
82118

83-
let url = this._baseUrl + path;
119+
if (result.status >= 400) {
120+
const error =
121+
output && typeof output === 'object' && 'error' in output && typeof output.error === 'string'
122+
? output.error
123+
: 'Unknown error';
124+
throw new RestError(error, result);
125+
}
84126

85-
const query: string[] = [];
86-
const headers: { [key: string]: string } = {
127+
return output;
128+
}
129+
130+
private createOptions():Options {
131+
const query: string[] = []
132+
const headers = new Headers({
133+
...this._headers,
87134
accept: 'application/json',
88-
};
89-
const opts: RequestInit = {
90-
method,
91-
headers,
135+
});
136+
const out:Options = {
137+
url: this.url,
138+
init: {
139+
method: this.method,
140+
headers,
141+
}
92142
};
93143

94-
requestArguments.forEach((requestArgument) => {
95-
switch (requestArgument.transport.toLowerCase()) {
144+
this._requestArguments.forEach((requestArgument) => {
145+
const transport = requestArgument.transport?.toLowerCase() as Lowercase<RequestArgumentTransport>;
146+
switch (transport) {
96147
case 'path':
97-
url = url.replaceAll('{' + requestArgument.name + '}', requestArgument.value);
148+
out.url = out.url.replace('{' + requestArgument.name + '}', requestArgument.value);
98149
break;
99150
case 'header':
100-
headers[requestArgument.name] = requestArgument.value;
151+
headers.set(requestArgument.name, requestArgument.value);
101152
break;
102153
case 'body':
103-
if (!headers['content-type']) {
104-
headers['content-type'] = 'application/json';
154+
if (!headers.has('content-type')) {
155+
headers.set('content-type', 'application/json');
105156
}
106-
opts.body = JSON.stringify(requestArgument.value, JSONStringifyReplacer);
157+
out.init.body = JSON.stringify(requestArgument.value, JSONStringifyReplacer);
107158
break;
108159
case 'query':
109160
query.push(
110161
encodeURIComponent(requestArgument.name) + '=' + encodeURIComponent(requestArgument.value)
111162
);
112163
break;
113164
default:
165+
transport satisfies never;
114166
throw new Error('Unknown argument transport: ' + requestArgument.transport);
115167
}
116168
});
117169

118170
if (query.length > 0) {
119-
url += '?' + query.join('&');
171+
out.url += '?' + query.join('&');
172+
}
173+
return out;
174+
}
175+
176+
}
177+
178+
export class RestClient {
179+
private static globalHeaders: { [key: string]: string } = {};
180+
181+
static setHeader(name: string, value: string|undefined) {
182+
if (!value) {
183+
delete this.globalHeaders[name];
184+
return this;
120185
}
186+
this.globalHeaders[name] = value;
187+
return this;
188+
}
121189

122-
const result = await fetch(url, opts);
190+
static setAuthorization(auth: string|undefined) {
191+
return this.setHeader('Authorization', auth);
192+
}
123193

124-
if (result.status === 404) {
125-
return null;
194+
static setBearerToken(token: string|undefined) {
195+
return this.setAuthorization(token ? `Bearer ${token}` : token);
196+
}
197+
198+
private readonly _baseUrl: string;
199+
private _fixedHeaders: { [key: string]: string } = {};
200+
201+
/**
202+
* Initialise rest client
203+
*/
204+
constructor(baseUrl: string) {
205+
if (!baseUrl) {
206+
baseUrl = '/';
126207
}
127208

128-
let output: ReturnData | null = null;
129-
if (result.headers.get('content-type')?.startsWith('application/json')) {
130-
//Only parse json if content-type is application/json
131-
const text = await result.text();
132-
output = text ? (JSON.parse(text) as ReturnData) : null;
209+
if (!baseUrl.endsWith('/')) {
210+
baseUrl += '/';
133211
}
134212

135-
if (result.status >= 400) {
136-
const error =
137-
output && typeof output === 'object' && 'error' in output && typeof output.error === 'string'
138-
? output.error
139-
: 'Unknown error';
140-
throw new RestError(error, result);
213+
this._baseUrl = baseUrl;
214+
}
215+
216+
public get baseUrl() {
217+
return this._baseUrl;
218+
}
219+
220+
public withHeader(name: string, value: string|undefined) {
221+
if (!value) {
222+
delete this._fixedHeaders[name];
223+
return this;
141224
}
225+
this._fixedHeaders[name] = value;
226+
return this;
227+
}
142228

143-
return output;
229+
public withContentType(contentType: string|undefined) {
230+
return this.withHeader('Content-Type', contentType);
231+
}
232+
233+
public withAuthorization(auth: string|undefined) {
234+
return this.withHeader('Authorization', auth);
235+
}
236+
237+
public withBearerToken(token: string|undefined) {
238+
return this.withAuthorization(token ? `Bearer ${token}` : token);
239+
}
240+
241+
protected afterCreate(request: RestClientRequest):void {
242+
// Override this method to add additional headers or similar to all requests
243+
}
244+
245+
public create<ReturnType = any>(method: RequestMethod, path: string, requestArguments: RequestArgument[]):RestClientRequest<ReturnType> {
246+
const request = new RestClientRequest<ReturnType>(this._baseUrl, method, path, requestArguments);
247+
248+
Object.entries(RestClient.globalHeaders).forEach(([key, value]) => {
249+
request.withHeader(key, value);
250+
});
251+
252+
Object.entries(this._fixedHeaders).forEach(([key, value]) => {
253+
request.withHeader(key, value);
254+
});
255+
256+
this.afterCreate(request);
257+
return request;
258+
}
259+
260+
/**
261+
* Executes a request to the specified path using the specified method.
262+
*/
263+
public execute<ReturnType = any>(method: RequestMethod, path: string, requestArguments: RequestArgument[]) {
264+
const request = this.create<ReturnType>(method, path, requestArguments);
265+
266+
return request.call()
144267
}
145268
}

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@
7474
]
7575
},
7676
"devDependencies": {
77-
"typescript": "^5.1.3",
77+
"typescript": "^5.3.3",
7878
"@kapeta/eslint-config": "^0.6.0",
7979
"@kapeta/prettier-config": "^0.6.0",
8080
"eslint": "^8.42.0",

0 commit comments

Comments
 (0)