@@ -12,21 +12,29 @@ import {
12
12
import { CSPNonce , CSRFToken , ContentLanguage , IsFormDesigner } from './headers' ;
13
13
import { setLanguage } from './i18n' ;
14
14
15
- const fetchDefaults = {
16
- credentials : 'include' , // required for Firefox 60, which is used in werkplekken
15
+ interface ApiCallOptions extends Omit < RequestInit , 'headers' > {
16
+ headers ?: Record < string , string > ;
17
+ }
18
+
19
+ const fetchDefaults : ApiCallOptions = {
20
+ credentials : 'include' ,
17
21
} ;
18
22
19
23
const SessionExpiresInHeader = 'X-Session-Expires-In' ;
20
24
21
- let sessionExpiresAt = createState ( { expiry : null } ) ;
25
+ interface SessionExpiryState {
26
+ expiry : Date | null ;
27
+ }
28
+
29
+ const sessionExpiresAt = createState < SessionExpiryState > ( { expiry : null } ) ;
22
30
23
- export const updateSessionExpiry = seconds => {
31
+ export const updateSessionExpiry = ( seconds : number ) : void => {
24
32
const newExpiry = new Date ( ) ;
25
33
newExpiry . setSeconds ( newExpiry . getSeconds ( ) + seconds ) ;
26
34
sessionExpiresAt . setValue ( { expiry : newExpiry } ) ;
27
35
} ;
28
36
29
- const throwForStatus = async response => {
37
+ const throwForStatus = async ( response : Response ) : Promise < void > => {
30
38
if ( response . ok ) return ;
31
39
32
40
let responseData = null ;
@@ -75,7 +83,10 @@ const throwForStatus = async response => {
75
83
throw new ErrorClass ( errorMessage , response . status , responseData . detail , responseData . code ) ;
76
84
} ;
77
85
78
- const addHeaders = ( headers , method ) => {
86
+ const addHeaders = (
87
+ headers : Record < string , string > | undefined ,
88
+ method : string
89
+ ) : Record < string , string > => {
79
90
if ( ! headers ) headers = { } ;
80
91
81
92
// add the CSP nonce request header in case the backend needs to do any post-processing
@@ -94,10 +105,10 @@ const addHeaders = (headers, method) => {
94
105
return headers ;
95
106
} ;
96
107
97
- const updateStoredHeadersValues = headers => {
108
+ const updateStoredHeadersValues = ( headers : Headers ) : void => {
98
109
const sessionExpiry = headers . get ( SessionExpiresInHeader ) ;
99
110
if ( sessionExpiry ) {
100
- updateSessionExpiry ( parseInt ( sessionExpiry ) , 10 ) ;
111
+ updateSessionExpiry ( parseInt ( sessionExpiry , 10 ) ) ;
101
112
}
102
113
103
114
const CSRFTokenValue = headers . get ( CSRFToken . headerName ) ;
@@ -117,7 +128,7 @@ const updateStoredHeadersValues = headers => {
117
128
}
118
129
} ;
119
130
120
- const apiCall = async ( url , opts = { } ) => {
131
+ const apiCall = async ( url : string , opts : ApiCallOptions = { } ) : Promise < Response > => {
121
132
const method = opts . method || 'GET' ;
122
133
const options = { ...fetchDefaults , ...opts } ;
123
134
options . headers = addHeaders ( options . headers , method ) ;
@@ -129,7 +140,17 @@ const apiCall = async (url, opts = {}) => {
129
140
return response ;
130
141
} ;
131
142
132
- const get = async ( url , params = { } , multiParams = [ ] ) => {
143
+ /**
144
+ * Make a GET api call to `url`, with optional query string parameters.
145
+ *
146
+ * The return data is the JSON response body, or `null` if there is no content. Specify
147
+ * the generic type parameter `T` to get typed return data.
148
+ */
149
+ const get = async < T = unknown > (
150
+ url : string ,
151
+ params : Record < string , string > = { } ,
152
+ multiParams : Record < string , string > [ ] = [ ]
153
+ ) : Promise < T | null > => {
133
154
let searchParams = new URLSearchParams ( ) ;
134
155
if ( Object . keys ( params ) . length ) {
135
156
searchParams = new URLSearchParams ( params ) ;
@@ -142,16 +163,43 @@ const get = async (url, params = {}, multiParams = []) => {
142
163
}
143
164
url += `?${ searchParams } ` ;
144
165
const response = await apiCall ( url ) ;
145
- const data = response . status === 204 ? null : await response . json ( ) ;
166
+ const data : T | null = response . status === 204 ? null : await response . json ( ) ;
146
167
return data ;
147
168
} ;
148
169
149
- const _unsafe = async ( method = 'POST' , url , data , signal ) => {
150
- const opts = {
170
+ export interface UnsafeResponseData < T = unknown > {
171
+ /**
172
+ * The parsed response body JSON, if there was one.
173
+ */
174
+ data : T | null ;
175
+ /**
176
+ * Whether the request completed successfully or not.
177
+ */
178
+ ok : boolean ;
179
+ /**
180
+ * The HTTP response status code.
181
+ */
182
+ status : number ;
183
+ }
184
+
185
+ /**
186
+ * Make an unsafe (POST, PUT, PATCH) API call to `url`.
187
+ *
188
+ * The return data is the JSON response body, or `null` if there is no content. Specify
189
+ * the generic type parameter `T` to get typed return data, and `U` for strongly typing
190
+ * the request data (before JSON serialization).
191
+ */
192
+ const _unsafe = async < T = unknown , U = unknown > (
193
+ method = 'POST' ,
194
+ url : string ,
195
+ data : U ,
196
+ signal ?: AbortSignal
197
+ ) : Promise < UnsafeResponseData < T > > => {
198
+ const opts : ApiCallOptions = {
151
199
method,
152
200
headers : {
153
201
'Content-Type' : 'application/json' ,
154
- [ CSRFToken . headerName ] : CSRFToken . getValue ( ) ,
202
+ [ CSRFToken . headerName ] : CSRFToken . getValue ( ) ?? '' ,
155
203
} ,
156
204
} ;
157
205
if ( data ) {
@@ -161,30 +209,57 @@ const _unsafe = async (method = 'POST', url, data, signal) => {
161
209
opts . signal = signal ;
162
210
}
163
211
const response = await apiCall ( url , opts ) ;
164
- const responseData = response . status === 204 ? null : await response . json ( ) ;
212
+ const responseData : T | null = response . status === 204 ? null : await response . json ( ) ;
165
213
return {
166
214
ok : response . ok ,
167
215
status : response . status ,
168
216
data : responseData ,
169
217
} ;
170
218
} ;
171
219
172
- const post = async ( url , data , signal ) => {
173
- const resp = await _unsafe ( 'POST' , url , data , signal ) ;
174
- return resp ;
175
- } ;
220
+ /**
221
+ * Make a POST call to `url`.
222
+ *
223
+ * The return data is the JSON response body, or `null` if there is no content. Specify
224
+ * the generic type parameter `T` to get typed return data, and `U` for strongly typing
225
+ * the request data (before JSON serialization).
226
+ */
227
+ const post = async < T = unknown , U = unknown > (
228
+ url : string ,
229
+ data : U ,
230
+ signal ?: AbortSignal
231
+ ) : Promise < UnsafeResponseData < T > > => await _unsafe < T , U > ( 'POST' , url , data , signal ) ;
176
232
177
- const patch = async ( url , data = { } ) => {
178
- const resp = await _unsafe ( 'PATCH' , url , data ) ;
179
- return resp ;
180
- } ;
233
+ /**
234
+ * Make a PATCH call to `url`.
235
+ *
236
+ * The return data is the JSON response body, or `null` if there is no content. Specify
237
+ * the generic type parameter `T` to get typed return data, and `U` for strongly typing
238
+ * the request data (before JSON serialization).
239
+ */
240
+ const patch = async < T = unknown , U = unknown > (
241
+ url : string ,
242
+ data : U
243
+ ) : Promise < UnsafeResponseData < T > > => await _unsafe < T , U > ( 'PATCH' , url , data ) ;
181
244
182
- const put = async ( url , data = { } ) => {
183
- const resp = await _unsafe ( 'PUT' , url , data ) ;
184
- return resp ;
185
- } ;
245
+ /**
246
+ * Make a PUT call to `url`.
247
+ *
248
+ * The return data is the JSON response body, or `null` if there is no content. Specify
249
+ * the generic type parameter `T` to get typed return data, and `U` for strongly typing
250
+ * the request data (before JSON serialization).
251
+ */
252
+ const put = async < T = unknown , U = unknown > (
253
+ url : string ,
254
+ data : U
255
+ ) : Promise < UnsafeResponseData < T > > => await _unsafe < T , U > ( 'PUT' , url , data ) ;
186
256
187
- const destroy = async url => {
257
+ /**
258
+ * Make a DELETE call to `url`.
259
+ *
260
+ * If the delete was not successfull, an error is thrown.
261
+ */
262
+ const destroy = async ( url : string ) : Promise < void > => {
188
263
const opts = {
189
264
method : 'DELETE' ,
190
265
} ;
0 commit comments