@equinor/fusion-framework-module-http 6.1.0 → 6.2.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.
Files changed (33) hide show
  1. package/CHANGELOG.md +145 -0
  2. package/README.md +49 -0
  3. package/dist/esm/configurator.js +7 -2
  4. package/dist/esm/configurator.js.map +1 -1
  5. package/dist/esm/lib/operators/capitalize-request-method.operator.js +17 -0
  6. package/dist/esm/lib/operators/capitalize-request-method.operator.js.map +1 -0
  7. package/dist/esm/lib/operators/fetch-request.schema.js +109 -0
  8. package/dist/esm/lib/operators/fetch-request.schema.js.map +1 -0
  9. package/dist/esm/lib/operators/index.js +2 -0
  10. package/dist/esm/lib/operators/index.js.map +1 -1
  11. package/dist/esm/lib/operators/process-operators.js +10 -0
  12. package/dist/esm/lib/operators/process-operators.js.map +1 -1
  13. package/dist/esm/lib/operators/request-validation.operator.js +29 -0
  14. package/dist/esm/lib/operators/request-validation.operator.js.map +1 -0
  15. package/dist/esm/version.js +1 -1
  16. package/dist/tsconfig.tsbuildinfo +1 -1
  17. package/dist/types/lib/operators/capitalize-request-method.operator.d.ts +10 -0
  18. package/dist/types/lib/operators/fetch-request.schema.d.ts +170 -0
  19. package/dist/types/lib/operators/index.d.ts +2 -0
  20. package/dist/types/lib/operators/process-operators.d.ts +7 -0
  21. package/dist/types/lib/operators/request-validation.operator.d.ts +33 -0
  22. package/dist/types/lib/operators/types.d.ts +6 -0
  23. package/dist/types/version.d.ts +1 -1
  24. package/package.json +2 -1
  25. package/src/configurator.ts +13 -4
  26. package/src/lib/operators/capitalize-request-method.operator.ts +22 -0
  27. package/src/lib/operators/fetch-request.schema.ts +119 -0
  28. package/src/lib/operators/index.ts +2 -0
  29. package/src/lib/operators/process-operators.ts +11 -0
  30. package/src/lib/operators/request-validation.operator.ts +50 -0
  31. package/src/lib/operators/types.ts +7 -0
  32. package/src/version.ts +1 -1
  33. package/tests/operators.test.ts +101 -0
