@mittwald/api-client-commons 0.0.0-development-04b7288-20240610 → 0.0.0-development-7c0a25b-20240611

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 (65) hide show
  1. package/dist/esm/axios.js +1 -0
  2. package/dist/esm/core/ApiClientBase.js +13 -0
  3. package/dist/esm/core/ApiClientError.js +12 -0
  4. package/dist/esm/core/OpenAPIPath.js +20 -0
  5. package/dist/esm/core/Request.js +76 -0
  6. package/dist/esm/core/Request.test.js +53 -0
  7. package/dist/esm/core/index.js +4 -0
  8. package/dist/esm/index.js +4 -0
  9. package/dist/esm/interceptors/accessToken.js +19 -0
  10. package/dist/esm/interceptors/consistencyHandling.js +27 -0
  11. package/dist/esm/interceptors/index.js +2 -0
  12. package/dist/esm/react/ApiCallAsyncResourceFactory.js +35 -0
  13. package/dist/esm/react/ApiCallAsyncResourceFactory.test-types.js +36 -0
  14. package/dist/esm/react/ApiCallAsyncResourceFactory.test.js +46 -0
  15. package/dist/esm/react/index.js +1 -0
  16. package/dist/esm/react/types.js +1 -0
  17. package/dist/esm/testing/expectStatus.js +7 -0
  18. package/dist/esm/testing/index.js +1 -0
  19. package/dist/esm/types/NullableOnNoRequiredKeysDeep.js +1 -0
  20. package/dist/esm/types/OpenAPIOperation.js +1 -0
  21. package/dist/esm/types/RequestFunction.js +1 -0
  22. package/dist/esm/types/RequestFunction.test-types.js +29 -0
  23. package/dist/esm/types/RequestType.js +1 -0
  24. package/dist/esm/types/RequestType.test-types.js +146 -0
  25. package/dist/esm/types/Response.js +1 -0
  26. package/dist/esm/types/Response.test-types.js +70 -0
  27. package/dist/esm/types/assertOneOfStatus.js +7 -0
  28. package/dist/esm/types/assertStatus.js +5 -0
  29. package/dist/esm/types/assertStatus.test-types.js +8 -0
  30. package/dist/esm/types/http.js +1 -0
  31. package/dist/esm/types/index.js +8 -0
  32. package/dist/esm/types/simplify.js +1 -0
  33. package/dist/types/axios.d.ts +1 -0
  34. package/dist/types/core/ApiClientBase.d.ts +9 -0
  35. package/dist/types/core/ApiClientError.d.ts +7 -0
  36. package/dist/types/core/OpenAPIPath.d.ts +9 -0
  37. package/dist/types/core/Request.d.ts +13 -0
  38. package/dist/types/core/Request.test.d.ts +1 -0
  39. package/dist/types/core/index.d.ts +4 -0
  40. package/dist/types/index.d.ts +4 -0
  41. package/dist/types/interceptors/accessToken.d.ts +4 -0
  42. package/dist/types/interceptors/consistencyHandling.d.ts +3 -0
  43. package/dist/types/interceptors/index.d.ts +2 -0
  44. package/dist/types/react/ApiCallAsyncResourceFactory.d.ts +12 -0
  45. package/dist/types/react/ApiCallAsyncResourceFactory.test-types.d.ts +1 -0
  46. package/dist/types/react/ApiCallAsyncResourceFactory.test.d.ts +1 -0
  47. package/dist/types/react/index.d.ts +1 -0
  48. package/dist/types/react/types.d.ts +3 -0
  49. package/dist/types/testing/expectStatus.d.ts +5 -0
  50. package/dist/types/testing/index.d.ts +1 -0
  51. package/dist/types/types/NullableOnNoRequiredKeysDeep.d.ts +6 -0
  52. package/dist/types/types/OpenAPIOperation.d.ts +20 -0
  53. package/dist/types/types/RequestFunction.d.ts +11 -0
  54. package/dist/types/types/RequestFunction.test-types.d.ts +1 -0
  55. package/dist/types/types/RequestType.d.ts +21 -0
  56. package/dist/types/types/RequestType.test-types.d.ts +1 -0
  57. package/dist/types/types/Response.d.ts +7 -0
  58. package/dist/types/types/Response.test-types.d.ts +1 -0
  59. package/dist/types/types/assertOneOfStatus.d.ts +5 -0
  60. package/dist/types/types/assertStatus.d.ts +5 -0
  61. package/dist/types/types/assertStatus.test-types.d.ts +1 -0
  62. package/dist/types/types/http.d.ts +13 -0
  63. package/dist/types/types/index.d.ts +8 -0
  64. package/dist/types/types/simplify.d.ts +3 -0
  65. package/package.json +2 -2
