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

thorgate / tg-resources / 11929801395

20 Nov 2024 08:47AM UTC coverage: 97.114% (-0.2%) from 97.297%
11929801395

push

github

web-flow
Merge pull request #132 from thorgate/feat/improved-typings

feat: Improve typings

385 of 425 branches covered (90.59%)

62 of 72 new or added lines in 12 files covered. (86.11%)

4 existing lines in 1 file now uncovered.

673 of 693 relevant lines covered (97.11%)

36.12 hits per line

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

95.56
/packages/tg-resources-fetch/src/resource.ts
1
import {
1✔
2
    AllowedMethods,
3
    Attachments,
4
    Kwargs,
5
    ObjectMap,
6
    Optional,
7
    Query,
8
    RequestConfig,
9
    Resource,
10
    ResponseInterface,
11
} from '@tg-resources/core';
12
import { hasValue, isArray, isObject, isString } from '@tg-resources/is';
1✔
13
import qs from 'qs';
1✔
14

15
interface HeadersObject {
16
    [key: string]: any;
17
}
18

19
interface FetchResponseInterface {
20
    status: number;
21
    headers: HeadersObject;
22

23
    text: string | null;
24
    body: any | null;
25
}
26

27
export class FetchResponse extends ResponseInterface {
1✔
28
    public get response(): Optional<FetchResponseInterface> {
29
        return this._response;
327✔
30
    }
31

32
    public get status() {
33
        if (this.response) {
43✔
34
            return this.response.status;
43✔
35
        }
36

37
        // istanbul ignore next: Only happens on network errors
38
        return null;
39
    }
40

41
    public get statusType() {
42
        if (this.status) {
1✔
43
            // eslint-disable-next-line no-bitwise
44
            return (this.status / 100) | 0;
1✔
45
        }
46

47
        // istanbul ignore next: Only happens on network errors
48
        return null;
49
    }
50

51
    public get text() {
52
        if (this.response) {
9✔
53
            return this.response.text;
9✔
54
        }
55

56
        // istanbul ignore next: Only happens on network errors
57
        return null;
58
    }
59

60
    public get data() {
61
        if (this.response) {
23✔
62
            // Return text if response is of type text/*
63
            if (this.contentType && this.contentType.startsWith('text/')) {
23✔
64
                return this.text;
3✔
65
            }
66

67
            return this.response.body || this.text;
20✔
68
        }
69

70
        // istanbul ignore next: Only happens on network errors
71
        return null;
72
    }
73

74
    public get contentType() {
75
        if (this.headers) {
44✔
76
            return this.headers['content-type'];
44✔
77
        }
78

79
        // istanbul ignore next: Only happens on network errors
80
        return null;
81
    }
82

83
    public get headers(): HeadersObject {
84
        if (this.response) {
90✔
85
            return this.response.headers;
90✔
86
        }
87

88
        // istanbul ignore next: Only happens on network errors
89
        return {};
90
    }
91

92
    public get wasAborted(): boolean {
93
        return this.hasError && this.error && this.error.name === 'AbortError';
3✔
94
    }
95
}
96

97
function parseMethod(method: AllowedMethods) {
98
    switch (method) {
31✔
99
        case 'get':
100
            return 'GET';
23✔
101

102
        case 'head':
103
            return 'HEAD';
1✔
104

105
        case 'put':
106
            return 'PUT';
1✔
107

108
        case 'patch':
109
            return 'PATCH';
1✔
110

111
        case 'post':
112
            return 'POST';
3✔
113

114
        case 'del':
115
        case 'delete':
116
            return 'DELETE';
1✔
117

118
        default:
119
            return method.toUpperCase();
1✔
120
    }
121
}
122

123
function parseHeaders(headers: Headers): HeadersObject {
124
    const headersObject: HeadersObject = {};
28✔
125

126
    headers.forEach((value, key) => {
28✔
127
        headersObject[key] = value;
192✔
128
    });
129

130
    return headersObject;
28✔
131
}
132

133
function parseFetchResponse(
134
    response: Response,
135
    req: Request
136
): Promise<FetchResponseInterface> {
137
    // Content type will not be required when there is no content
138
    // This is only valid for HEAD requests and status_code=204
139
    if (response.status === 204 || req.method.toLowerCase() === 'head') {
28✔
140
        return Promise.resolve({
2✔
141
            status: response.status,
142
            headers: parseHeaders(response.headers),
143
            // Respond with same values that superagent produces
144
            body: response.status === 204 ? null : {},
2✔
145
            text: response.status === 204 ? null : '{}',
2✔
146
        });
147
    }
148

149
    // Get content string to use correct parser
150
    const contentType: string = (response.headers.get('content-type') ||
26!
151
        'text/plain') as string;
152

153
    if (contentType.includes('application/json')) {
26✔
154
        return response.json().then((body: any) => ({
22✔
155
            status: response.status,
156
            headers: parseHeaders(response.headers),
157
            text: JSON.stringify(body),
158
            body,
159
        }));
160
    }
161

162
    // form urlencoded data get's converted to object
163
    if (contentType.includes('application/x-www-form-urlencoded')) {
4✔
164
        return response.text().then((body: any) => ({
1✔
165
            status: response.status,
166
            headers: parseHeaders(response.headers),
167
            text: body,
168
            body: qs.parse(body),
169
        }));
170
    }
171

172
    // Fallback parsing scheme is text
173
    return response.text().then((body: any) => ({
3✔
174
        status: response.status,
175
        headers: parseHeaders(response.headers),
176
        text: body,
177
        body,
178
    }));
179
}
180