@@ -0,0 +1,10 @@
1
+ import type { ProcessOperator } from './types';
2
+ /**
3
+ * Ensures that the HTTP method of the given request is in uppercase.
4
+ *
5
+ * @param request - The HTTP request object to process.
6
+ * @returns A new request object with the HTTP method in uppercase.
7
+ */
8
+ export declare const capitalizeRequestMethodOperator: <T extends RequestInit>(options?: {
9
+ silent?: boolean;
10
+ }) => ProcessOperator<T>;
@@ -0,0 +1,170 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * Validates that the provided HTTP method string is in uppercase.
4
+ *
5
+ * @link https://www.rfc-editor.org/rfc/rfc7231#section-4.1
6
+ */
7
+ export declare const requestMethodCasing: () => z.ZodType<string>;
8
+ /**
9
+ * Creates a Zod enum schema for HTTP request methods.
10
+ *
11
+ * The schema validates that the value is one of the standard HTTP methods:
12
+ * 'GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD', 'CONNECT', 'TRACE'.
13
+ *
14
+ * If the validation fails, a custom error message is returned, indicating the
15
+ * expected methods and the received value. The error message also references
16
+ * RFC 2616 for more information.
17
+ *
18
+ * @link https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html
19
+ */
20
+ export declare const requestMethodVerb: () => z.ZodEnum<["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD", "CONNECT", "TRACE"]>;
21
+ export declare const requestMethod: () => z.ZodPipeline<z.ZodType<string, z.ZodTypeDef, string>, z.ZodEnum<["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD", "CONNECT", "TRACE"]>>;
22
+ /**
23
+ * Schema for validating the initialization options of a request.
24
+ *
25
+ * @link https://developer.mozilla.org/en-US/docs/Web/API/Request/Request
26
+ *
27
+ * This schema is used to ensure that the request options conform to the expected structure and types.
28
+ */
29
+ export declare const requestInitSchema: z.ZodObject<{
30
+ attributionReporting: z.ZodOptional<z.ZodObject<{
31
+ eventSourceEligible: z.ZodOptional<z.ZodBoolean>;
32
+ triggerEligible: z.ZodOptional<z.ZodBoolean>;
33
+ }, "strip", z.ZodTypeAny, {
34
+ eventSourceEligible?: boolean | undefined;
35
+ triggerEligible?: boolean | undefined;
36
+ }, {
37
+ eventSourceEligible?: boolean | undefined;
38
+ triggerEligible?: boolean | undefined;
39
+ }>>;
40
+ body: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodType<Blob, z.ZodTypeDef, Blob>, z.ZodType<ArrayBuffer, z.ZodTypeDef, ArrayBuffer>, z.ZodType<FormData, z.ZodTypeDef, FormData>, z.ZodType<URLSearchParams, z.ZodTypeDef, URLSearchParams>, z.ZodType<ReadableStream<unknown>, z.ZodTypeDef, ReadableStream<unknown>>]>>;
41
+ browsingTopics: z.ZodOptional<z.ZodBoolean>;
42
+ cache: z.ZodOptional<z.ZodEnum<["default", "no-store", "reload", "no-cache", "force-cache", "only-if-cached"]>>;
43
+ credentials: z.ZodOptional<z.ZodEnum<["omit", "same-origin", "include"]>>;
44
+ headers: z.ZodUnion<[z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>, z.ZodType<Headers, z.ZodTypeDef, Headers>]>;
45
+ integrity: z.ZodOptional<z.ZodString>;
46
+ keepalive: z.ZodOptional<z.ZodBoolean>;
47
+ method: z.ZodOptional<z.ZodPipeline<z.ZodType<string, z.ZodTypeDef, string>, z.ZodEnum<["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD", "CONNECT", "TRACE"]>>>;
48
+ mode: z.ZodOptional<z.ZodEnum<["same-origin", "cors", "no-cors", "navigate", "websocket"]>>;
49
+ priority: z.ZodOptional<z.ZodEnum<["low", "high", "auto"]>>;
50
+ redirect: z.ZodOptional<z.ZodEnum<["follow", "error", "manual"]>>;
51
+ referrer: z.ZodOptional<z.ZodString>;
52
+ referrerPolicy: z.ZodOptional<z.ZodEnum<["no-referrer", "no-referrer-when-downgrade", "origin", "origin-when-cross-origin", "same-origin", "strict-origin", "strict-origin-when-cross-origin", "unsafe-url"]>>;
53
+ signal: z.ZodOptional<z.ZodType<AbortSignal, z.ZodTypeDef, AbortSignal>>;
54
+ }, "strip", z.ZodTypeAny, {
55
+ body?: string | Blob | ArrayBuffer | FormData | URLSearchParams | ReadableStream<unknown> | undefined;
56
+ cache?: "default" | "force-cache" | "no-cache" | "no-store" | "only-if-cached" | "reload" | undefined;
57
+ credentials?: "include" | "omit" | "same-origin" | undefined;
58
+ headers?: Headers | Record<string, string> | undefined;
59
+ integrity?: string | undefined;
60
+ keepalive?: boolean | undefined;
61
+ method?: "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "OPTIONS" | "HEAD" | "CONNECT" | "TRACE" | undefined;
62
+ mode?: "cors" | "same-origin" | "navigate" | "no-cors" | "websocket" | undefined;
63
+ priority?: "auto" | "high" | "low" | undefined;
64
+ redirect?: "error" | "follow" | "manual" | undefined;
65
+ referrer?: string | undefined;
66
+ referrerPolicy?: "same-origin" | "no-referrer" | "no-referrer-when-downgrade" | "origin" | "origin-when-cross-origin" | "strict-origin" | "strict-origin-when-cross-origin" | "unsafe-url" | undefined;
67
+ signal?: AbortSignal | undefined;
68
+ attributionReporting?: {
69
+ eventSourceEligible?: boolean | undefined;
70
+ triggerEligible?: boolean | undefined;
71
+ } | undefined;
72
+ browsingTopics?: boolean | undefined;
73
+ }, {
74
+ body?: string | Blob | ArrayBuffer | FormData | URLSearchParams | ReadableStream<unknown> | undefined;
75
+ cache?: "default" | "force-cache" | "no-cache" | "no-store" | "only-if-cached" | "reload" | undefined;
76
+ credentials?: "include" | "omit" | "same-origin" | undefined;
77
+ headers?: Headers | Record<string, string> | undefined;
78
+ integrity?: string | undefined;
79
+ keepalive?: boolean | undefined;
80
+ method?: string | undefined;
81
+ mode?: "cors" | "same-origin" | "navigate" | "no-cors" | "websocket" | undefined;
82
+ priority?: "auto" | "high" | "low" | undefined;
83
+ redirect?: "error" | "follow" | "manual" | undefined;
84
+ referrer?: string | undefined;
85
+ referrerPolicy?: "same-origin" | "no-referrer" | "no-referrer-when-downgrade" | "origin" | "origin-when-cross-origin" | "strict-origin" | "strict-origin-when-cross-origin" | "unsafe-url" | undefined;
86
+ signal?: AbortSignal | undefined;
87
+ attributionReporting?: {
88
+ eventSourceEligible?: boolean | undefined;
89
+ triggerEligible?: boolean | undefined;
90
+ } | undefined;
91
+ browsingTopics?: boolean | undefined;
92
+ }>;
93
+ /**
94
+ * Schema for validating fetch request configurations.
95
+ *
96
+ * This schema extends the `requestInitSchema` and adds additional properties:
97
+ * - `uri`: A required string representing the URI of the request.
98
+ * - `path`: An optional string representing the path of the request.
99
+ */
100
+ export declare const fetchRequestSchema: z.ZodObject<z.objectUtil.extendShape<{
101
+ attributionReporting: z.ZodOptional<z.ZodObject<{
102
+ eventSourceEligible: z.ZodOptional<z.ZodBoolean>;
103
+ triggerEligible: z.ZodOptional<z.ZodBoolean>;
104
+ }, "strip", z.ZodTypeAny, {
105
+ eventSourceEligible?: boolean | undefined;
106
+ triggerEligible?: boolean | undefined;
107
+ }, {
108
+ eventSourceEligible?: boolean | undefined;
109
+ triggerEligible?: boolean | undefined;
110
+ }>>;
111
+ body: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodType<Blob, z.ZodTypeDef, Blob>, z.ZodType<ArrayBuffer, z.ZodTypeDef, ArrayBuffer>, z.ZodType<FormData, z.ZodTypeDef, FormData>, z.ZodType<URLSearchParams, z.ZodTypeDef, URLSearchParams>, z.ZodType<ReadableStream<unknown>, z.ZodTypeDef, ReadableStream<unknown>>]>>;
112
+ browsingTopics: z.ZodOptional<z.ZodBoolean>;
113
+ cache: z.ZodOptional<z.ZodEnum<["default", "no-store", "reload", "no-cache", "force-cache", "only-if-cached"]>>;
114
+ credentials: z.ZodOptional<z.ZodEnum<["omit", "same-origin", "include"]>>;
115
+ headers: z.ZodUnion<[z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>, z.ZodType<Headers, z.ZodTypeDef, Headers>]>;
116
+ integrity: z.ZodOptional<z.ZodString>;
117
+ keepalive: z.ZodOptional<z.ZodBoolean>;
118
+ method: z.ZodOptional<z.ZodPipeline<z.ZodType<string, z.ZodTypeDef, string>, z.ZodEnum<["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD", "CONNECT", "TRACE"]>>>;
119
+ mode: z.ZodOptional<z.ZodEnum<["same-origin", "cors", "no-cors", "navigate", "websocket"]>>;
120
+ priority: z.ZodOptional<z.ZodEnum<["low", "high", "auto"]>>;
121
+ redirect: z.ZodOptional<z.ZodEnum<["follow", "error", "manual"]>>;
122
+ referrer: z.ZodOptional<z.ZodString>;
123
+ referrerPolicy: z.ZodOptional<z.ZodEnum<["no-referrer", "no-referrer-when-downgrade", "origin", "origin-when-cross-origin", "same-origin", "strict-origin", "strict-origin-when-cross-origin", "unsafe-url"]>>;
124
+ signal: z.ZodOptional<z.ZodType<AbortSignal, z.ZodTypeDef, AbortSignal>>;
125
+ }, {
126
+ uri: z.ZodString;
127
+ path: z.ZodOptional<z.ZodString>;
128
+ }>, "strip", z.ZodTypeAny, {
129
+ uri: string;
130
+ body?: string | Blob | ArrayBuffer | FormData | URLSearchParams | ReadableStream<unknown> | undefined;
131
+ path?: string | undefined;
132
+ cache?: "default" | "force-cache" | "no-cache" | "no-store" | "only-if-cached" | "reload" | undefined;
133
+ credentials?: "include" | "omit" | "same-origin" | undefined;
134
+ headers?: Headers | Record<string, string> | undefined;
135
+ integrity?: string | undefined;
136
+ keepalive?: boolean | undefined;
137
+ method?: "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "OPTIONS" | "HEAD" | "CONNECT" | "TRACE" | undefined;
138
+ mode?: "cors" | "same-origin" | "navigate" | "no-cors" | "websocket" | undefined;
139
+ priority?: "auto" | "high" | "low" | undefined;
140
+ redirect?: "error" | "follow" | "manual" | undefined;
141
+ referrer?: string | undefined;
142
+ referrerPolicy?: "same-origin" | "no-referrer" | "no-referrer-when-downgrade" | "origin" | "origin-when-cross-origin" | "strict-origin" | "strict-origin-when-cross-origin" | "unsafe-url" | undefined;
143
+ signal?: AbortSignal | undefined;
144
+ attributionReporting?: {
145
+ eventSourceEligible?: boolean | undefined;
146
+ triggerEligible?: boolean | undefined;
147
+ } | undefined;
148
+ browsingTopics?: boolean | undefined;
149
+ }, {
150
+ uri: string;
151
+ body?: string | Blob | ArrayBuffer | FormData | URLSearchParams | ReadableStream<unknown> | undefined;
152
+ path?: string | undefined;
153
+ cache?: "default" | "force-cache" | "no-cache" | "no-store" | "only-if-cached" | "reload" | undefined;
154
+ credentials?: "include" | "omit" | "same-origin" | undefined;
155
+ headers?: Headers | Record<string, string> | undefined;
156
+ integrity?: string | undefined;
157
+ keepalive?: boolean | undefined;
158
+ method?: string | undefined;
159
+ mode?: "cors" | "same-origin" | "navigate" | "no-cors" | "websocket" | undefined;
160
+ priority?: "auto" | "high" | "low" | undefined;
161
+ redirect?: "error" | "follow" | "manual" | undefined;
162
+ referrer?: string | undefined;
163
+ referrerPolicy?: "same-origin" | "no-referrer" | "no-referrer-when-downgrade" | "origin" | "origin-when-cross-origin" | "strict-origin" | "strict-origin-when-cross-origin" | "unsafe-url" | undefined;
164
+ signal?: AbortSignal | undefined;
165
+ attributionReporting?: {
166
+ eventSourceEligible?: boolean | undefined;
167
+ triggerEligible?: boolean | undefined;
168
+ } | undefined;
169
+ browsingTopics?: boolean | undefined;
170
+ }>;
@@ -1,4 +1,6 @@
1
1
  export { HttpRequestHandler } from './http-request-handler';