@@ -0,0 +1 @@
1
+ export * from "axios";
@@ -0,0 +1,13 @@
1
+ import axios, { Axios } from "axios";
2
+ import Request from "./Request.js";
3
+ export class ApiClientBase {
4
+ axios;
5
+ constructor(axiosConfig = axios) {
6
+ this.axios =
7
+ axiosConfig instanceof Axios ? axiosConfig : axios.create(axiosConfig);
8
+ }
9
+ requestFunctionFactory(operation) {
10
+ return (conf) => new Request(operation, conf).execute(this.axios);
11
+ }
12
+ }
13
+ export default ApiClientBase;
@@ -0,0 +1,12 @@
1
+ import { AxiosError } from "axios";
2
+ export class ApiClientError extends AxiosError {
3
+ constructor(message, code, config, request, response) {
4
+ super(message, code, config, request, response);
5
+ Object.setPrototypeOf(this, ApiClientError.prototype);
6
+ this.name = "ApiClientError";
7
+ }
8
+ static fromResponse(message, response) {
9
+ return new ApiClientError(message, undefined, response.config, response.request, response);
10
+ }
11
+ }
12
+ export default ApiClientError;
@@ -0,0 +1,20 @@
1
+ export class OpenAPIPath {
2
+ rawPath;
3
+ params;
4
+ constructor(rawPath, params) {
5
+ this.rawPath = rawPath;
6
+ this.params = params;
7
+ }
8
+ buildUrl() {
9
+ return OpenAPIPath.setPathParams(this.rawPath, this.params);
10
+ }
11
+ static setPathParams(path, params) {
12
+ const asEntries = Object.entries(params ?? {});
13
+ const finalPath = asEntries.reduce((path, entry) => {
14
+ const [key, value] = entry;
15
+ return path.replace(`{${key}}`, encodeURIComponent(value));
16
+ }, path);
17
+ return finalPath.startsWith("/") ? finalPath.substring(1) : finalPath;
18
+ }
19
+ }
20
+ export default OpenAPIPath;
@@ -0,0 +1,76 @@
1
+ import OpenAPIPath from "./OpenAPIPath.js";
2
+ export class Request {
3
+ operationDescriptor;
4
+ requestObject;
5
+ requestConfig;
6
+ constructor(operationDescriptor, requestObject) {
7
+ this.operationDescriptor = operationDescriptor;
8
+ this.requestObject = requestObject;
9
+ this.requestConfig = Object.freeze(this.buildAxiosConfig());
10
+ }
11
+ execute(axios) {
12
+ return axios.request(this.requestConfig);
13
+ }
14
+ buildAxiosConfig() {
15
+ const { method, path } = this.operationDescriptor;
16
+ const pathParameters = this.requestObject;
17
+ const openApiPath = new OpenAPIPath(path, pathParameters);
18
+ const url = openApiPath.buildUrl();
19
+ const data = this.requestObject && "data" in this.requestObject
20
+ ? this.requestObject.data
21
+ : undefined;
22
+ const headersConfig = this.requestObject && "headers" in this.requestObject
23
+ ? this.requestObject.headers
24
+ : undefined;
25
+ const headers = headersConfig
26
+ ? this.makeAxiosHeaders(headersConfig)
27
+ : undefined;
28
+ const queryParametersConfig = this.requestObject && "queryParameters" in this.requestObject
29
+ ? this.requestObject.queryParameters
30
+ : undefined;
31
+ const params = this.convertQueryToUrlSearchParams(queryParametersConfig);
32
+ return {
33
+ url,
34
+ method,
35
+ headers,
36
+ // Must be a plain object or an URLSearchParams object
37
+ params,
38
+ data,
39
+ validateStatus: () => true,
40
+ };
41
+ }
42
+ makeAxiosHeaders(headers) {
43
+ return Object.fromEntries(Object.entries(headers).map(([key, value]) => [key, value?.toString()]));
44
+ }
45
+ convertQueryToUrlSearchParams(query) {
46
+ if (query === undefined || query === null) {
47
+ return undefined;
48
+ }
49
+ if (query instanceof URLSearchParams) {
50
+ return query;
51
+ }
52
+ if (typeof query === "string") {
53
+ return new URLSearchParams(query);
54
+ }
55
+ if (typeof query === "object") {
56
+ const searchParams = new URLSearchParams();
57
+ for (const [key, value] of Object.entries(query)) {
58
+ if (Array.isArray(value)) {
59
+ for (const arrayItem of value) {
60
+ searchParams.append(key, arrayItem);
61
+ }
62
+ }
63
+ else {
64
+ searchParams.append(key, typeof value === "string" ||
65
+ typeof value === "number" ||
66
+ typeof value === "boolean"
67
+ ? value.toString()
68
+ : JSON.stringify(value));
69
+ }
70
+ }
71
+ return searchParams;
72
+ }
73
+ throw new Error(`Unexpected query parameter type (${typeof query})`);
74
+ }
75
+ }
76
+ export default Request;
@@ -0,0 +1,53 @@
1
+ import Request from "./Request.js";
2
+ import { jest } from "@jest/globals";
3
+ const requestFn = jest.fn();
4
+ const mockedAxios = {
5
+ request: requestFn,
6
+ };
7
+ beforeEach(() => {
8
+ jest.resetAllMocks();
9
+ });
10
+ describe("query parameters", () => {
11
+ const op = {
12
+ path: "/",
13
+ operationId: "test",
14
+ method: "GET",
15
+ };
16
+ const executeRequest = (query) => {
17
+ const request = new Request(op, { queryParameters: query });
18
+ request.execute(mockedAxios);
19
+ const requestConfig = requestFn.mock.calls[0][0];
20
+ return requestConfig.params.toString();
21
+ };
22
+ test("Empty query", () => {
23
+ const query = executeRequest({});
24
+ expect(query).toBe("");
25
+ });
26
+ test("Simple parameter", () => {
27
+ const query = executeRequest({
28
+ foo: "bar",
29
+ });
30
+ expect(query).toBe("foo=bar");
31
+ });
32
+ test("Two parameters", () => {
33
+ const query = executeRequest({
34
+ foo: "bar",
35
+ bam: "baz",
36
+ });
37
+ expect(query).toBe("foo=bar&bam=baz");
38
+ });
39
+ test("Array parameters", () => {
40
+ const query = executeRequest({
41
+ foo: ["bar", "bam"],
42
+ });
43
+ expect(query).toBe("foo=bar&foo=bam");
44
+ });
45
+ test("Number, boolean, JSON", () => {
46
+ const query = executeRequest({
47
+ foo: 1,
48
+ bar: true,
49
+ baz: { some: "value" },
50
+ });
51
+ expect(query).toBe("foo=1&bar=true&baz=%7B%22some%22%3A%22value%22%7D");
52
+ });
53
+ });
@@ -0,0 +1,4 @@
1
+ export * from "./ApiClientBase.js";
2
+ export * from "./ApiClientError.js";
3
+ export * from "./OpenAPIPath.js";
4
+ export * from "./Request.js";
@@ -0,0 +1,4 @@
1
+ export * from "./core/index.js";
2
+ export * from "./types/index.js";
3
+ export * from "./axios.js";
4
+ export * from "./interceptors/index.js";
@@ -0,0 +1,19 @@
1
+ import ApiClientBase from "../core/ApiClientBase.js";
2
+ function configureAccessTokenInterceptor(axios, token, headerName = "x-access-token") {
3
+ axios.interceptors.request.use((conf) => {
4
+ if (!conf.headers.has(headerName)) {
5
+ conf.headers.set(headerName, token);
6
+ }
7
+ return conf;
8
+ });
9
+ }
10
+ export function withAccessToken(clientOrAxiosInstance, token, headerName = "x-access-token") {
11
+ if (token) {
12
+ const axiosInstance = clientOrAxiosInstance instanceof ApiClientBase
13
+ ? clientOrAxiosInstance.axios
14
+ : clientOrAxiosInstance;
15
+ configureAccessTokenInterceptor(axiosInstance, token, headerName);
16
+ }
17
+ return clientOrAxiosInstance;
18
+ }
19
+ export default withAccessToken;
@@ -0,0 +1,27 @@
1
+ import ApiClientBase from "../core/ApiClientBase.js";
2
+ function isMutatingRequest(request) {
3
+ return ["post", "put", "delete", "patch"].includes(request.method?.toLowerCase() ?? "");
4
+ }
5
+ function configureConsistencyHandlingInterceptor(axios) {
6
+ let lastEventId = undefined;
7
+ axios.interceptors.request.use((config) => {
8
+ if (lastEventId !== undefined && !isMutatingRequest(config)) {
9
+ config.headers["if-event-reached"] = lastEventId;
10
+ }
11
+ return config;
12
+ });
13
+ axios.interceptors.response.use((response) => {
14
+ const headers = response.headers;
15
+ if (headers.has("etag") && isMutatingRequest(response.config)) {
16
+ lastEventId = headers.get("etag");
17
+ }
18
+ return response;
19
+ });
20
+ }
21
+ export function withEventConsistencyHandling(clientOrAxiosInstance) {
22
+ const axiosInstance = clientOrAxiosInstance instanceof ApiClientBase
23
+ ? clientOrAxiosInstance.axios
24
+ : clientOrAxiosInstance;
25
+ configureConsistencyHandlingInterceptor(axiosInstance);
26
+ return clientOrAxiosInstance;
27
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./accessToken.js";
2
+ export * from "./consistencyHandling.js";
@@ -0,0 +1,35 @@
1
+ import { getAsyncResource } from "@mittwald/react-use-promise";
2
+ import { assertStatus, } from "../types/index.js";
3
+ import Request from "../core/Request.js";
4
+ export class ApiCallAsyncResourceFactory {
5
+ static namespace = "@mittwald/api-client";
6
+ operation;
7
+ requestFn;
8
+ constructor(operation, requestFn) {
9
+ this.operation = operation;
10
+ this.requestFn = requestFn;
11
+ }
12
+ getAsyncResourceId() {
13
+ return `${ApiCallAsyncResourceFactory.namespace}/${this.operation.operationId}`;
14
+ }
15
+ getAsyncResourceTags(request) {
16
+ const url = request.requestConfig.url ?? "";
17
+ return [
18
+ this.getAsyncResourceId(),
19
+ `${ApiCallAsyncResourceFactory.namespace}/${this.operation.method}`,
20
+ `${ApiCallAsyncResourceFactory.namespace}/${url}`,
21
+ ];
22
+ }
23
+ async executeRequest(requestObj) {
24
+ const response = await this.requestFn(requestObj);
25
+ assertStatus(response, 200);
26
+ return response.data;
27
+ }
28
+ getApiResource = ((requestObj) => {
29
+ const request = new Request(this.operation, requestObj);
30
+ return getAsyncResource((requestObj) => this.executeRequest(requestObj), [requestObj], {
31
+ tags: this.getAsyncResourceTags(request),
32
+ loaderId: this.getAsyncResourceId(),
33
+ });
34
+ });
35
+ }
@@ -0,0 +1,36 @@
1
+ import { ApiCallAsyncResourceFactory } from "./ApiCallAsyncResourceFactory.js";
2
+ const getStuff = new ApiCallAsyncResourceFactory({
3
+ operationId: "getStuff",
4
+ path: "/stuff",
5
+ method: "GET",
6
+ }, {});
7
+ function ignoredCheckRequestType() {
8
+ getStuff.getApiResource({
9
+ data: {
10
+ // @ts-expect-error Not matching request type
11
+ foo: "",
12
+ },
13
+ });
14
+ getStuff.getApiResource({
15
+ data: {
16
+ requestString: "",
17
+ },
18
+ });
19
+ }
20
+ function ignoredCheckResponseType() {
21
+ const stuff = getStuff
22
+ .getApiResource({
23
+ data: {
24
+ requestString: "",
25
+ },
26
+ })
27
+ .use();
28
+ // @ts-expect-error Accessing unknown prop
29
+ stuff.foo;
30
+ (function (ignored) {
31
+ // @ts-expect-error is a number
32
+ })(stuff.responseData);
33
+ (function (ignored) {
34
+ // is number
35
+ })(stuff.responseData);
36
+ }
@@ -0,0 +1,46 @@
1
+ import { beforeEach, expect, jest } from "@jest/globals";
2
+ import { ApiCallAsyncResourceFactory } from "./ApiCallAsyncResourceFactory.js";
3
+ import { refresh } from "@mittwald/react-use-promise";
4
+ beforeEach(() => {
5
+ refresh();
6
+ jest.resetAllMocks();
7
+ });
8
+ const requestMock = jest.fn();
9
+ const getStuff = new ApiCallAsyncResourceFactory({
10
+ operationId: "getStuff",
11
+ path: "/stuff",
12
+ method: "GET",
13
+ }, requestMock);
14
+ const testRequest1 = {
15
+ data: {
16
+ foo: "bar",
17
+ },
18
+ };
19
+ const testRequest2 = {
20
+ data: {
21
+ foo: "baz",
22
+ },
23
+ };
24
+ test("Resource loader executes request", async () => {
25
+ await getStuff.getApiResource(testRequest1).load();
26
+ expect(requestMock).toHaveBeenCalledTimes(1);
27
+ const firstRequestParams = requestMock.mock.calls[0][0];
28
+ expect(firstRequestParams).toMatchObject(testRequest1);
29
+ });
30
+ test("Resource is cached under URL", async () => {
31
+ await getStuff.getApiResource(testRequest1).load();
32
+ expect(requestMock).toHaveBeenCalledTimes(1);
33
+ await getStuff.getApiResource(testRequest1).load();
34
+ expect(requestMock).toHaveBeenCalledTimes(1);
35
+ refresh({
36
+ tag: "@mittwald/api-client/stuff",
37
+ });
38
+ await getStuff.getApiResource(testRequest1).load();
39
+ expect(requestMock).toHaveBeenCalledTimes(2);
40
+ });
41
+ test("Resources are different when request object changes", async () => {
42
+ await getStuff.getApiResource(testRequest1).load();
43
+ expect(requestMock).toHaveBeenCalledTimes(1);
44
+ await getStuff.getApiResource(testRequest2).load();
45
+ expect(requestMock).toHaveBeenCalledTimes(2);
46
+ });
@@ -0,0 +1 @@
1
+ export * from "./ApiCallAsyncResourceFactory.js";
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,7 @@
1
+ import { expect } from "@jest/globals";
2
+ import assertStatus from "../types/assertStatus.js";
3
+ export function expectStatus(response, status) {
4
+ expect(response.status).toBe(status);
5
+ assertStatus(response, status);
6
+ }
7
+ export default expectStatus;
@@ -0,0 +1 @@
1
+ export * from "./expectStatus.js";
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,29 @@
1
+ function ignoredTestEmptyRequestTypes() {
2
+ const f = {};
3
+ void f();
4
+ void f({
5
+ headers: {},
6
+ });
7
+ void f({
8
+ headers: { extra: true },
9
+ });
10
+ }
11
+ function ignoredTestOptionalHeadersRequestTypes() {
12
+ const f = {};
13
+ void f();
14
+ void f({
15
+ headers: {},
16
+ });
17
+ void f({
18
+ headers: { extra: true },
19
+ });
20
+ }
21
+ function ignoredTestPathParametersAreInRootOfRequestConfig() {
22
+ const f = {};
23
+ void f({
24
+ foo: "",
25
+ });
26
+ // @ts-expect-error Missing parameter
27
+ void f({});
28
+ }
29
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,146 @@
1
+ import { expectAssignable } from "tsd";
2
+ function ignoredTestEmptyRequestTypes() {
3
+ expectAssignable({});
4
+ // @ts-expect-error Not assignable
5
+ expectAssignable({ extra: true });
6
+ // @ts-expect-error Not assignable
7
+ expectAssignable({ data: {} });
8
+ // @ts-expect-error Not assignable
9
+ expectAssignable({ data: null });
10
+ // @ts-expect-error Not assignable
11
+ expectAssignable({ pathParameters: {} });
12
+ }
13
+ function ignoredTestRequestTypesWithDataType() {
14
+ expectAssignable({ data: { foo: "" } });
15
+ // @ts-expect-error Not assignable
16
+ expectAssignable({});
17
+ expectAssignable({
18
+ // @ts-expect-error Not assignable
19
+ data: { foo: "", extra: "" },
20
+ });
21
+ // @ts-expect-error Not assignable
22
+ expectAssignable({ data: { noFoo: "" } });
23
+ }
24
+ function ignoredTestRequestTypesWithPathParameters() {
25
+ expectAssignable({
26
+ data: { foo: "" },
27
+ pathParameters: { bar: "" },
28
+ });
29
+ expectAssignable({
30
+ pathParameters: { bar: "" },
31
+ });
32
+ // @ts-expect-error Not assignable
33
+ expectAssignable({});
34
+ expectAssignable({
35
+ // @ts-expect-error Not assignable
36
+ pathParameters: {},
37
+ });
38
+ expectAssignable({
39
+ // @ts-expect-error Not assignable
40
+ pathParameters: { foo: "", extra: "" },
41
+ });
42
+ }
43
+ function ignoredTestRequestTypesWithHeader() {
44
+ expectAssignable({
45
+ data: {
46
+ foo: "",
47
+ },
48
+ pathParameters: { bar: "" },
49
+ headers: { baz: "" },
50
+ });
51
+ expectAssignable({
52
+ pathParameters: { bar: "" },
53
+ headers: { baz: "" },
54
+ });
55
+ expectAssignable({
56
+ headers: { baz: "" },
57
+ });
58
+ // @ts-expect-error Not assignable
59
+ expectAssignable({});
60
+ expectAssignable({
61
+ headers: {
62
+ // @ts-expect-error Not assignable
63
+ baz: 42,
64
+ },
65
+ });
66
+ expectAssignable({
67
+ // @ts-expect-error Not assignable
68
+ headers: {},
69
+ });
70
+ expectAssignable({
71
+ // @ts-expect-error Not assignable
72
+ data: {},
73
+ headers: {
74
+ baz: "",
75
+ },
76
+ });
77
+ expectAssignable({
78
+ // @ts-expect-error Not assignable
79
+ pathParameters: {},
80
+ headers: {
81
+ baz: "",
82
+ },
83
+ });
84
+ }
85
+ function ignoredTestRequestTypesWithQuery() {
86
+ expectAssignable({
87
+ data: {
88
+ foo: "",
89
+ },
90
+ pathParameters: { bar: "" },
91
+ headers: { baz: "" },
92
+ queryParameters: {
93
+ whut: "",
94
+ },
95
+ });
96
+ expectAssignable({
97
+ pathParameters: { bar: "" },
98
+ headers: { baz: "" },
99
+ queryParameters: {
100
+ whut: "",
101
+ },
102
+ });
103
+ expectAssignable({
104
+ queryParameters: {
105
+ whut: "",
106
+ },
107
+ });
108
+ // @ts-expect-error Not assignable
109
+ expectAssignable({});
110
+ expectAssignable({
111
+ queryParameters: {
112
+ // @ts-expect-error Not assignable
113
+ whut: 42,
114
+ },
115
+ });
116
+ expectAssignable({
117
+ // @ts-expect-error Not assignable
118
+ queryParameters: {},
119
+ });
120
+ }
121
+ function ignoredTestAdditionalHeadersCanAlwaysBeSet() {
122
+ expectAssignable({
123
+ headers: { extra: true },
124
+ });
125
+ expectAssignable({
126
+ headers: { extra: true },
127
+ });
128
+ expectAssignable({
129
+ headers: { extra: true },
130
+ });
131
+ expectAssignable({
132
+ data: {
133
+ foo: "",
134
+ },
135
+ headers: { extra: true },
136
+ });
137
+ expectAssignable({
138
+ pathParameters: {
139
+ bar: "",
140
+ },
141
+ headers: { extra: true },
142
+ });
143
+ expectAssignable({
144
+ headers: { extra: true, baz: "" },
145
+ });
146
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,70 @@
1
+ import { expectAssignable, expectNotAssignable, expectType } from "tsd";
2
+ const additionalAxiosResponseData = {
3
+ statusText: "",
4
+ headers: {},
5
+ config: {},
6
+ mediaType: "application/json",
7
+ };
8
+ expectAssignable({
9
+ data: { a: "" },
10
+ status: 200,
11
+ ...additionalAxiosResponseData,
12
+ });
13
+ expectNotAssignable({
14
+ data: { a: "", extra: "!" },
15
+ status: 200,
16
+ ...additionalAxiosResponseData,
17
+ });
18
+ expectAssignable({
19
+ data: {
20
+ b: "",
21
+ },
22
+ status: 201,
23
+ ...additionalAxiosResponseData,
24
+ });
25
+ expectAssignable({
26
+ data: null,
27
+ status: 400,
28
+ ...additionalAxiosResponseData,
29
+ });
30
+ expectNotAssignable({
31
+ data: null,
32
+ status: 42,
33
+ ...additionalAxiosResponseData,
34
+ });
35
+ expectNotAssignable({
36
+ data: null,
37
+ status: 42,
38
+ extra: "!",
39
+ ...additionalAxiosResponseData,
40
+ });
41
+ expectNotAssignable({
42
+ data: { extraContent: "" },
43
+ status: 400,
44
+ ...additionalAxiosResponseData,
45
+ });
46
+ function ignoredTestRequestTypesWithDataPathParameters() {
47
+ const someResponse = {};
48
+ expectType(someResponse.status);
49
+ if (someResponse.status === 200) {
50
+ // @ts-expect-error > a is not in data
51
+ someResponse.data.a;
52
+ // @ts-expect-error > b is not in data
53
+ someResponse.data.b;
54
+ if (someResponse.mediaType === "text/plain") {
55
+ // @ts-expect-error > a is not in data
56
+ someResponse.data.a;
57
+ expectType(someResponse.data.text);
58
+ }
59
+ else {
60
+ // @ts-expect-error > text is not in data
61
+ someResponse.data.text;
62
+ expectType(someResponse.data.a);
63
+ }
64
+ }
65
+ else if (someResponse.status === 201) {
66
+ expectType(someResponse.data.b);
67
+ // @ts-expect-error > a is not in data
68
+ someResponse.data.a;
69
+ }
70
+ }
@@ -0,0 +1,7 @@
1
+ import ApiClientError from "../core/ApiClientError.js";
2
+ export function assertOneOfStatus(response, expectedStatus) {
3
+ if (!expectedStatus.includes(response.status)) {
4
+ throw ApiClientError.fromResponse(`Unexpected response status (expected ${expectedStatus}, got: ${response.status})`, response);
5
+ }
6
+ }
7
+ export default assertOneOfStatus;
@@ -0,0 +1,5 @@
1
+ import assertOneOfStatus from "./assertOneOfStatus.js";
2
+ export function assertStatus(response, expectedStatus) {
3
+ assertOneOfStatus(response, [expectedStatus]);
4
+ }
5
+ export default assertStatus;
@@ -0,0 +1,8 @@
1
+ import { expectAssignable } from "tsd";
2
+ import assertStatus from "./assertStatus.js";
3
+ function ignoredTestAssertStatusAssertsAlsoTheCorrectResponseType() {
4
+ assertStatus(someResponse, 200);
5
+ expectAssignable(someResponse);
6
+ // @ts-expect-error Not assignable
7
+ expectAssignable(someResponse);
8
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,8 @@
1
+ export * from "./RequestType.js";
2
+ export * from "./RequestFunction.js";
3
+ export * from "./Response.js";
4
+ export * from "./OpenAPIOperation.js";
5
+ export * from "./http.js";
6
+ export * from "./simplify.js";
7
+ export * from "./assertStatus.js";
8
+ export * from "./assertOneOfStatus.js";
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export * from "axios";
@@ -0,0 +1,9 @@
1
+ import { AxiosInstance, CreateAxiosDefaults } from "axios";
2
+ import { RequestFunction } from "../types/index.js";
3
+ import { OpenAPIOperation } from "../types/index.js";
4
+ export declare abstract class ApiClientBase {
5
+ axios: AxiosInstance;
6
+ constructor(axiosConfig?: AxiosInstance | CreateAxiosDefaults);
7
+ protected requestFunctionFactory<TOp extends OpenAPIOperation>(operation: TOp): RequestFunction<TOp>;
8
+ }
9
+ export default ApiClientBase;
@@ -0,0 +1,7 @@
1
+ import { AxiosError, AxiosResponse, InternalAxiosRequestConfig } from "axios";
2
+ import { AnyResponse } from "../types/Response.js";
3
+ export declare class ApiClientError<T = unknown, D = unknown> extends AxiosError<T, D> {
4
+ constructor(message?: string, code?: string, config?: InternalAxiosRequestConfig<D>, request?: unknown, response?: AxiosResponse<T, D>);
5
+ static fromResponse(message: string, response: AnyResponse): ApiClientError;
6
+ }
7
+ export default ApiClientError;
@@ -0,0 +1,9 @@
1
+ import { PathParameters } from "../types/http.js";
2
+ export declare class OpenAPIPath {
3
+ private readonly rawPath;
4
+ private readonly params?;
5
+ constructor(rawPath: string, params?: PathParameters);
6
+ buildUrl(): string;
7
+ private static setPathParams;
8
+ }
9
+ export default OpenAPIPath;
@@ -0,0 +1,13 @@
1
+ import { OpenAPIOperation, RequestObject, ResponsePromise } from "../types/index.js";
2
+ import { AxiosInstance, AxiosRequestConfig } from "axios";
3
+ export declare class Request<TOp extends OpenAPIOperation> {
4
+ private readonly operationDescriptor;
5
+ private readonly requestObject?;
6
+ readonly requestConfig: AxiosRequestConfig;
7
+ constructor(operationDescriptor: TOp, requestObject?: RequestObject<TOp>);
8
+ execute(axios: AxiosInstance): ResponsePromise<TOp>;
9
+ private buildAxiosConfig;
10
+ private makeAxiosHeaders;
11
+ private convertQueryToUrlSearchParams;
12
+ }
13
+ export default Request;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,4 @@
1
+ export * from "./ApiClientBase.js";
2
+ export * from "./ApiClientError.js";
3
+ export * from "./OpenAPIPath.js";
4
+ export * from "./Request.js";
@@ -0,0 +1,4 @@
1
+ export * from "./core/index.js";
2
+ export * from "./types/index.js";
3
+ export * from "./axios.js";
4
+ export * from "./interceptors/index.js";
@@ -0,0 +1,4 @@
1
+ import { AxiosInstance } from "axios";
2
+ import ApiClientBase from "../core/ApiClientBase.js";
3
+ export declare function withAccessToken<T extends ApiClientBase | AxiosInstance>(clientOrAxiosInstance: T, token: string | undefined, headerName?: string): T;
4
+ export default withAccessToken;
@@ -0,0 +1,3 @@
1
+ import { AxiosInstance } from "axios";
2
+ import ApiClientBase from "../core/ApiClientBase.js";
3
+ export declare function withEventConsistencyHandling<T extends ApiClientBase | AxiosInstance>(clientOrAxiosInstance: T): T;
@@ -0,0 +1,2 @@
1
+ export * from "./accessToken.js";
2
+ export * from "./consistencyHandling.js";
@@ -0,0 +1,12 @@
1
+ import { OpenAPIOperation, RequestFunction } from "../types/index.js";
2
+ import { GetApiResourceFn } from "./types.js";
3
+ export declare class ApiCallAsyncResourceFactory<TOp extends OpenAPIOperation> {
4
+ private static namespace;
5
+ private readonly operation;
6
+ private readonly requestFn;
7
+ constructor(operation: TOp, requestFn: RequestFunction<TOp>);
8
+ private getAsyncResourceId;
9
+ private getAsyncResourceTags;
10
+ private executeRequest;
11
+ getApiResource: GetApiResourceFn<TOp>;
12
+ }
@@ -0,0 +1 @@
1
+ export * from "./ApiCallAsyncResourceFactory.js";
@@ -0,0 +1,3 @@
1
+ import { OpenAPIOperation, RequestObject, InferredResponseData } from "../types/index.js";
2
+ import { AsyncResource } from "@mittwald/react-use-promise";
3
+ export type GetApiResourceFn<TOp extends OpenAPIOperation> = null extends RequestObject<TOp> ? (conf?: RequestObject<TOp>) => AsyncResource<InferredResponseData<TOp>> : (conf: RequestObject<TOp>) => AsyncResource<InferredResponseData<TOp>>;
@@ -0,0 +1,5 @@
1
+ import { Response } from "../types/Response.js";
2
+ export declare function expectStatus<T extends Response, S extends T["status"]>(response: T, status: S): asserts response is T & {
3
+ status: S;
4
+ };
5
+ export default expectStatus;
@@ -0,0 +1 @@
1
+ export * from "./expectStatus.js";
@@ -0,0 +1,6 @@
1
+ import { HasRequiredKeys, OmitIndexSignature, PartialOnUndefinedDeep } from "type-fest";
2
+ type PartialOnNoRequiredKeysDeep<T> = PartialOnUndefinedDeep<OmitIndexSignature<{
3
+ [TKey in keyof T]: HasRequiredKeys<PartialOnNoRequiredKeysDeep<T[TKey]>> extends true ? T[TKey] : T[TKey] | undefined;
4
+ }>>;
5
+ export type NullableOnNoRequiredKeysDeep<T> = HasRequiredKeys<PartialOnNoRequiredKeysDeep<T>> extends true ? PartialOnNoRequiredKeysDeep<T> : PartialOnNoRequiredKeysDeep<T> | null;
6
+ export {};
@@ -0,0 +1,20 @@
1
+ import { AnyResponse, Response } from "./Response.js";
2
+ import { AnyRequest, RequestType } from "./RequestType.js";
3
+ import { HttpMethod, HttpStatus } from "./http.js";
4
+ export interface OpenAPIOperation<TIgnoredRequest extends AnyRequest = RequestType, IgnoredResponse extends AnyResponse = Response> {
5
+ operationId: string;
6
+ path: string;
7
+ method: HttpMethod;
8
+ }
9
+ export type InferredRequestType<TOp> = TOp extends OpenAPIOperation<infer TReq> ? TReq : never;
10
+ export type InferredResponseType<TOp> = TOp extends OpenAPIOperation<RequestType, infer TRes> ? TRes : never;
11
+ export type InferredResponseData<TOp, TStatus extends HttpStatus = 200> = Extract<InferredResponseType<TOp>, {
12
+ status: TStatus;
13
+ }>["data"];
14
+ export type InferredRequestData<TOp> = TOp extends OpenAPIOperation ? InferredRequestType<TOp> extends {
15
+ data: infer TData;
16
+ } ? TData : never : never;
17
+ /** @deprecated Use InferredResponseData */
18
+ export type ResponseData<TOp> = InferredResponseData<TOp>;
19
+ /** @deprecated Use InferredRequestData */
20
+ export type RequestData<TOp> = InferredRequestData<TOp>;
@@ -0,0 +1,11 @@
1
+ import { InferredRequestType, InferredResponseType, OpenAPIOperation } from "./OpenAPIOperation.js";
2
+ import { NullableOnNoRequiredKeysDeep } from "./NullableOnNoRequiredKeysDeep.js";
3
+ type UnboxPathParameters<T> = T extends {
4
+ pathParameters: infer TPath;
5
+ } ? Omit<T, "pathParameters"> & TPath : T;
6
+ export type RequestObject<TOp extends OpenAPIOperation> = NullableOnNoRequiredKeysDeep<UnboxPathParameters<InferredRequestType<TOp>>>;
7
+ export type ResponsePromise<TOp extends OpenAPIOperation> = Promise<InferredResponseType<TOp>>;
8
+ type RequestFunctionWithOptionalRequest<TOp extends OpenAPIOperation> = (request?: RequestObject<TOp>) => ResponsePromise<TOp>;
9
+ type RequestFunctionWithRequiredRequest<TOp extends OpenAPIOperation> = (request: RequestObject<TOp>) => ResponsePromise<TOp>;
10
+ export type RequestFunction<TOp extends OpenAPIOperation> = null extends RequestObject<TOp> ? RequestFunctionWithOptionalRequest<TOp> : RequestFunctionWithRequiredRequest<TOp>;
11
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,21 @@
1
+ import { HttpHeaders, HttpPayload, PathParameters, QueryParameters } from "./http.js";
2
+ type EmptyObject = Record<string, never>;
3
+ type EmptyRequestComponent = EmptyObject | null;
4
+ type RequestWithOptionalHeaders = {
5
+ headers?: HttpHeaders;
6
+ };
7
+ type RequestWithData<TData> = TData extends EmptyRequestComponent ? RequestWithOptionalHeaders : {
8
+ data: TData;
9
+ };
10
+ type RequestWithPathParameters<TPathParameters> = TPathParameters extends EmptyRequestComponent ? RequestWithOptionalHeaders : {
11
+ pathParameters: TPathParameters;
12
+ };
13
+ type RequestWithHeaders<THeaders> = THeaders extends EmptyRequestComponent ? RequestWithOptionalHeaders : {
14
+ headers: THeaders & HttpHeaders;
15
+ };
16
+ type RequestWithQueryParameters<TQuery> = TQuery extends EmptyRequestComponent ? RequestWithOptionalHeaders : {
17
+ queryParameters: TQuery & HttpHeaders;
18
+ };
19
+ export type RequestType<TData extends HttpPayload = EmptyRequestComponent, TPathParameters extends PathParameters | EmptyRequestComponent = EmptyRequestComponent, TQueryParameters extends QueryParameters | EmptyRequestComponent = EmptyRequestComponent, THeader extends HttpHeaders | EmptyRequestComponent = EmptyRequestComponent> = TData | TPathParameters | THeader | TQueryParameters extends EmptyRequestComponent ? RequestWithOptionalHeaders : RequestWithData<TData> & RequestWithPathParameters<TPathParameters> & RequestWithQueryParameters<TQueryParameters> & RequestWithHeaders<THeader>;
20
+ export type AnyRequest = RequestType<any, any, any, any>;
21
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,7 @@
1
+ import { HttpMediaType, HttpPayload, HttpStatus } from "./http.js";
2
+ import { AxiosResponse } from "axios";
3
+ export type Response<TContent extends HttpPayload = HttpPayload, TStatus extends HttpStatus = HttpStatus, TMediaType extends HttpMediaType | null = HttpMediaType> = AxiosResponse<TContent> & {
4
+ status: TStatus;
5
+ mediaType: TMediaType;
6
+ };
7
+ export type AnyResponse = Response<any, any, any>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,5 @@
1
+ import { Response } from "./Response.js";
2
+ export declare function assertOneOfStatus<T extends Response, S extends T["status"]>(response: T, expectedStatus: S[]): asserts response is T & {
3
+ status: S;
4
+ };
5
+ export default assertOneOfStatus;
@@ -0,0 +1,5 @@
1
+ import { Response } from "./Response.js";
2
+ export declare function assertStatus<T extends Response, S extends T["status"]>(response: T, expectedStatus: S): asserts response is T & {
3
+ status: S;
4
+ };
5
+ export default assertStatus;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,13 @@
1
+ export type HttpPayload = unknown;
2
+ export type HttpStatus = number | "default";
3
+ export type HttpMediaType = string;
4
+ type SafeHttpMethod = "GET" | "HEAD" | "OPTIONS";
5
+ type UnsafeHttpMethod = "PUT" | "DELETE" | "POST" | "PATCH";
6
+ export type HttpMethod = SafeHttpMethod | UnsafeHttpMethod;
7
+ type HeaderValue = string | number | boolean;
8
+ export type HttpHeaders = Partial<{
9
+ [TKey: string]: HeaderValue | HeaderValue[];
10
+ }>;
11
+ export type PathParameters = Record<string, string | number>;
12
+ export type QueryParameters = Record<string, unknown>;
13
+ export {};
@@ -0,0 +1,8 @@
1
+ export * from "./RequestType.js";
2
+ export * from "./RequestFunction.js";
3
+ export * from "./Response.js";
4
+ export * from "./OpenAPIOperation.js";
5
+ export * from "./http.js";
6
+ export * from "./simplify.js";
7
+ export * from "./assertStatus.js";
8
+ export * from "./assertOneOfStatus.js";
@@ -0,0 +1,3 @@
1
+ export type Simplify<T> = {
2
+ [KeyType in keyof T]: T[KeyType];
3
+ } & {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mittwald/api-client-commons",
3
- "version": "0.0.0-development-04b7288-20240610",
3
+ "version": "0.0.0-development-7c0a25b-20240611",
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",
@@ -78,5 +78,5 @@
78
78
  "optional": true
79
79
  }
80
80
  },
81
- "gitHead": "97660b1fee614cc23b778573ba3e8ae75a961517"
81
+ "gitHead": "6e1f85a8f705547fa14be6279e7380b9fd7d4aa9"
82
82
  }