181
export class FetchResource<
1✔
182
    Params extends Kwargs | null = Kwargs,
183
    TFetchResponse = any,
184
    TPostPayload extends ObjectMap | string | null = any,
185
    TPostResponse = TFetchResponse
186
> extends Resource<Params, TFetchResponse, TPostPayload, TPostResponse> {
187
    protected createRequest<TPayload extends ObjectMap | string | null = any>(
188
        method: AllowedMethods,
189
        url: string,
190
        query: Query,
191
        data: TPayload | string | null,
192
        attachments: Attachments,
193
        requestConfig: RequestConfig
194
    ): any {
195
        let headers: { [key: string]: any } | null = null;
31✔
196
        let body: string | FormData | null = null;
31✔
197
        let contentType: string | null = null;
31✔
198
        if (data) {
31✔
199
            if (isString(data)) {
6✔
200
                contentType = 'application/x-www-form-urlencoded';
1✔
201
                body = data;
1✔
202
            } else if (attachments) {
5✔
203
                const form = new FormData();
1✔
204

205
                attachments.forEach((attachment) => {
1✔
206
                    form.append(
1✔
207
                        attachment.field,
208
                        attachment.file as any,
209
                        attachment.name
210
                    );
211
                });
212

213
                // Set all the fields
214
                Object.keys(data).forEach((fieldKey) => {
1✔
215
                    const value = data[fieldKey];
7✔
216

217
                    // Future: Make this logic configurable
218
                    if (hasValue(value)) {
7✔
219
                        if (isArray(value)) {
5✔
220
                            // Send arrays as multipart arrays
221
                            value.forEach((fValue) => {
1✔
222
                                form.append(`${fieldKey}[]`, fValue);
2✔
223
                            });
224
                        } else if (isObject(value)) {
4✔
225
                            // Posting objects as stringifyed field contents to keep things consistent
226
                            form.append(fieldKey, JSON.stringify(value));
1✔
227
                        } else {
228
                            // Convert values via their toString
229
                            form.append(fieldKey, `${value}`);
3✔
230
                        }
231
                    }
232
                });
233

234
                body = form;
1✔
235

236
                if ('getHeaders' in (form as any)) {
1!
UNCOV
237
                    headers = (form as any).getHeaders();
×
238
                }
239
            } else {
240
                body = JSON.stringify(data);
4✔
241
                contentType = 'application/json';
4✔
242
            }
243
        }
244

245
        let theUrl = url;
31✔
246
        if (query) {
31✔
247
            // eslint-disable-next-line prefer-destructuring
248
            const querySerializeOptions: qs.IStringifyOptions | undefined =
249
                this.config(requestConfig).querySerializeOptions;
1✔
250
            theUrl = `${theUrl}?${qs.stringify(query, querySerializeOptions)}`;
1✔
251
        }
252

253
        let credentials: 'omit' | 'include' = 'omit';
31✔
254
        if (this.config(requestConfig).withCredentials) {
31✔
255
            credentials = 'include';
1✔
256
        }
257

258
        const { signal } = this.config(requestConfig);
31✔
259

260
        const req = new Request(theUrl, {
31✔
261
            method: parseMethod(method),
262
            redirect: 'follow',
263
            credentials,
264
            body,
265
            signal: signal || null,
60✔
266
        });
267

268
        if (hasValue(headers)) {
31!
UNCOV
269
            Object.keys(headers).forEach((key) => {
×
UNCOV
270
                if (hasValue(headers)) {
×
UNCOV
271
                    req.headers.set(key, headers[key]);
×
272
                }
273
            });
274
        }
275

276
        if (contentType) {
31✔
277
            req.headers.set('content-type', contentType);
5✔
278
        }
279

280
        return req;
31✔
281
    }
282

283
    protected doRequest(
284
        req: Request,
285
        resolve: (response: any, error: any) => void
286
    ): void {
287
        fetch(req)
31✔
288
            .then((res) => parseFetchResponse(res, req))
28✔
289
            .then((response) => {
290
                resolve(response, null);
28✔
291
            })
292
            .catch((error) => {
293
                resolve(null, error);
3✔
294
            });
295
    }
296

297
    protected setHeader(req: Request, key: string, value: string | null): any {
298
        if (value) {
38✔
299
            req.headers.set(key, value);
38✔
300
        }
301

302
        return req;
38✔
303
    }
304

305
    protected wrapResponse(
306
        res: FetchResponseInterface | null,
307
        error: any,
308
        req?: Request
309
    ): ResponseInterface {
310
        return new FetchResponse(res, error, req);
31✔
311
    }
312
}
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