2
2
  export { HttpResponseHandler } from './http-response-handler';
3
3
  export { ProcessOperators } from './process-operators';
4
+ export { capitalizeRequestMethodOperator } from './capitalize-request-method.operator';
5
+ export { requestValidationOperator } from './request-validation.operator';
4
6
  export * from './types';
@@ -40,6 +40,13 @@ export declare class ProcessOperators<T> implements IProcessOperators<T> {
40
40
  * @returns The instance of ProcessOperators for chaining.
41
41
  */
42
42
  set(key: string, operator: ProcessOperator<T>): ProcessOperators<T>;
43
+ /**
44
+ * Removes an operator from the collection by its key.
45
+ *
46
+ * @param key - The key of the operator to remove.
47
+ * @returns The current instance of `ProcessOperators` for method chaining.
48
+ */
49
+ remove(key: string): ProcessOperators<T>;
43
50
  /**
44
51
  * Retrieves an operator from the collection by its key.
45
52
  * @param key The key of the operator to retrieve.
@@ -0,0 +1,33 @@
1
+ import type { ProcessOperator } from './types';
2
+ import type { FetchRequest } from '../client/types';
3
+ /**
4
+ * Validates the given request using the `requestInitSchema`.
5
+ *
6
+ * By default, the validation is not strict, meaning that additional properties
7
+ * not defined in the schema will be allowed and passed through without causing validation errors.
8
+ * Also the operator will not modify the request object, it will only log an error message if the validation fails.
9
+ *
10
+ * @link https://developer.mozilla.org/en-US/docs/Web/API/Request/Request
11
+ *
12
+ * @param request - The request object to be validated.
13
+ * @returns The validated request object if validation is successful.
14
+ * @throws Will log an error message if the request validation fails.
15
+ */
16
+ export declare const requestValidationOperator: <T extends FetchRequest>(options?: {
17
+ /**
18
+ * When enabled, the function will return the parsed result.
19
+ * This means that if the request object passes validation,
20
+ * the parsed and potentially transformed request object will be returned.
21
+ * If this option is not enabled, the function will not return anything
22
+ * even if the request object is valid.
23
+ */
24
+ parse?: boolean;
25
+ /**
26
+ * When set to true, the validation will be strict, meaning that any additional properties
27
+ * not defined in the schema will cause the validation to fail. If set to false or omitted,
28
+ * additional properties will be allowed and passed through without causing validation errors.
29
+ *
30
+ * @note this option is only applicable when the `parse` option is enabled.
31
+ */
32
+ strict?: boolean;
33
+ }) => ProcessOperator<T>;
@@ -35,6 +35,12 @@ export interface IProcessOperators<T> {
35
35
  * @returns The updated collection of process operators.
36
36
  */
37
37
  set(key: string, operator: ProcessOperator<T>): IProcessOperators<T>;
38
+ /**
39
+ * Removes a process operator from the collection.
40
+ * @param key The key of the operator to remove.
41
+ * @returns The updated collection of process operators.
42
+ */
43
+ remove(key: string): IProcessOperators<T>;
38
44
  /**
39
45
  * Gets a process operator from the collection.
40
46
  * @param key The key of the operator to retrieve.
@@ -1 +1 @@
1
- export declare const version = "6.1.0";
1
+ export declare const version = "6.2.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@equinor/fusion-framework-module-http",
3
- "version": "6.1.0",
3
+ "version": "6.2.0",
4
4
  "description": "",
5
5
  "main": "dist/esm/index.js",
6
6
  "types": "index.d.ts",
@@ -55,6 +55,7 @@
55
55
  },
56
56
  "dependencies": {
57
57
  "rxjs": "^7.8.1",
58
+ "zod": "^3.23.8",
58
59
  "@equinor/fusion-framework-module": "^4.3.5",
59
60
  "@equinor/fusion-framework-module-msal": "^3.1.5"
60
61
  },
@@ -1,4 +1,8 @@
1
- import { HttpRequestHandler } from './lib/operators';
1
+ import {
2
+ capitalizeRequestMethodOperator,
3
+ requestValidationOperator,
4
+ HttpRequestHandler,
5
+ } from './lib/operators';
2
6
 
3
7
  import type { FetchRequest, IHttpClient } from './lib/client';
4
8
  import type { IHttpRequestHandler, IHttpResponseHandler } from './lib/operators';
@@ -132,9 +136,14 @@ export class HttpClientConfigurator<TClient extends IHttpClient>
132
136
  readonly defaultHttpClientCtor: HttpClientConstructor<TClient>;
133
137
 
134
138
  /** default request handler for http clients, applied on creation */
135
- readonly defaultHttpRequestHandler = new HttpRequestHandler<
136
- HttpClientRequestInitType<TClient>
137
- >();
139
+ readonly defaultHttpRequestHandler = new HttpRequestHandler<HttpClientRequestInitType<TClient>>(
140
+ {
141
+ // convert all request methods to uppercase
142
+ ['capitalize-method']: capitalizeRequestMethodOperator(),
143
+ // validate the request object
144
+ ['request-validation']: requestValidationOperator(),
145
+ },
146
+ );
138
147
 
139
148
  /**
140
149
  * Create a instance of http configuration
@@ -0,0 +1,22 @@
1
+ import { requestMethodCasing } from './fetch-request.schema';
2
+ import type { ProcessOperator } from './types';
3
+
4
+ /**
5
+ * Ensures that the HTTP method of the given request is in uppercase.
6
+ *
7
+ * @param request - The HTTP request object to process.
8
+ * @returns A new request object with the HTTP method in uppercase.
9
+ */
10
+ export const capitalizeRequestMethodOperator =
11
+ <T extends RequestInit>(options?: { silent?: boolean }): ProcessOperator<T> =>
12
+ (request): T => {
13
+ const { error, success, data } = requestMethodCasing().safeParse(request.method);
14
+
15
+ request.method = success ? data : request.method?.toUpperCase();
16
+
17
+ if (error && !options?.silent) {
18
+ error.errors.forEach((e) => console.warn(e.message));
19
+ }
20
+
21
+ return request;
22
+ };
@@ -0,0 +1,119 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * Validates that the provided HTTP method string is in uppercase.
5
+ *
6
+ * @link https://www.rfc-editor.org/rfc/rfc7231#section-4.1
7
+ */
8
+ export const requestMethodCasing = (): z.ZodType<string> => {
9
+ return z.custom<string>(
10
+ (value?: string) => value === value?.toUpperCase(),
11
+ (value: string) => ({
12
+ code: z.ZodIssueCode.custom,
13
+ validation: 'uppercase',
14
+ path: ['method'],
15
+ message: [
16
+ `Provided HTTP method '${value}' must be in uppercase.`,
17
+ 'See RFC 7231 Section 4.1 for more information',
18
+ 'https://www.rfc-editor.org/rfc/rfc7231#section-4.1',
19
+ ].join(' '),
20
+ }),
21
+ );
22
+ };
23
+
24
+ /**
25
+ * Creates a Zod enum schema for HTTP request methods.
26
+ *
27
+ * The schema validates that the value is one of the standard HTTP methods:
28
+ * 'GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD', 'CONNECT', 'TRACE'.
29
+ *
30
+ * If the validation fails, a custom error message is returned, indicating the
31
+ * expected methods and the received value. The error message also references
32
+ * RFC 2616 for more information.
33
+ *
34
+ * @link https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html
35
+ */
36
+ export const requestMethodVerb = () => {
37
+ return z.enum(
38
+ ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD', 'CONNECT', 'TRACE'],
39
+ {
40
+ errorMap: (error) => {
41
+ const { received, options } = error as z.ZodInvalidEnumValueIssue;
42
+ return {
43
+ message: [
44
+ 'Invalid request method.',
45
+ `Expected '${options.join(' | ')}', but received '${received}'.`,
46
+ 'See RFC 2615 Section 9 for more information',
47
+ 'https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html',
48
+ ].join(' '),
49
+ };
50
+ },
51
+ },
52
+ );
53
+ };
54
+
55
+ export const requestMethod = () => requestMethodCasing().pipe(requestMethodVerb());
56
+
57
+ /**
58
+ * Schema for validating the initialization options of a request.
59
+ *
60
+ * @link https://developer.mozilla.org/en-US/docs/Web/API/Request/Request
61
+ *
62
+ * This schema is used to ensure that the request options conform to the expected structure and types.
63
+ */
64
+ export const requestInitSchema = z.object({
65
+ attributionReporting: z
66
+ .object({
67
+ eventSourceEligible: z.boolean().optional(),
68
+ triggerEligible: z.boolean().optional(),
69
+ })
70
+ .optional(),
71
+ body: z
72
+ .union([
73
+ z.string(),
74
+ z.instanceof(Blob),
75
+ z.instanceof(ArrayBuffer),
76
+ z.instanceof(FormData),
77
+ z.instanceof(URLSearchParams),
78
+ z.instanceof(ReadableStream),
79
+ ])
80
+ .optional(),
81
+ browsingTopics: z.boolean().optional(),
82
+ cache: z
83
+ .enum(['default', 'no-store', 'reload', 'no-cache', 'force-cache', 'only-if-cached'])
84
+ .optional(),
85
+ credentials: z.enum(['omit', 'same-origin', 'include']).optional(),
86
+ headers: z.record(z.string(), z.string()).optional().or(z.instanceof(Headers)),
87
+ integrity: z.string().optional(),
88
+ keepalive: z.boolean().optional(),
89
+ method: requestMethod().optional(),
90
+ mode: z.enum(['same-origin', 'cors', 'no-cors', 'navigate', 'websocket']).optional(),
91
+ priority: z.enum(['low', 'high', 'auto']).optional(),
92
+ redirect: z.enum(['follow', 'error', 'manual']).optional(),
93
+ referrer: z.string().optional(),
94
+ referrerPolicy: z
95
+ .enum([
96
+ 'no-referrer',
97
+ 'no-referrer-when-downgrade',
98
+ 'origin',
99
+ 'origin-when-cross-origin',
100
+ 'same-origin',
101
+ 'strict-origin',
102
+ 'strict-origin-when-cross-origin',
103
+ 'unsafe-url',
104
+ ])
105
+ .optional(),
106
+ signal: z.instanceof(AbortSignal).optional(),
107
+ });
108
+
109
+ /**
110
+ * Schema for validating fetch request configurations.
111
+ *
112
+ * This schema extends the `requestInitSchema` and adds additional properties:
113
+ * - `uri`: A required string representing the URI of the request.
114
+ * - `path`: An optional string representing the path of the request.
115
+ */
116
+ export const fetchRequestSchema = requestInitSchema.extend({
117
+ uri: z.string(),
118
+ path: z.string().optional(),
119
+ });
@@ -1,5 +1,7 @@
1
1
  export { HttpRequestHandler } from './http-request-handler';
2
2
  export { HttpResponseHandler } from './http-response-handler';
3
3
  export { ProcessOperators } from './process-operators';
4
+ export { capitalizeRequestMethodOperator } from './capitalize-request-method.operator';
5
+ export { requestValidationOperator } from './request-validation.operator';
4
6
 
5
7
  export * from './types';
@@ -63,6 +63,17 @@ export class ProcessOperators<T> implements IProcessOperators<T> {
63
63
  return this;
64
64
  }
65
65
 
66
+ /**
67
+ * Removes an operator from the collection by its key.
68
+ *
69
+ * @param key - The key of the operator to remove.
70
+ * @returns The current instance of `ProcessOperators` for method chaining.
71
+ */
72
+ remove(key: string): ProcessOperators<T> {
73
+ delete this._operators[key];
74
+ return this;
75
+ }
76
+
66
77
  /**
67
78
  * Retrieves an operator from the collection by its key.
68
79
  * @param key The key of the operator to retrieve.
@@ -0,0 +1,50 @@
1
+ import { z } from 'zod';
2
+ import type { ProcessOperator } from './types';
3
+ import type { FetchRequest } from '../client/types';
4
+ import { fetchRequestSchema } from './fetch-request.schema';
5
+
6
+ /**
7
+ * Validates the given request using the `requestInitSchema`.
8
+ *
9
+ * By default, the validation is not strict, meaning that additional properties
10
+ * not defined in the schema will be allowed and passed through without causing validation errors.
11
+ * Also the operator will not modify the request object, it will only log an error message if the validation fails.
12
+ *
13
+ * @link https://developer.mozilla.org/en-US/docs/Web/API/Request/Request
14
+ *
15
+ * @param request - The request object to be validated.
16
+ * @returns The validated request object if validation is successful.
17
+ * @throws Will log an error message if the request validation fails.
18
+ */
19
+ export const requestValidationOperator =
20
+ <T extends FetchRequest>(options?: {
21
+ /**
22
+ * When enabled, the function will return the parsed result.
23
+ * This means that if the request object passes validation,
24
+ * the parsed and potentially transformed request object will be returned.
25
+ * If this option is not enabled, the function will not return anything
26
+ * even if the request object is valid.
27
+ */
28
+ parse?: boolean;
29
+ /**
30
+ * When set to true, the validation will be strict, meaning that any additional properties
31
+ * not defined in the schema will cause the validation to fail. If set to false or omitted,
32
+ * additional properties will be allowed and passed through without causing validation errors.
33
+ *
34
+ * @note this option is only applicable when the `parse` option is enabled.
35
+ */
36
+ strict?: boolean;
37
+ }): ProcessOperator<T> =>
38
+ (request) => {
39
+ const { strict, parse } = options ?? {};
40
+ const schema = strict ? fetchRequestSchema : fetchRequestSchema.passthrough();
41
+ try {
42
+ const result = schema.parse(request) as T;
43
+ return parse ? result : void 0;
44
+ } catch (error) {
45
+ if (parse) {
46
+ throw error;
47
+ }
48
+ console.error('Invalid request options', (error as z.ZodError).message);
49
+ }
50
+ };
@@ -40,6 +40,13 @@ export interface IProcessOperators<T> {
40
40
  */
41
41
  set(key: string, operator: ProcessOperator<T>): IProcessOperators<T>;
42
42
 
43
+ /**
44
+ * Removes a process operator from the collection.
45
+ * @param key The key of the operator to remove.
46
+ * @returns The updated collection of process operators.
47
+ */
48
+ remove(key: string): IProcessOperators<T>;
49
+
43
50
  /**
44
51
  * Gets a process operator from the collection.
45
52
  * @param key The key of the operator to retrieve.
package/src/version.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  // Generated by genversion.
2
- export const version = '6.1.0';
2
+ export const version = '6.2.0';
@@ -0,0 +1,101 @@
1
+ /* eslint-disable @typescript-eslint/ban-ts-comment */
2
+ import { describe, it, expect, vi } from 'vitest';
3
+ import {
4
+ capitalizeRequestMethodOperator,
5
+ ProcessOperator,
6
+ requestValidationOperator,
7
+ } from '../src/lib/operators';
8
+ import { FetchRequest } from '../src/lib';
9
+
10
+ const executeOperator = <R, O extends ProcessOperator<FetchRequest, R>>(
11
+ operator: O,
12
+ request: Parameters<O>[0],
13
+ ): Promise<ReturnType<O>> => {
14
+ return new Promise((resolve, rejects) => {
15
+ try {
16
+ resolve(operator(request) as ReturnType<O>);
17
+ } catch (error) {
18
+ rejects(error);
19
+ }
20
+ });
21
+ };
22
+
23
+ describe('capitalizeRequestMethodOperator', () => {
24
+ it('should capitalize request method', async () => {
25
+ const consoleWarn = vi.spyOn(console, 'warn').mockImplementationOnce((msg) => {
26
+ expect(msg).toMatch(/RFC 7231/);
27
+ });
28
+
29
+ const operator = capitalizeRequestMethodOperator();
30
+
31
+ const result = executeOperator(operator, { method: 'get' });
32
+
33
+ expect(result).resolves.toMatchObject({ method: 'GET' });
34
+ expect(consoleWarn).toHaveBeenCalled();
35
+ });
36
+ });
37
+
38
+ describe('requestValidationOperator', () => {
39
+ /**
40
+ * A mock FetchRequest object used for testing purposes.
41
+ */
42
+ const mockRequest: FetchRequest = Object.freeze({
43
+ method: 'GET',
44
+ uri: 'https://foo.bar',
45
+ path: 'api',
46
+ });
47
+
48
+ it('should pass validation for valid request parameters', async () => {
49
+ const operator = requestValidationOperator();
50
+
51
+ const result = executeOperator(operator, mockRequest);
52
+
53
+ expect(result).resolves.toBeUndefined();
54
+ });
55
+
56
+ it('should parse valid request parameters', async () => {
57
+ const operator = requestValidationOperator({ parse: true });
58
+
59
+ const result = executeOperator(operator, mockRequest);
60
+
61
+ expect(result).resolves.toStrictEqual(mockRequest);
62
+ });
63
+
64
+ it('should allow additional properties when strict validation is disabled', async () => {
65
+ const operator = requestValidationOperator({ parse: true });
66
+
67
+ const result = executeOperator(operator, {
68
+ ...mockRequest,
69
+ // @ts-expect-error
70
+ additionalProperty: 'some-value',
71
+ });
72
+
73
+ expect(result).resolves.toMatchObject(mockRequest);
74
+ expect(result).resolves.toHaveProperty('additionalProperty', 'some-value');
75
+ });
76
+
77
+ it('should remove additional properties when strict validation is enabled', async () => {
78
+ const operator = requestValidationOperator({ parse: true, strict: true });
79
+
80
+ const result = executeOperator(operator, {
81
+ ...mockRequest,
82
+ // @ts-expect-error
83
+ additionalProperty: 'some-value',
84
+ method: 'GET',
85
+ });
86
+
87
+ expect(result).resolves.toStrictEqual(mockRequest);
88
+ expect(result).resolves.not.toHaveProperty('additionalProperty');
89
+ });
90
+
91
+ it('should throw an error for invalid request parameters', async () => {
92
+ const operator = requestValidationOperator({ parse: true });
93
+
94
+ const result = executeOperator(operator, {
95
+ ...mockRequest,
96
+ method: 'GETS', // invalid method
97
+ });
98
+
99
+ expect(result).rejects.toThrowError('RFC 2615');
100
+ });
101
+ });