• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

thorgate / tg-resources / 11914381385

19 Nov 2024 01:45PM UTC coverage: 97.106%. First build
11914381385

Pull #132

github

web-flow
Merge 2a1ec070f into aa16fc409
Pull Request #132: feat: Improve typings

382 of 422 branches covered (90.52%)

60 of 70 new or added lines in 12 files covered. (85.71%)

671 of 691 relevant lines covered (97.11%)

36.23 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.92
/packages/core/src/resource.ts
1
import { hasValue, isFunction, isObject, isStatusCode } from '@tg-resources/is';
10✔
2
import { routeTemplate } from '@tg-resources/route-template';
10✔
3

4
import DEFAULTS from './constants';
10✔
5
import {
10✔
6
    AbortError,
7
    InvalidResponseCode,
8
    NetworkError,
9
    RequestValidationError,
10
} from './errors';
11
import { Route } from './route';
10✔
12
import {
13
    AllowedFetchMethods,
14
    AllowedPostMethods,
15
    Attachments,
16
    ConfigType,
17
    EmptyPayload,
18
    Kwargs,
19
    ObjectMap,
20
    Query,
21
    RequestConfig,
22
    ResourceErrorInterface,
23
    ResourceInterface,
24
    ResponseInterface,
25
    RouteConfig,
26
} from './types';
27
import { mergeConfig, serializeCookies } from './util';
10✔
28

29
export abstract class Resource<
10✔
30
        Params extends Kwargs | null = Kwargs,
31
        TFetchResponse = any,
32
        TPostPayload extends ObjectMap | string | null = any,
33
        TPostResponse = TFetchResponse
34
    >
35
    extends Route
36
    implements
37
        ResourceInterface<Params, TFetchResponse, TPostPayload, TPostResponse>
