@odata2ts/http-client-fetch 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/CHANGELOG.md ADDED
@@ -0,0 +1,10 @@
1
+ # Change Log
2
+
3
+ All notable changes to this project will be documented in this file.
4
+ See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
+
6
+ # 0.1.0 (2023-05-29)
7
+
8
+ ### Features
9
+
10
+ * **fetch:** full fetch client implementation ([a8e5fb7](https://github.com/odata2ts/http-client/commit/a8e5fb73594cf2d446eefc69e77b8b5e4bcae1ca))
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 odata2ts
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,45 @@
1
+ [![npm (scoped)](https://img.shields.io/npm/v/@odata2ts/http-client-fetch?style=for-the-badge)](https://www.npmjs.com/package/@odata2ts/http-client-fetch)
2
+
3
+ # Fetch OData HTTP Client
4
+ Fetch based HTTP client for [odata2ts](https://github.com/odata2ts/odata2ts) realizing the OData communication.
5
+ This client uses - as its name suggests - [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)
6
+ for realizing the HTTP communication.
7
+
8
+ It supports:
9
+
10
+ - request configuration
11
+ - automatic CSRF token handling
12
+
13
+ ## Installation
14
+
15
+ Install package `@odata2ts/http-client-fetch` as runtime dependency:
16
+
17
+ ```bash
18
+ npm install --save @odata2ts/http-client-fetch
19
+ ```
20
+
21
+ ## Documentation
22
+ [Fetch OData Client](https://odata2ts.github.io/docs/http-client/fetch)
23
+
24
+ Main documentation for the odata2ts eco system:
25
+ [https://odata2ts.github.io](https://odata2ts.github.io/)
26
+
27
+ ## Tests
28
+ See folder [test](https://github.com/odata2ts/http-client/tree/main/packages/fetch/test)
29
+ for unit tests.
30
+
31
+ See folder [int-test](https://github.com/odata2ts/http-client/tree/main/packages/fetch/int-test) for
32
+ integration tests.
33
+
34
+
35
+ ## Support, Feedback, Contributing
36
+
37
+ This project is open to feature requests, suggestions, bug reports, usage questions etc.
38
+ via [GitHub issues](https://github.com/odata2ts/http-client/issues).
39
+
40
+ Contributions and feedback are encouraged and always welcome.
41
+
42
+ See the [contribution guidelines](https://github.com/odata2ts/http-client/blob/main/CONTRIBUTING.md) for further information.
43
+
44
+ ## License
45
+ MIT - see [License](./LICENSE).
@@ -0,0 +1,28 @@
1
+ import { HttpResponseModel, ODataClient } from "@odata2ts/odata-client-api";
2
+ import { FetchRequestConfig } from "./FetchRequestConfig";
3
+ export type ErrorMessageRetriever = (errorResponse: any) => string | undefined;
4
+ export interface ClientOptions {
5
+ useCsrfProtection?: boolean;
6
+ csrfTokenFetchUrl?: string;
7
+ }
8
+ export declare const getV2OrV4ErrorMessage: ErrorMessageRetriever;
9
+ export declare const DEFAULT_ERROR_MESSAGE = "No error message!";
10
+ export declare class FetchODataClient implements ODataClient<FetchRequestConfig> {
11
+ private clientOptions?;
12
+ private readonly config;
13
+ private csrfToken;
14
+ private retrieveErrorMessage;
15
+ constructor(config?: FetchRequestConfig, clientOptions?: ClientOptions | undefined);
16
+ setErrorMessageRetriever(getErrorMsg: ErrorMessageRetriever): void;
17
+ private setupSecurityToken;
18
+ private fetchSecurityToken;
19
+ private sendRequest;
20
+ private getResponseBody;
21
+ private prepareData;
22
+ get<ResponseModel>(url: string, requestConfig?: FetchRequestConfig): Promise<HttpResponseModel<ResponseModel>>;
23
+ post<ResponseModel>(url: string, data: any, requestConfig?: FetchRequestConfig): Promise<HttpResponseModel<ResponseModel>>;
24
+ put<ResponseModel>(url: string, data: any, requestConfig?: FetchRequestConfig): Promise<HttpResponseModel<ResponseModel>>;
25
+ patch<ResponseModel>(url: string, data: any, requestConfig?: FetchRequestConfig): Promise<HttpResponseModel<ResponseModel>>;
26
+ merge<ResponseModel>(url: string, data: any, requestConfig?: FetchRequestConfig): Promise<HttpResponseModel<ResponseModel>>;
27
+ delete(url: string, requestConfig?: FetchRequestConfig): Promise<HttpResponseModel<void>>;
28
+ }
@@ -0,0 +1,148 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FetchODataClient = exports.DEFAULT_ERROR_MESSAGE = exports.getV2OrV4ErrorMessage = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const FetchODataClientError_1 = require("./FetchODataClientError");
6
+ const FetchRequestConfig_1 = require("./FetchRequestConfig");
7
+ const getV2OrV4ErrorMessage = (errorResponse) => {
8
+ var _a;
9
+ const eMsg = (_a = errorResponse === null || errorResponse === void 0 ? void 0 : errorResponse.error) === null || _a === void 0 ? void 0 : _a.message;
10
+ return typeof (eMsg === null || eMsg === void 0 ? void 0 : eMsg.value) === "string" ? eMsg.value : eMsg;
11
+ };
12
+ exports.getV2OrV4ErrorMessage = getV2OrV4ErrorMessage;
13
+ exports.DEFAULT_ERROR_MESSAGE = "No error message!";
14
+ const FETCH_FAILURE_MESSAGE = "OData request failed entirely: ";
15
+ const JSON_RETRIEVAL_FAILURE_MESSAGE = "Retrieving JSON body from OData response failed: ";
16
+ const RESPONSE_FAILURE_MESSAGE = "OData server responded with error: ";
17
+ function buildErrorMessage(prefix, error) {
18
+ const msg = typeof error === "string" ? error : error === null || error === void 0 ? void 0 : error.message;
19
+ return prefix + (msg || exports.DEFAULT_ERROR_MESSAGE);
20
+ }
21
+ class FetchODataClient {
22
+ constructor(config, clientOptions) {
23
+ var _a;
24
+ this.clientOptions = clientOptions;
25
+ this.retrieveErrorMessage = exports.getV2OrV4ErrorMessage;
26
+ this.config = (0, FetchRequestConfig_1.getDefaultConfig)(config);
27
+ if (clientOptions && clientOptions.useCsrfProtection && !((_a = clientOptions.csrfTokenFetchUrl) === null || _a === void 0 ? void 0 : _a.trim())) {
28
+ throw new Error("When automatic CSRF token fetching is activated, the URL must be supplied with attribute [csrfTokenFetchUrl]!");
29
+ }
30
+ }
31
+ setErrorMessageRetriever(getErrorMsg) {
32
+ this.retrieveErrorMessage = getErrorMsg;
33
+ }
34
+ setupSecurityToken() {
35
+ return tslib_1.__awaiter(this, void 0, void 0, function* () {
36
+ if (!this.csrfToken) {
37
+ this.csrfToken = yield this.fetchSecurityToken();
38
+ }
39
+ return this.csrfToken;
40
+ });
41
+ }
42
+ fetchSecurityToken() {
43
+ return tslib_1.__awaiter(this, void 0, void 0, function* () {
44
+ const fetchUrl = this.clientOptions.csrfTokenFetchUrl;
45
+ const response = yield this.get(fetchUrl, { headers: { "x-csrf-token": "Fetch" } });
46
+ return response.headers["x-csrf-token"];
47
+ });
48
+ }
49
+ sendRequest(url, config, requestConfig) {
50
+ var _a, _b;
51
+ return tslib_1.__awaiter(this, void 0, void 0, function* () {
52
+ // noinspection SuspiciousTypeOfGuard
53
+ if (typeof url !== "string") {
54
+ throw new Error("Value for URL must be provided!");
55
+ }
56
+ const mergedConfig = (0, FetchRequestConfig_1.mergeFetchConfig)(this.config, requestConfig, config);
57
+ // setup automatic CSRF token handling
58
+ if (((_a = this.clientOptions) === null || _a === void 0 ? void 0 : _a.useCsrfProtection) &&
59
+ mergedConfig.method &&
60
+ ["POST", "PUT", "PATCH", "DELETE"].includes(mergedConfig.method.toUpperCase())) {
61
+ const csrfToken = yield this.setupSecurityToken();
62
+ if (typeof csrfToken === "string") {
63
+ mergedConfig.headers.set("x-csrf-token", csrfToken);
64
+ }
65
+ }
66
+ // the actual request
67
+ let response;
68
+ try {
69
+ response = yield fetch(url, mergedConfig);
70
+ }
71
+ catch (fetchError) {
72
+ throw new FetchODataClientError_1.FetchODataClientError(buildErrorMessage(FETCH_FAILURE_MESSAGE, fetchError), undefined, fetchError);
73
+ }
74
+ // error response
75
+ if (!response.ok) {
76
+ // automatic CSRF token handling
77
+ if (((_b = this.clientOptions) === null || _b === void 0 ? void 0 : _b.useCsrfProtection) &&
78
+ response.status === 403 &&
79
+ response.headers.get("x-csrf-token") === "Required") {
80
+ // csrf token expired, let's reset it and perform the original request again
81
+ this.csrfToken = undefined;
82
+ return this.sendRequest(url, config, requestConfig);
83
+ }
84
+ let data = yield this.getResponseBody(response, false);
85
+ const errMsg = this.retrieveErrorMessage(data);
86
+ throw new FetchODataClientError_1.FetchODataClientError(buildErrorMessage(RESPONSE_FAILURE_MESSAGE, errMsg), response.status, new Error(errMsg || exports.DEFAULT_ERROR_MESSAGE), response);
87
+ }
88
+ const data = yield this.getResponseBody(response, true);
89
+ // header
90
+ // Impl Note: No entries prop available, otherwise as one liner Array.from(response.headers.entries)
91
+ const headers = {};
92
+ response.headers.forEach((value, key) => {
93
+ headers[key] = value;
94
+ });
95
+ return {
96
+ status: response.status,
97
+ statusText: response.statusText,
98
+ headers,
99
+ data,
100
+ };
101
+ });
102
+ }
103
+ getResponseBody(response, isFailedJsonFatal) {
104
+ return tslib_1.__awaiter(this, void 0, void 0, function* () {
105
+ if (response.status === 204) {
106
+ return undefined;
107
+ }
108
+ try {
109
+ return yield response.json();
110
+ }
111
+ catch (error) {
112
+ if (isFailedJsonFatal) {
113
+ throw new FetchODataClientError_1.FetchODataClientError(buildErrorMessage(JSON_RETRIEVAL_FAILURE_MESSAGE, error), response.status, error);
114
+ }
115
+ return undefined;
116
+ }
117
+ });
118
+ }
119
+ prepareData(data) {
120
+ return JSON.stringify(data);
121
+ }
122
+ get(url, requestConfig) {
123
+ return this.sendRequest(url, { method: "GET" }, requestConfig);
124
+ }
125
+ post(url, data, requestConfig) {
126
+ return this.sendRequest(url, { body: this.prepareData(data), method: "POST" }, requestConfig);
127
+ }
128
+ put(url, data, requestConfig) {
129
+ return this.sendRequest(url, { body: this.prepareData(data), method: "PUT" }, requestConfig);
130
+ }
131
+ patch(url, data, requestConfig) {
132
+ return this.sendRequest(url, { body: this.prepareData(data), method: "PATCH" }, requestConfig);
133
+ }
134
+ merge(url, data, requestConfig) {
135
+ return this.sendRequest(url, {
136
+ body: this.prepareData(data),
137
+ method: "POST",
138
+ headers: {
139
+ "X-Http-Method": "MERGE",
140
+ },
141
+ }, requestConfig);
142
+ }
143
+ delete(url, requestConfig) {
144
+ return this.sendRequest(url, { method: "DELETE" }, requestConfig);
145
+ }
146
+ }
147
+ exports.FetchODataClient = FetchODataClient;
148
+ //# sourceMappingURL=FetchODataClient.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FetchODataClient.js","sourceRoot":"","sources":["../src/FetchODataClient.ts"],"names":[],"mappings":"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","sourcesContent":["import { HttpResponseModel, ODataClient } from \"@odata2ts/odata-client-api\";\r\n\r\nimport { FetchODataClientError } from \"./FetchODataClientError\";\r\nimport { FetchRequestConfig, getDefaultConfig, mergeFetchConfig } from \"./FetchRequestConfig\";\r\n\r\nexport type ErrorMessageRetriever = (errorResponse: any) => string | undefined;\r\n\r\nexport interface ClientOptions {\r\n useCsrfProtection?: boolean;\r\n csrfTokenFetchUrl?: string;\r\n}\r\n\r\nexport const getV2OrV4ErrorMessage: ErrorMessageRetriever = (errorResponse: any): string | undefined => {\r\n const eMsg = errorResponse?.error?.message;\r\n return typeof eMsg?.value === \"string\" ? eMsg.value : eMsg;\r\n};\r\n\r\nexport const DEFAULT_ERROR_MESSAGE = \"No error message!\";\r\nconst FETCH_FAILURE_MESSAGE = \"OData request failed entirely: \";\r\nconst JSON_RETRIEVAL_FAILURE_MESSAGE = \"Retrieving JSON body from OData response failed: \";\r\nconst RESPONSE_FAILURE_MESSAGE = \"OData server responded with error: \";\r\n\r\nfunction buildErrorMessage(prefix: string, error: any) {\r\n const msg = typeof error === \"string\" ? error : (error as Error)?.message;\r\n return prefix + (msg || DEFAULT_ERROR_MESSAGE);\r\n}\r\n\r\nexport class FetchODataClient implements ODataClient<FetchRequestConfig> {\r\n private readonly config: RequestInit;\r\n private csrfToken: string | undefined;\r\n private retrieveErrorMessage: ErrorMessageRetriever = getV2OrV4ErrorMessage;\r\n\r\n constructor(config?: FetchRequestConfig, private clientOptions?: ClientOptions) {\r\n this.config = getDefaultConfig(config);\r\n if (clientOptions && clientOptions.useCsrfProtection && !clientOptions.csrfTokenFetchUrl?.trim()) {\r\n throw new Error(\r\n \"When automatic CSRF token fetching is activated, the URL must be supplied with attribute [csrfTokenFetchUrl]!\"\r\n );\r\n }\r\n }\r\n\r\n public setErrorMessageRetriever(getErrorMsg: ErrorMessageRetriever) {\r\n this.retrieveErrorMessage = getErrorMsg;\r\n }\r\n\r\n private async setupSecurityToken() {\r\n if (!this.csrfToken) {\r\n this.csrfToken = await this.fetchSecurityToken();\r\n }\r\n return this.csrfToken;\r\n }\r\n\r\n private async fetchSecurityToken(): Promise<string | undefined> {\r\n const fetchUrl = this.clientOptions!.csrfTokenFetchUrl!;\r\n const response = await this.get(fetchUrl, { headers: { \"x-csrf-token\": \"Fetch\" } });\r\n\r\n return response.headers[\"x-csrf-token\"];\r\n }\r\n\r\n private async sendRequest<ResponseType>(\r\n url: string,\r\n config: RequestInit,\r\n requestConfig?: FetchRequestConfig\r\n ): Promise<HttpResponseModel<ResponseType>> {\r\n // noinspection SuspiciousTypeOfGuard\r\n if (typeof url !== \"string\") {\r\n throw new Error(\"Value for URL must be provided!\");\r\n }\r\n\r\n const mergedConfig = mergeFetchConfig(this.config, requestConfig, config);\r\n\r\n // setup automatic CSRF token handling\r\n if (\r\n this.clientOptions?.useCsrfProtection &&\r\n mergedConfig.method &&\r\n [\"POST\", \"PUT\", \"PATCH\", \"DELETE\"].includes(mergedConfig.method.toUpperCase())\r\n ) {\r\n const csrfToken = await this.setupSecurityToken();\r\n if (typeof csrfToken === \"string\") {\r\n mergedConfig.headers.set(\"x-csrf-token\", csrfToken);\r\n }\r\n }\r\n\r\n // the actual request\r\n let response: Response;\r\n try {\r\n response = await fetch(url, mergedConfig);\r\n } catch (fetchError) {\r\n throw new FetchODataClientError(\r\n buildErrorMessage(FETCH_FAILURE_MESSAGE, fetchError),\r\n undefined,\r\n fetchError as Error\r\n );\r\n }\r\n\r\n // error response\r\n if (!response.ok) {\r\n // automatic CSRF token handling\r\n if (\r\n this.clientOptions?.useCsrfProtection &&\r\n response.status === 403 &&\r\n response.headers.get(\"x-csrf-token\") === \"Required\"\r\n ) {\r\n // csrf token expired, let's reset it and perform the original request again\r\n this.csrfToken = undefined;\r\n return this.sendRequest<ResponseType>(url, config, requestConfig);\r\n }\r\n\r\n let data = await this.getResponseBody(response, false);\r\n const errMsg = this.retrieveErrorMessage(data);\r\n\r\n throw new FetchODataClientError(\r\n buildErrorMessage(RESPONSE_FAILURE_MESSAGE, errMsg),\r\n response.status,\r\n new Error(errMsg || DEFAULT_ERROR_MESSAGE),\r\n response\r\n );\r\n }\r\n\r\n const data = await this.getResponseBody(response, true);\r\n\r\n // header\r\n // Impl Note: No entries prop available, otherwise as one liner Array.from(response.headers.entries)\r\n const headers: { [key: string]: string } = {};\r\n response.headers.forEach((value, key) => {\r\n headers[key] = value;\r\n });\r\n\r\n return {\r\n status: response.status,\r\n statusText: response.statusText,\r\n headers,\r\n data,\r\n };\r\n }\r\n\r\n private async getResponseBody(response: Response, isFailedJsonFatal: boolean) {\r\n if (response.status === 204) {\r\n return undefined;\r\n }\r\n try {\r\n return await response.json();\r\n } catch (error) {\r\n if (isFailedJsonFatal) {\r\n throw new FetchODataClientError(\r\n buildErrorMessage(JSON_RETRIEVAL_FAILURE_MESSAGE, error),\r\n response.status,\r\n error as Error\r\n );\r\n }\r\n return undefined;\r\n }\r\n }\r\n\r\n private prepareData(data: any): string {\r\n return JSON.stringify(data);\r\n }\r\n\r\n public get<ResponseModel>(\r\n url: string,\r\n requestConfig?: FetchRequestConfig\r\n ): Promise<HttpResponseModel<ResponseModel>> {\r\n return this.sendRequest<ResponseModel>(url, { method: \"GET\" }, requestConfig);\r\n }\r\n public post<ResponseModel>(\r\n url: string,\r\n data: any,\r\n requestConfig?: FetchRequestConfig\r\n ): Promise<HttpResponseModel<ResponseModel>> {\r\n return this.sendRequest<ResponseModel>(url, { body: this.prepareData(data), method: \"POST\" }, requestConfig);\r\n }\r\n public put<ResponseModel>(\r\n url: string,\r\n data: any,\r\n requestConfig?: FetchRequestConfig\r\n ): Promise<HttpResponseModel<ResponseModel>> {\r\n return this.sendRequest<ResponseModel>(url, { body: this.prepareData(data), method: \"PUT\" }, requestConfig);\r\n }\r\n public patch<ResponseModel>(\r\n url: string,\r\n data: any,\r\n requestConfig?: FetchRequestConfig\r\n ): Promise<HttpResponseModel<ResponseModel>> {\r\n return this.sendRequest<ResponseModel>(url, { body: this.prepareData(data), method: \"PATCH\" }, requestConfig);\r\n }\r\n public merge<ResponseModel>(\r\n url: string,\r\n data: any,\r\n requestConfig?: FetchRequestConfig\r\n ): Promise<HttpResponseModel<ResponseModel>> {\r\n return this.sendRequest<ResponseModel>(\r\n url,\r\n {\r\n body: this.prepareData(data),\r\n method: \"POST\",\r\n headers: {\r\n \"X-Http-Method\": \"MERGE\",\r\n },\r\n },\r\n requestConfig\r\n );\r\n }\r\n public delete(url: string, requestConfig?: FetchRequestConfig): Promise<HttpResponseModel<void>> {\r\n return this.sendRequest<void>(url, { method: \"DELETE\" }, requestConfig);\r\n }\r\n}\r\n"]}
@@ -0,0 +1,6 @@
1
+ export declare class FetchODataClientError extends Error {
2
+ readonly status?: number | undefined;
3
+ readonly cause?: Error | undefined;
4
+ readonly response?: Response | undefined;
5
+ constructor(message: string, status?: number | undefined, cause?: Error | undefined, response?: Response | undefined);
6
+ }
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FetchODataClientError = void 0;
4
+ class FetchODataClientError extends Error {
5
+ constructor(message, status, cause, response) {
6
+ // @ts-ignore: fetch requires lib "dom" or "webworker", but then the "cause" property becomes unknown to TS
7
+ super(message, { cause });
8
+ this.status = status;
9
+ this.cause = cause;
10
+ this.response = response;
11
+ this.name = this.constructor.name;
12
+ }
13
+ }
14
+ exports.FetchODataClientError = FetchODataClientError;
15
+ //# sourceMappingURL=FetchODataClientError.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FetchODataClientError.js","sourceRoot":"","sources":["../src/FetchODataClientError.ts"],"names":[],"mappings":";;;AAAA,MAAa,qBAAsB,SAAQ,KAAK;IAC9C,YACE,OAAe,EACC,MAAe,EACf,KAAa,EACb,QAAmB;QAEnC,2GAA2G;QAC3G,KAAK,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;QALV,WAAM,GAAN,MAAM,CAAS;QACf,UAAK,GAAL,KAAK,CAAQ;QACb,aAAQ,GAAR,QAAQ,CAAW;QAInC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC;IACpC,CAAC;CACF;AAXD,sDAWC","sourcesContent":["export class FetchODataClientError extends Error {\r\n constructor(\r\n message: string,\r\n public readonly status?: number,\r\n public readonly cause?: Error,\r\n public readonly response?: Response\r\n ) {\r\n // @ts-ignore: fetch requires lib \"dom\" or \"webworker\", but then the \"cause\" property becomes unknown to TS\r\n super(message, { cause });\r\n this.name = this.constructor.name;\r\n }\r\n}\r\n"]}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Available config options for end user when making a given request.
3
+ */
4
+ export interface FetchRequestConfig extends Pick<RequestInit, "credentials" | "cache" | "mode" | "redirect" | "referrerPolicy"> {
5
+ headers?: Record<string, string> | Headers;
6
+ }
7
+ export interface InternalFetchConfig extends Omit<RequestInit, "headers"> {
8
+ headers: Headers;
9
+ }
10
+ export declare function getDefaultConfig(config?: FetchRequestConfig): RequestInit;
11
+ export declare function mergeFetchConfig(): undefined;
12
+ export declare function mergeFetchConfig(...configs: Array<RequestInit | undefined>): InternalFetchConfig;
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.mergeFetchConfig = exports.getDefaultConfig = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const DEFAULT_CONFIG = {
6
+ headers: { Accept: "application/json", "Content-Type": "application/json" },
7
+ cache: "no-cache",
8
+ };
9
+ function getDefaultConfig(config) {
10
+ return mergeFetchConfig(DEFAULT_CONFIG, config);
11
+ }
12
+ exports.getDefaultConfig = getDefaultConfig;
13
+ function mergeFetchConfig(...configs) {
14
+ if (!configs.length) {
15
+ return undefined;
16
+ }
17
+ return configs
18
+ .filter((c) => !!c)
19
+ .reduce((collector, current) => {
20
+ const { headers } = current, passThrough = tslib_1.__rest(current, ["headers"]);
21
+ const collectedHeaders = collector.headers;
22
+ // headers as Headers object
23
+ if (headers && headers instanceof Headers) {
24
+ headers.forEach((val, key) => collectedHeaders.set(key, val));
25
+ }
26
+ // headers as plain Record<string,string>
27
+ else if (headers) {
28
+ Object.entries(headers).forEach(([key, val]) => collectedHeaders.set(key, val));
29
+ }
30
+ return Object.assign(Object.assign({}, collector), passThrough);
31
+ }, { headers: new Headers() });
32
+ }
33
+ exports.mergeFetchConfig = mergeFetchConfig;
34
+ //# sourceMappingURL=FetchRequestConfig.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FetchRequestConfig.js","sourceRoot":"","sources":["../src/FetchRequestConfig.ts"],"names":[],"mappings":";;;;AAAA,MAAM,cAAc,GAAgB;IAClC,OAAO,EAAE,EAAE,MAAM,EAAE,kBAAkB,EAAE,cAAc,EAAE,kBAAkB,EAAE;IAC3E,KAAK,EAAE,UAAU;CAClB,CAAC;AAcF,SAAgB,gBAAgB,CAAC,MAA2B;IAC1D,OAAO,gBAAgB,CAAC,cAAc,EAAE,MAAM,CAAC,CAAC;AAClD,CAAC;AAFD,4CAEC;AAID,SAAgB,gBAAgB,CAAC,GAAG,OAAuC;IACzE,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE;QACnB,OAAO,SAAS,CAAC;KAClB;IACD,OAAO,OAAO;SACX,MAAM,CAAC,CAAC,CAAC,EAAoB,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;SACpC,MAAM,CACL,CAAC,SAAS,EAAE,OAAO,EAAE,EAAE;QACrB,MAAM,EAAE,OAAO,KAAqB,OAAO,EAAvB,WAAW,kBAAK,OAAO,EAArC,WAA2B,CAAU,CAAC;QAC5C,MAAM,gBAAgB,GAAG,SAAS,CAAC,OAAkB,CAAC;QAEtD,4BAA4B;QAC5B,IAAI,OAAO,IAAI,OAAO,YAAY,OAAO,EAAE;YACzC,OAAO,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,CAAC,gBAAgB,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;SAC/D;QACD,yCAAyC;aACpC,IAAI,OAAO,EAAE;YAChB,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,EAAE,EAAE,CAAC,gBAAgB,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;SACjF;QAED,uCAAY,SAAS,GAAK,WAAW,EAAG;IAC1C,CAAC,EACD,EAAE,OAAO,EAAE,IAAI,OAAO,EAAE,EAAE,CAC3B,CAAC;AACN,CAAC;AAxBD,4CAwBC","sourcesContent":["const DEFAULT_CONFIG: RequestInit = {\r\n headers: { Accept: \"application/json\", \"Content-Type\": \"application/json\" },\r\n cache: \"no-cache\",\r\n};\r\n\r\n/**\r\n * Available config options for end user when making a given request.\r\n */\r\nexport interface FetchRequestConfig\r\n extends Pick<RequestInit, \"credentials\" | \"cache\" | \"mode\" | \"redirect\" | \"referrerPolicy\"> {\r\n headers?: Record<string, string> | Headers;\r\n}\r\n\r\nexport interface InternalFetchConfig extends Omit<RequestInit, \"headers\"> {\r\n headers: Headers;\r\n}\r\n\r\nexport function getDefaultConfig(config?: FetchRequestConfig): RequestInit {\r\n return mergeFetchConfig(DEFAULT_CONFIG, config);\r\n}\r\n\r\nexport function mergeFetchConfig(): undefined;\r\nexport function mergeFetchConfig(...configs: Array<RequestInit | undefined>): InternalFetchConfig;\r\nexport function mergeFetchConfig(...configs: Array<RequestInit | undefined>) {\r\n if (!configs.length) {\r\n return undefined;\r\n }\r\n return configs\r\n .filter((c): c is RequestInit => !!c)\r\n .reduce<InternalFetchConfig>(\r\n (collector, current) => {\r\n const { headers, ...passThrough } = current;\r\n const collectedHeaders = collector.headers as Headers;\r\n\r\n // headers as Headers object\r\n if (headers && headers instanceof Headers) {\r\n headers.forEach((val, key) => collectedHeaders.set(key, val));\r\n }\r\n // headers as plain Record<string,string>\r\n else if (headers) {\r\n Object.entries(headers).forEach(([key, val]) => collectedHeaders.set(key, val));\r\n }\r\n\r\n return { ...collector, ...passThrough };\r\n },\r\n { headers: new Headers() }\r\n );\r\n}\r\n"]}
package/lib/index.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from "./FetchODataClient";
2
+ export { FetchODataClientError } from "./FetchODataClientError";
3
+ export { FetchRequestConfig } from "./FetchRequestConfig";
package/lib/index.js ADDED
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FetchODataClientError = void 0;
4
+ const tslib_1 = require("tslib");
5
+ tslib_1.__exportStar(require("./FetchODataClient"), exports);
6
+ var FetchODataClientError_1 = require("./FetchODataClientError");
7
+ Object.defineProperty(exports, "FetchODataClientError", { enumerable: true, get: function () { return FetchODataClientError_1.FetchODataClientError; } });
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;AAAA,6DAAmC;AACnC,iEAAgE;AAAvD,8HAAA,qBAAqB,OAAA","sourcesContent":["export * from \"./FetchODataClient\";\r\nexport { FetchODataClientError } from \"./FetchODataClientError\";\r\nexport { FetchRequestConfig } from \"./FetchRequestConfig\";\r\n"]}
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@odata2ts/http-client-fetch",
3
+ "version": "0.1.0",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "description": "HTTP client based on fetch and consumable by odata2ts",
8
+ "license": "MIT",
9
+ "repository": "git@github.com:odata2ts/http-client.git",
10
+ "author": "texttechne",
11
+ "main": "./lib/index.js",
12
+ "scripts": {
13
+ "build": "yarn clean && yarn compile",
14
+ "check-circular-deps": "madge ./src --extensions ts --circular",
15
+ "clean": "rimraf lib coverage",
16
+ "compile": "tsc",
17
+ "int-test": "jest ./int-test",
18
+ "prepublish": "yarn build",
19
+ "test": "jest ./test"
20
+ },
21
+ "files": [
22
+ "*.md",
23
+ "lib",
24
+ "LICENSE"
25
+ ],
26
+ "keywords": [
27
+ "http client",
28
+ "fetch",
29
+ "odata",
30
+ "ts",
31
+ "odata2ts"
32
+ ],
33
+ "dependencies": {
34
+ "@odata2ts/odata-client-api": "^0.6.3"
35
+ },
36
+ "devDependencies": {
37
+ "@odata2ts/odata-core": "^0.3.7",
38
+ "@types/jest": "^27.4.1",
39
+ "@types/node": "^17.0.23",
40
+ "jest": "^29.5.0",
41
+ "rimraf": "^3.0.2",
42
+ "ts-jest": "^29.1.0",
43
+ "ts-node": "10.7.0",
44
+ "typescript": "5.0.4"
45
+ },
46
+ "types": "./lib/index.d.ts",
47
+ "gitHead": "82a5ba5a0e98450970cf83755d3b08c1ba15cfb4"
48
+ }