@mittwald/api-client-commons 4.466.0 → 4.468.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.
@@ -1,3 +1,4 @@
1
+ import { serializeDates } from "./serializeDates.js";
1
2
  export class OpenAPIPath {
2
3
  rawPath;
3
4
  params;
@@ -12,7 +13,7 @@ export class OpenAPIPath {
12
13
  const asEntries = Object.entries(params ?? {});
13
14
  const finalPath = asEntries.reduce((path, entry) => {
14
15
  const [key, value] = entry;
15
- return path.replace(`{${key}}`, encodeURIComponent(value));
16
+ return path.replace(`{${key}}`, encodeURIComponent(String(serializeDates(value))));
16
17
  }, path);
17
18
  return finalPath.startsWith("/") ? finalPath.substring(1) : finalPath;
18
19
  }
@@ -1,4 +1,5 @@
1
1
  import OpenAPIPath from "./OpenAPIPath.js";
2
+ import { serializeDates } from "./serializeDates.js";
2
3
  import { AxiosError, } from "axios";
3
4
  export class Request {
4
5
  operationDescriptor;
@@ -31,20 +32,24 @@ export class Request {
31
32
  }
32
33
  buildAxiosConfig() {
33
34
  const { method, path } = this.operationDescriptor;
34
- const pathParameters = this.requestObject;
35
+ /**
36
+ * Any `Date` passed by the caller – in the body, in query, path or header
37
+ * parameters, nested at any depth – is converted to an ISO 8601 string
38
+ * here. The original request object is left untouched.
39
+ */
40
+ const requestObject = serializeDates(this.requestObject);
41
+ const pathParameters = requestObject;
35
42
  const openApiPath = new OpenAPIPath(path, pathParameters);
36
43
  const url = openApiPath.buildUrl();
37
- const data = this.requestObject && "data" in this.requestObject
38
- ? this.requestObject.data
39
- : undefined;
40
- const headersConfig = this.requestObject && "headers" in this.requestObject
41
- ? this.requestObject.headers
44
+ const data = requestObject && "data" in requestObject ? requestObject.data : undefined;
45
+ const headersConfig = requestObject && "headers" in requestObject
46
+ ? requestObject.headers
42
47
  : undefined;
43
48
  const headers = headersConfig
44
49
  ? this.makeAxiosHeaders(headersConfig)
45
50
  : undefined;
46
- const queryParametersConfig = this.requestObject && "queryParameters" in this.requestObject
47
- ? this.requestObject.queryParameters
51
+ const queryParametersConfig = requestObject && "queryParameters" in requestObject
52
+ ? requestObject.queryParameters
48
53
  : undefined;
49
54
  const params = this.convertQueryToUrlSearchParams(queryParametersConfig);
50
55
  return {
@@ -63,7 +68,10 @@ export class Request {
63
68
  };
64
69
  }
65
70
  makeAxiosHeaders(headers) {
66
- return Object.fromEntries(Object.entries(headers).map(([key, value]) => [key, value?.toString()]));
71
+ return Object.fromEntries(Object.entries(serializeDates(headers)).map(([key, value]) => [
72
+ key,
73
+ value?.toString(),
74
+ ]));
67
75
  }
68
76
  convertQueryToUrlSearchParams(query) {
69
77
  if (query === undefined || query === null) {
@@ -77,7 +85,7 @@ export class Request {
77
85
  }
78
86
  if (typeof query === "object") {
79
87
  const searchParams = new URLSearchParams();
80
- for (const [key, value] of Object.entries(query)) {
88
+ for (const [key, value] of Object.entries(serializeDates(query))) {
81
89
  if (value === undefined) {
82
90
  continue;
83
91
  }
@@ -50,4 +50,71 @@ describe("query parameters", () => {
50
50
  });
51
51
  expect(query).toBe("foo=1&bar=true&baz=%7B%22some%22%3A%22value%22%7D");
52
52
  });
53
+ test("Date parameter", () => {
54
+ const query = executeRequest({
55
+ since: new Date("2024-10-12T09:08:07.006Z"),
56
+ });
57
+ expect(query).toBe("since=2024-10-12T09%3A08%3A07.006Z");
58
+ });
59
+ test("Array of Dates", () => {
60
+ const query = executeRequest({
61
+ at: [new Date("2024-10-12T09:08:07.006Z")],
62
+ });
63
+ expect(query).toBe("at=2024-10-12T09%3A08%3A07.006Z");
64
+ });
65
+ });
66
+ describe("dates in requests", () => {
67
+ const date = new Date("2024-10-12T09:08:07.006Z");
68
+ const iso = "2024-10-12T09:08:07.006Z";
69
+ const op = {
70
+ path: "/things/{at}",
71
+ operationId: "test",
72
+ method: "POST",
73
+ };
74
+ const executeRequest = (
75
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
76
+ requestObject) => {
77
+ const request = new Request(op, requestObject);
78
+ request.execute(mockedAxios);
79
+ return requestFn.mock.calls[0][0];
80
+ };
81
+ test("Dates in the request body are serialized to ISO 8601", () => {
82
+ const config = executeRequest({
83
+ at: "now",
84
+ data: {
85
+ terminationTargetDate: date,
86
+ nested: { at: date, list: [{ at: date }] },
87
+ untouched: "10/12/2024",
88
+ nothing: null,
89
+ },
90
+ });
91
+ expect(config.data).toEqual({
92
+ terminationTargetDate: iso,
93
+ nested: { at: iso, list: [{ at: iso }] },
94
+ untouched: "10/12/2024",
95
+ nothing: null,
96
+ });
97
+ });
98
+ test("Dates in path parameters are serialized to ISO 8601", () => {
99
+ const config = executeRequest({ at: date, data: {} });
100
+ expect(config.url).toBe(`things/${encodeURIComponent(iso)}`);
101
+ });
102
+ test("Dates in headers are serialized to ISO 8601", () => {
103
+ const config = executeRequest({
104
+ at: "now",
105
+ data: {},
106
+ headers: { "x-since": date },
107
+ });
108
+ expect(config.headers).toEqual({ "x-since": iso });
109
+ });
110
+ test("The request object passed in is not mutated", () => {
111
+ const requestObject = { at: "now", data: { at: date } };
112
+ executeRequest(requestObject);
113
+ expect(requestObject.data.at).toBe(date);
114
+ });
115
+ test("Non-plain payloads are passed through by reference", () => {
116
+ const data = new URLSearchParams({ a: "b" });
117
+ const config = executeRequest({ at: "now", data });
118
+ expect(config.data).toBe(data);
119
+ });
53
120
  });
@@ -2,3 +2,4 @@ export * from "./ApiClientBase.js";
2
2
  export * from "./ApiClientError.js";
3
3
  export * from "./OpenAPIPath.js";
4
4
  export * from "./Request.js";
5
+ export * from "./serializeDates.js";
@@ -0,0 +1,30 @@
1
+ const isPlainObject = (value) => {
2
+ const prototype = Object.getPrototypeOf(value);
3
+ return prototype === Object.prototype || prototype === null;
4
+ };
5
+ /**
6
+ * Recursively replaces every `Date` in a request payload with its ISO 8601
7
+ * representation, so that callers may pass a JS `Date` wherever the API expects
8
+ * a `format: date-time` string.
9
+ *
10
+ * The conversion is deliberately schema-agnostic – the runtime has no access to
11
+ * the OpenAPI schema of the operation being called. Everything that is not a
12
+ * `Date`, a plain object or an array is returned as-is (and by reference), so
13
+ * payloads like `FormData`, `Blob`, `ArrayBuffer` or streams are never touched
14
+ * or cloned.
15
+ *
16
+ * The input is never mutated.
17
+ */
18
+ export const serializeDates = (value) => {
19
+ if (value instanceof Date) {
20
+ return value.toISOString();
21
+ }
22
+ if (Array.isArray(value)) {
23
+ return value.map(serializeDates);
24
+ }
25
+ if (typeof value === "object" && value !== null && isPlainObject(value)) {
26
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, serializeDates(entry)]));
27
+ }
28
+ return value;
29
+ };
30
+ export default serializeDates;
@@ -0,0 +1,66 @@
1
+ import { serializeDates } from "./serializeDates.js";
2
+ const date = new Date("2024-10-12T09:08:07.006Z");
3
+ const iso = "2024-10-12T09:08:07.006Z";
4
+ describe("serializeDates", () => {
5
+ test("converts a Date to an ISO 8601 string", () => {
6
+ expect(serializeDates(date)).toBe(iso);
7
+ });
8
+ test("converts Dates nested in objects", () => {
9
+ expect(serializeDates({ data: { terminationTargetDate: date } })).toEqual({
10
+ data: { terminationTargetDate: iso },
11
+ });
12
+ });
13
+ test("converts Dates in arrays", () => {
14
+ expect(serializeDates({ dates: [date, date] })).toEqual({
15
+ dates: [iso, iso],
16
+ });
17
+ });
18
+ test("converts Dates in arrays of objects", () => {
19
+ expect(serializeDates([{ at: date }])).toEqual([{ at: iso }]);
20
+ });
21
+ test("passes strings through unchanged", () => {
22
+ expect(serializeDates({ at: iso })).toEqual({ at: iso });
23
+ });
24
+ test("keeps null and undefined", () => {
25
+ expect(serializeDates({ a: null, b: undefined })).toEqual({
26
+ a: null,
27
+ b: undefined,
28
+ });
29
+ expect(serializeDates(null)).toBeNull();
30
+ expect(serializeDates(undefined)).toBeUndefined();
31
+ });
32
+ test("keeps other primitives", () => {
33
+ expect(serializeDates({ a: 1, b: true, c: "x" })).toEqual({
34
+ a: 1,
35
+ b: true,
36
+ c: "x",
37
+ });
38
+ });
39
+ test("leaves non-plain objects untouched and by reference", () => {
40
+ class Custom {
41
+ at = date;
42
+ }
43
+ const custom = new Custom();
44
+ const params = new URLSearchParams({ a: "b" });
45
+ const bytes = new Uint8Array([1, 2, 3]);
46
+ const map = new Map([["a", date]]);
47
+ const result = serializeDates({ custom, params, bytes, map });
48
+ expect(result.custom).toBe(custom);
49
+ expect(result.params).toBe(params);
50
+ expect(result.bytes).toBe(bytes);
51
+ expect(result.map).toBe(map);
52
+ });
53
+ test("does not mutate the input", () => {
54
+ const input = { data: { at: date, list: [date] } };
55
+ serializeDates(input);
56
+ expect(input.data.at).toBe(date);
57
+ expect(input.data.list[0]).toBe(date);
58
+ });
59
+ test("handles objects without a prototype", () => {
60
+ const input = Object.assign(Object.create(null), { at: date });
61
+ expect(serializeDates(input)).toEqual({ at: iso });
62
+ });
63
+ test("handles an invalid Date by throwing, as Date#toISOString does", () => {
64
+ expect(() => serializeDates(new Date(NaN))).toThrow(RangeError);
65
+ });
66
+ });
@@ -2,3 +2,4 @@ export * from "./ApiClientBase.js";
2
2
  export * from "./ApiClientError.js";
3
3
  export * from "./OpenAPIPath.js";
4
4
  export * from "./Request.js";
5
+ export * from "./serializeDates.js";
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Recursively replaces every `Date` in a request payload with its ISO 8601
3
+ * representation, so that callers may pass a JS `Date` wherever the API expects
4
+ * a `format: date-time` string.
5
+ *
6
+ * The conversion is deliberately schema-agnostic – the runtime has no access to
7
+ * the OpenAPI schema of the operation being called. Everything that is not a
8
+ * `Date`, a plain object or an array is returned as-is (and by reference), so
9
+ * payloads like `FormData`, `Blob`, `ArrayBuffer` or streams are never touched
10
+ * or cloned.
11
+ *
12
+ * The input is never mutated.
13
+ */
14
+ export declare const serializeDates: <T>(value: T) => T;
15
+ export default serializeDates;
@@ -0,0 +1 @@
1
+ export {};
@@ -4,10 +4,14 @@ export type HttpMediaType = string;
4
4
  type SafeHttpMethod = "GET" | "HEAD" | "OPTIONS";
5
5
  type UnsafeHttpMethod = "PUT" | "DELETE" | "POST" | "PATCH";
6
6
  export type HttpMethod = SafeHttpMethod | UnsafeHttpMethod;
7
- type HeaderValue = string | number | boolean;
7
+ /**
8
+ * `Date` is accepted wherever the API expects a `format: date-time` string; it
9
+ * is serialized to ISO 8601 before the request is sent (see `serializeDates`).
10
+ */
11
+ type HeaderValue = string | number | boolean | Date;
8
12
  export type HttpHeaders = Partial<{
9
13
  [TKey: string]: HeaderValue | HeaderValue[];
10
14
  }>;
11
- export type PathParameters = Record<string, string | number>;
15
+ export type PathParameters = Record<string, string | number | Date>;
12
16
  export type QueryParameters = Record<string, unknown>;
13
17
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mittwald/api-client-commons",
3
- "version": "4.466.0",
3
+ "version": "4.468.0",
4
4
  "author": "Mittwald CM Service GmbH & Co. KG <opensource@mittwald.de>",
5
5
  "type": "module",
6
6
  "description": "Common types and utilities for mittwald API clients",
@@ -79,5 +79,5 @@
79
79
  "optional": true
80
80
  }
81
81
  },
82
- "gitHead": "58405d84af2171a4f823242baa066bfad61dfcff"
82
+ "gitHead": "83c2cdc00238d69632f94b07a4b38b80142f733a"
83
83
  }