38
{
39
    /**
40
     * @param apiEndpoint Endpoint used for this resource. Supports ES6 token syntax, e.g: "/foo/bar/${pk}"
41
     * @param config Customize config for this resource (see `Router.config`)
42
     */
43
    public constructor(apiEndpoint: string, config: RouteConfig = null) {
37✔
44
        super(config);
62✔
45

46
        this._apiEndpoint = apiEndpoint;
62✔
47

48
        this._routeTemplate = routeTemplate(apiEndpoint);
62✔
49
    }
50

51
    private readonly _apiEndpoint: string;
52

53
    private readonly _routeTemplate: ReturnType<typeof routeTemplate>;
54

55
    public get apiEndpoint() {
56
        return this._apiEndpoint;
9✔
57
    }
58

59
    public config(requestConfig: RequestConfig = null): ConfigType {
68✔
60
        if (!this._config) {
172✔
61
            this._config = mergeConfig(
56✔
62
                this.parent ? this.parent.config() : DEFAULTS,
56!
63
                this._customConfig
64
            );
65
        }
66

67
        if (requestConfig && isObject(requestConfig)) {
172✔
68
            return mergeConfig(this._config, requestConfig);
4✔
69
        }
70

71
        return this._config as ConfigType;
168✔
72
    }
73

74
    public clearConfigCache() {
75
        this._config = null;
3✔
76
    }
77

78
    public getHeaders(requestConfig: RequestConfig = null) {
15✔
79
        const config = this.config(requestConfig);
24✔
80
        const headers = {
24✔
81
            ...(this.parent ? this.parent.getHeaders() : {}),
24!
82
            ...((isFunction(config.headers)
57✔
83
                ? config.headers()
84
                : config.headers) || {}),
85
        };
86

87
        const cookieVal = serializeCookies(this.getCookies(requestConfig));
24✔
88
        if (cookieVal) {
24✔
89
            headers.Cookie = cookieVal;
1✔
90
        }
91

92
        // if Accept is null/undefined, add default accept header automatically (backwards incompatible for text/html)
93
        if (!hasValue(headers.Accept)) {
24✔
94
            headers.Accept = config.defaultAcceptHeader;
24✔
95
        }
96

97
        return headers;
24✔
98
    }
99

100
    public getCookies(requestConfig: RequestConfig = null) {
17✔
101
        const config = this.config(requestConfig);
41✔
102
        return {
41✔
103
            ...(this.parent ? this.parent.getCookies() : {}),
41!
104
            ...((isFunction(config.cookies)
105✔
105
                ? config.cookies()
106
                : config.cookies) || {}),
107
        };
108
    }
109

110
    public get = <TResponse = TFetchResponse, TParams extends Params = Params>(
62✔
111
        kwargs: TParams | null = null,
1✔
112
        query: Query | null = null,
1✔
113
        requestConfig: RequestConfig | null = null
1✔
114
    ): Promise<TResponse> =>
115
        this._fetch<TResponse, TParams>(kwargs, query, requestConfig, 'get');
3✔
116

117
    public fetch = <
62✔
118
        TResponse = TFetchResponse,
119
        TParams extends Params = Params
120
    >(
121
        kwargs: TParams | null = null,
2✔
122
        query: Query | null = null,
2✔
123
        requestConfig: RequestConfig | null = null
2✔
124
    ): Promise<TResponse> =>
125
        this.get<TResponse, TParams>(kwargs, query, requestConfig);
2✔
126

127
    public head = <
62✔
128
        TResponse = Record<string, never>,
129
        TParams extends Params = Params
130
    >(
131
        kwargs: TParams | null = null,
×
132
        query: Query | null = null,
×
133
        requestConfig: RequestConfig | null = null
×
134
    ): Promise<TResponse> =>
135
        // istanbul ignore next: Tested in package that implement Resource
NEW
136
        this._fetch<TResponse, TParams>(kwargs, query, requestConfig, 'head');
×
137

138
    public options = <
62✔
139
        TResponse = TFetchResponse,
140
        TParams extends Params = Params
141
    >(
142
        kwargs: TParams | null = null,
×
143
        query: Query | null = null,
×
144
        requestConfig: RequestConfig = null
×
145
    ): Promise<TResponse> =>
146
        // istanbul ignore next: Tested in package that implement Resource
NEW
147
        this._fetch<TResponse, TParams>(
×
148
            kwargs,
149
            query,
150
            requestConfig,
151
            'options'
152
        );
153

154
    public post = <
62✔
155
        TResponse = TPostResponse,
156
        TPayload extends TPostPayload = TPostPayload,
157
        TParams extends Params = Params
158
    >(
159
        kwargs: TParams | null = null,
2✔
160
        data: TPayload | string | null = null,
2✔
161
        query: Query | null = null,
2✔
162
        attachments: Attachments | null = null,
2✔
163
        requestConfig: RequestConfig | null = null
2✔
164
    ): Promise<TResponse> =>
165
        this._post<TResponse, TPayload, TParams>(
2✔
166
            kwargs,
167
            data,
168
            query,
169
            attachments,
170
            requestConfig,
171
            'post'
172
        );
173

174
    public patch = <
62✔
175
        TResponse = TPostResponse,
176
        TPayload extends TPostPayload = TPostPayload,
177
        TParams extends Params = Params
178
    >(
179
        kwargs: TParams | null = null,
1✔
180
        data: TPayload | string | null = null,
1✔
181
        query: Query | null = null,
1✔
182
        attachments: Attachments | null = null,
1✔
183
        requestConfig: RequestConfig | null = null
1✔
184
    ): Promise<TResponse> =>
185
        this._post<TResponse, TPayload, TParams>(
1✔
186
            kwargs,
187
            data,
188
            query,
189
            attachments,
190
            requestConfig,
191
            'patch'
192
        );
193

194
    public put = <
62✔
195
        TResponse = TPostResponse,
196
        TPayload extends TPostPayload = TPostPayload,
197
        TParams extends Params = Params
198
    >(
199
        kwargs: TParams | null = null,
1✔
200
        data: TPayload | string | null = null,
1✔
201
        query: Query | null = null,
1✔
202
        attachments: Attachments | null = null,
1✔
203
        requestConfig: RequestConfig | null = null
1✔
204
    ): Promise<TResponse> =>
205
        this._post<TResponse, TPayload, TParams>(
1✔
206
            kwargs,
207
            data,
208
            query,
209
            attachments,
210
            requestConfig,
211
            'put'
212
        );
213

214
    public del = <
62✔
215
        TResponse = EmptyPayload,
216
        TPayload extends TPostPayload = TPostPayload,
217
        TParams extends Params = Params
218
    >(
219
        kwargs: TParams | null = null,
1✔
220
        data: TPayload | string | null = null,
1✔
221
        query: Query | null = null,
1✔
222
        attachments: Attachments | null = null,
1✔
223
        requestConfig: RequestConfig | null = null
1✔
224
    ): Promise<TResponse> =>
225
        this._post<TResponse, TPayload, TParams>(
1✔
226
            kwargs,
227
            data,
228
            query,
229
            attachments,
230
            requestConfig,
231
            'del'
232
        );
233

234
    public renderPath<TParams extends Kwargs | null = Params>(
235
        urlParams: TParams | null = null,
×
236
        requestConfig: RequestConfig = null
×
237
    ): string {
238
        const config = this.config(requestConfig);
8✔
239

240
        // istanbul ignore next: Tested in package that implements Resource
241
        if (isObject(urlParams)) {
242
            this._routeTemplate.configure(
243
                config.apiRoot,
244
                config.useLodashTemplate
245
            );
246

247
            return this._routeTemplate(urlParams);
248
        }
249

250
        return `${config.apiRoot}${this.apiEndpoint}`;
8✔
251
    }
252

253
    /* Internal API */
254

255
    protected abstract wrapResponse(
256
        res: any,
257
        error: any,
258
        req?: any
259
    ): ResponseInterface;
260

261
    protected abstract setHeader(
262
        req: any,
263
        key: string,
264
        value: string | null
265
    ): any;
266

267
    protected abstract createRequest<
268
        TPayload extends ObjectMap | string | null = any
269
    >(
270
        method: string,
271
        url: string,
272
        query: Query,
273
        data: TPayload | null,
274
        attachments: Attachments,
275
        requestConfig: RequestConfig
276
    ): any;
277

278
    protected abstract doRequest(
279
        req: any,
280
        resolve: (response: any, error: any) => void
281
    ): void;
282

283
    protected _fetch<
284
        TResponse = TFetchResponse,
285
        TParams extends Kwargs | null = Params
286
    >(
287
        kwargs: TParams | null = null,
×
288
        query: Query | null = null,
×
289
        requestConfig: RequestConfig | null = null,
×
290
        method: AllowedFetchMethods = 'get'
×
291
    ): Promise<TResponse> {
292
        const thePath = this.renderPath(kwargs, requestConfig);
3✔
293
        return this.handleRequest(
3✔
294
            this.createRequest(
295
                method,
296
                thePath,
297
                query,
298
                null,
299
                null,
300
                requestConfig
301
            ),
302
            requestConfig
303
        );
304
    }
305

306
    protected _post<
307
        TResponse = TPostResponse,
308
        TPayload extends ObjectMap | string | null = TPostPayload,
309
        TParams extends Kwargs | null = Params
310
    >(
311
        kwargs: TParams | null = null,
×
312
        data: TPayload | string | null = null,
×
313
        query: Query = null,
×
314
        attachments: Attachments = null,
×
315
        requestConfig: RequestConfig = null,
×
316
        method: AllowedPostMethods = 'post'
×
317
    ): Promise<TResponse> {
318
        const config = this.config(requestConfig);
5✔
319

320
        // istanbul ignore next: Tested in package that implement Resource
321
        if (attachments && !config.allowAttachments) {
322
            throw new Error(
323
                'Misconfiguration: "allowAttachments=true" is required when sending attachments!'
324
            );
325
        }
326

327
        const thePath = this.renderPath(kwargs, requestConfig);
5✔
328

329
        return this.handleRequest(
5✔
330
            this.createRequest(
331
                method,
332
                thePath,
333
                query,
334
                data || {},
10✔
335
                attachments,
336
                requestConfig
337
            ),
338
            requestConfig
339
        );
340
    }
341

342
    protected mutateRawResponse<T extends ResponseInterface>(
343
        rawResponse: ResponseInterface,
344
        requestConfig: RequestConfig
345
    ): T {
346
        const config = this.config(requestConfig);
8✔
347

348
        // istanbul ignore next: Tested in package that implement Resource
349
        if (isFunction(config.mutateRawResponse)) {
350
            return config.mutateRawResponse(rawResponse, requestConfig);
351
        }
352

353
        return rawResponse as T;
8✔
354
    }
355

356
    protected mutateResponse<R, T extends R = any>(
357
        responseData: R,
358
        rawResponse: ResponseInterface,
359
        requestConfig: RequestConfig
360
    ): T {
361
        const config = this.config(requestConfig);
8✔
362

363
        // istanbul ignore next: Tested in package that implement Resource
364
        if (isFunction(config.mutateResponse)) {
365
            return config.mutateResponse(
366
                responseData,
367
                rawResponse,
368
                this,
369
                requestConfig
370
            );
371
        }
372

373
        return responseData as T;
8✔
374
    }
375

376
    protected mutateError<T extends ResourceErrorInterface>(
377
        error: ResourceErrorInterface,
378
        rawResponse: ResponseInterface,
379
        requestConfig: RequestConfig
380
    ): T {
381
        // istanbul ignore next: Tested in package that implement Resource
382
        const config = this.config(requestConfig);
383

384
        // istanbul ignore next: Tested in package that implement Resource
385
        if (isFunction(config.mutateError)) {
386
            return config.mutateError(error, rawResponse, this, config);
387
        }
388

389
        // istanbul ignore next: Tested in package that implement Resource
390
        return error as T;
391
    }
392

393
    protected handleRequest<R>(
394
        req: any,
395
        requestConfig: RequestConfig
396
    ): Promise<R> {
397
        return this.ensureStatusAndJson<R>(
8✔
398
            new Promise((resolve) => {
399
                const headers = this.getHeaders(requestConfig);
8✔
400

401
                if (headers && isObject(headers)) {
8✔
402
                    Object.keys(headers).forEach((key) => {
8✔
403
                        if (hasValue(headers[key])) {
8✔
404
                            // eslint-disable-next-line no-param-reassign
405
                            req = this.setHeader(req, key, headers[key]);
8✔
406
                        }
407
                    });
408
                }
409

410
                this.doRequest(req, (response, error) =>
8✔
411
                    resolve(this.wrapResponse(response, error, req))
8✔
412
                );
413
            }),
414
            requestConfig
415
        );
416
    }
417

418
    protected ensureStatusAndJson<R>(
419
        prom: Promise<ResponseInterface>,
420
        requestConfig: RequestConfig
421
    ): Promise<R> {
422
        const config = this.config(requestConfig);
8✔
423
        return prom.then((origRes: ResponseInterface) => {
8✔
424
            const res = this.mutateRawResponse(origRes, requestConfig);
8✔
425

426
            // If no error occured, e.g we have response and !hasError
427
            // istanbul ignore next: Tested in package that implement Resource
428
            if (res && !res.hasError) {
429
                if (isStatusCode(config.statusSuccess, res.status)) {
430
                    // Got statusSuccess response code, lets resolve this promise
431
                    return this.mutateResponse<R>(res.data, res, requestConfig);
432
                }
433

434
                if (isStatusCode(config.statusValidationError, res.status)) {
435
                    // Got statusValidationError response code, lets throw RequestValidationError
436
                    // eslint-disable-next-line @typescript-eslint/no-throw-literal
437
                    throw this.mutateError(
438
                        new RequestValidationError(
439
                            res.status,
440
                            res.text,
441
                            config
442
                        ),
443
                        res,
444
                        requestConfig
445
                    );
446
                } else {
447
                    // Throw a InvalidResponseCode error
448
                    // eslint-disable-next-line @typescript-eslint/no-throw-literal
449
                    throw this.mutateError(
450
                        new InvalidResponseCode(res.status, res.text),
451
                        res,
452
                        requestConfig
453
                    );
454
                }
455
            } else {
456
                let error;
457

458
                if (res && res.wasAborted) {
459
                    error = new AbortError(res.error);
460
                } else {
461
                    // res.hasError should only be true if network level errors occur (not statuscode errors)
462
                    const message = res && res.hasError ? res.error : '';
463

464
                    error = new NetworkError(
465
                        message ||
466
                            'Something went awfully wrong with the request, check network log.'
467
                    );
468
                }
469

470
                // eslint-disable-next-line @typescript-eslint/no-throw-literal
471
                throw this.mutateError(error, res, requestConfig);
472
            }
473
        });
474
    }
475
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc