@rexeus/typeweaver-clients 0.0.1
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/README.md +411 -0
- package/dist/index.d.ts +1618 -0
- package/dist/index.js +985 -0
- package/dist/lib/ApiClient.ts +189 -0
- package/dist/lib/RequestCommand.ts +61 -0
- package/dist/lib/index.ts +2 -0
- package/dist/templates/Client.ejs +39 -0
- package/package.json +48 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
IHttpQuery,
|
|
3
|
+
IHttpParam,
|
|
4
|
+
IHttpHeader,
|
|
5
|
+
IHttpResponse,
|
|
6
|
+
} from "@rexeus/typeweaver-core";
|
|
7
|
+
import { RequestCommand } from "./RequestCommand";
|
|
8
|
+
import axios, {
|
|
9
|
+
AxiosError,
|
|
10
|
+
type AxiosInstance,
|
|
11
|
+
type AxiosResponse,
|
|
12
|
+
} from "axios";
|
|
13
|
+
import Case from "case";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Configuration options for ApiClient initialization.
|
|
17
|
+
*/
|
|
18
|
+
export type ApiClientProps = {
|
|
19
|
+
/** Custom Axios instance with pre-configured defaults */
|
|
20
|
+
axiosInstance?: AxiosInstance;
|
|
21
|
+
/** Base URL for API requests. If not provided, must be set in axiosInstance */
|
|
22
|
+
baseUrl?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Abstract base class for type-safe API clients.
|
|
27
|
+
*
|
|
28
|
+
* This class provides HTTP request execution with:
|
|
29
|
+
* - Automatic path parameter substitution (`:param` style)
|
|
30
|
+
* - Query string building with array support
|
|
31
|
+
* - Header normalization (converts to Header-Case)
|
|
32
|
+
* - Network error handling with specific error messages
|
|
33
|
+
* - Integration with RequestCommand pattern for type safety
|
|
34
|
+
*/
|
|
35
|
+
export abstract class ApiClient {
|
|
36
|
+
/** The Axios instance used for HTTP requests */
|
|
37
|
+
public readonly axiosInstance: AxiosInstance;
|
|
38
|
+
/** The base URL for all API requests */
|
|
39
|
+
public readonly baseUrl: string;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Creates a new ApiClient instance.
|
|
43
|
+
*
|
|
44
|
+
* @param props - Configuration options
|
|
45
|
+
* @throws {Error} If no base URL is provided in props or axios instance
|
|
46
|
+
*/
|
|
47
|
+
protected constructor(props: ApiClientProps) {
|
|
48
|
+
this.axiosInstance = props.axiosInstance ?? axios.create();
|
|
49
|
+
|
|
50
|
+
this.baseUrl = props.baseUrl ?? this.axiosInstance.defaults.baseURL ?? "";
|
|
51
|
+
if (!this.baseUrl) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
"Base URL must be provided either in axios instance or in constructor"
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Executes an HTTP request using the provided command.
|
|
60
|
+
*
|
|
61
|
+
* This method:
|
|
62
|
+
* 1. Substitutes path parameters (e.g., `:id` with actual values)
|
|
63
|
+
* 2. Builds the complete URL with query parameters
|
|
64
|
+
* 3. Sends the HTTP request via Axios
|
|
65
|
+
* 4. Normalizes the response format
|
|
66
|
+
* 5. Handles network errors with specific error messages
|
|
67
|
+
*
|
|
68
|
+
* @param request - The request command containing all HTTP parameters
|
|
69
|
+
* @returns Promise resolving to the HTTP response
|
|
70
|
+
* @throws {Error} Network errors with specific messages (connection refused, timeout, etc.)
|
|
71
|
+
* @protected
|
|
72
|
+
*/
|
|
73
|
+
protected async execute(request: RequestCommand): Promise<IHttpResponse> {
|
|
74
|
+
const { method, path, header, query, param, body } = request;
|
|
75
|
+
|
|
76
|
+
const headers = this.createHeader(header);
|
|
77
|
+
const pathWithParam = this.createPath(path, param);
|
|
78
|
+
const url = this.createUrl(pathWithParam, query);
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const response = await this.axiosInstance.request({
|
|
82
|
+
method,
|
|
83
|
+
url,
|
|
84
|
+
data: body,
|
|
85
|
+
headers,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
return this.createResponse(response);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if (error instanceof AxiosError) {
|
|
91
|
+
if (error.response) {
|
|
92
|
+
return this.createResponse(error.response);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// TODO: improve network error handling
|
|
96
|
+
if (error.code === "ECONNREFUSED") {
|
|
97
|
+
throw new Error("Network error: Connection refused");
|
|
98
|
+
}
|
|
99
|
+
if (error.code === "ECONNRESET") {
|
|
100
|
+
throw new Error("Network error: Connection reset by peer");
|
|
101
|
+
}
|
|
102
|
+
if (error.code === "ENOTFOUND") {
|
|
103
|
+
throw new Error("Network error: DNS lookup failed");
|
|
104
|
+
}
|
|
105
|
+
if (error.code === "ETIMEDOUT") {
|
|
106
|
+
throw new Error("Network error: Connection timed out");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
throw new Error("Network error: Unknown error");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
throw new Error(`Network error: ${error instanceof Error ? error.message : String(error)}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private createResponse(response: AxiosResponse): IHttpResponse {
|
|
117
|
+
const header: IHttpHeader = Object.entries(response.headers).reduce(
|
|
118
|
+
(acc, [key, value]) => {
|
|
119
|
+
if (!value) {
|
|
120
|
+
return acc;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const lowerCaseKey = key.toLowerCase();
|
|
124
|
+
const headerCaseKey = Case.header(lowerCaseKey);
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
...acc,
|
|
128
|
+
[headerCaseKey]: value,
|
|
129
|
+
[lowerCaseKey]: value,
|
|
130
|
+
};
|
|
131
|
+
},
|
|
132
|
+
{}
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
body: response.data,
|
|
137
|
+
header,
|
|
138
|
+
statusCode: response.status,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private createPath(path: string, param?: IHttpParam): string {
|
|
143
|
+
if (!param) {
|
|
144
|
+
return path;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return Object.entries(param).reduce((acc, [key, value]) => {
|
|
148
|
+
const result = acc.replace(`:${key}`, value);
|
|
149
|
+
|
|
150
|
+
if (result === acc) {
|
|
151
|
+
throw new Error(
|
|
152
|
+
`Path parameter '${key}' is not found in path '${path}'`
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return result;
|
|
157
|
+
}, path);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private addQuery(url: URL, query?: IHttpQuery): void {
|
|
161
|
+
if (!query) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const searchParams = url.searchParams;
|
|
166
|
+
for (const [key, value] of Object.entries(query)) {
|
|
167
|
+
if (!Array.isArray(value)) {
|
|
168
|
+
searchParams.append(key, value);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
for (const item of value) {
|
|
173
|
+
searchParams.append(key, item);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private createUrl(path: string, query?: IHttpQuery): string {
|
|
179
|
+
const url = new URL(path, this.baseUrl);
|
|
180
|
+
|
|
181
|
+
this.addQuery(url, query);
|
|
182
|
+
|
|
183
|
+
return url.toString();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private createHeader(header: any): any {
|
|
187
|
+
return header;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type HttpMethod,
|
|
3
|
+
type IHttpRequest,
|
|
4
|
+
type IHttpHeader,
|
|
5
|
+
type IHttpParam,
|
|
6
|
+
type IHttpQuery,
|
|
7
|
+
type IHttpBody,
|
|
8
|
+
type IHttpResponse,
|
|
9
|
+
HttpResponse,
|
|
10
|
+
} from "@rexeus/typeweaver-core";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Abstract base class for type-safe API request commands.
|
|
14
|
+
*
|
|
15
|
+
* This class represents a command pattern for HTTP requests, providing:
|
|
16
|
+
* - Type-safe request parameters (headers, path params, query, body)
|
|
17
|
+
* - Response processing abstraction
|
|
18
|
+
* - Integration with ApiClient for execution
|
|
19
|
+
*
|
|
20
|
+
* Implementations should:
|
|
21
|
+
* 1. Set all readonly properties in the constructor
|
|
22
|
+
* 2. Implement processResponse to handle response transformation
|
|
23
|
+
*
|
|
24
|
+
* @template Header - The HTTP header type
|
|
25
|
+
* @template Param - The path parameter type
|
|
26
|
+
* @template Query - The query string parameter type
|
|
27
|
+
* @template Body - The request body type
|
|
28
|
+
*/
|
|
29
|
+
export abstract class RequestCommand<
|
|
30
|
+
Header extends IHttpHeader = IHttpHeader | undefined,
|
|
31
|
+
Param extends IHttpParam = IHttpParam | undefined,
|
|
32
|
+
Query extends IHttpQuery = IHttpQuery | undefined,
|
|
33
|
+
Body extends IHttpBody = IHttpBody | undefined,
|
|
34
|
+
> implements IHttpRequest
|
|
35
|
+
{
|
|
36
|
+
/** The HTTP method for this request */
|
|
37
|
+
public readonly method!: HttpMethod;
|
|
38
|
+
/** The URL path pattern with parameter placeholders (e.g., '/users/:id') */
|
|
39
|
+
public readonly path!: string;
|
|
40
|
+
/** HTTP headers to send with the request */
|
|
41
|
+
public readonly header!: Header;
|
|
42
|
+
/** Path parameters to substitute in the URL */
|
|
43
|
+
public readonly param!: Param;
|
|
44
|
+
/** Query string parameters */
|
|
45
|
+
public readonly query!: Query;
|
|
46
|
+
/** Request body data */
|
|
47
|
+
public readonly body!: Body;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Processes the raw HTTP response into a typed response object.
|
|
51
|
+
*
|
|
52
|
+
* This method should handle:
|
|
53
|
+
* - Response validation
|
|
54
|
+
* - Data transformation
|
|
55
|
+
* - Error response handling
|
|
56
|
+
*
|
|
57
|
+
* @param response - The raw HTTP response from the server
|
|
58
|
+
* @returns The processed, type-safe response object
|
|
59
|
+
*/
|
|
60
|
+
public abstract processResponse(response: IHttpResponse): HttpResponse;
|
|
61
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { ApiClient, type ApiClientProps } from "../lib/clients";
|
|
2
|
+
<% for (const operation of operations) { %>
|
|
3
|
+
import { <%= operation.operationId %>RequestCommand, type Successful<%= operation.operationId %>Response } from "<%= operation.requestFile %>";
|
|
4
|
+
<% } %>
|
|
5
|
+
|
|
6
|
+
export type <%= pascalCaseEntityName %>RequestCommands =
|
|
7
|
+
<% for (const operation of operations) { %>
|
|
8
|
+
| <%= operation.operationId %>RequestCommand
|
|
9
|
+
<% } %>;
|
|
10
|
+
|
|
11
|
+
export type Successful<%= pascalCaseEntityName %>Responses =
|
|
12
|
+
<% for (const operation of operations) { %>
|
|
13
|
+
| Successful<%= operation.operationId %>Response
|
|
14
|
+
<% } %>;
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
export class <%= pascalCaseEntityName %>Client extends ApiClient {
|
|
18
|
+
public constructor(props: ApiClientProps) {
|
|
19
|
+
super(props);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
<% for (const operation of operations) { %>
|
|
23
|
+
public async send(command: <%= operation.operationId %>RequestCommand): Promise<Successful<%= operation.operationId %>Response>;
|
|
24
|
+
<% } %>
|
|
25
|
+
public async send(command: <%= pascalCaseEntityName %>RequestCommands): Promise<Successful<%= pascalCaseEntityName %>Responses> {
|
|
26
|
+
const response = await this.execute(command);
|
|
27
|
+
|
|
28
|
+
switch (true) {
|
|
29
|
+
<% for (const operation of operations) { %>
|
|
30
|
+
case command instanceof <%= operation.operationId %>RequestCommand: {
|
|
31
|
+
return command.processResponse(response);
|
|
32
|
+
}
|
|
33
|
+
<% } %>
|
|
34
|
+
default: {
|
|
35
|
+
throw new Error("Command is not supported");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rexeus/typeweaver-clients",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "HTTP client generator for TypeWeaver API framework",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"package.json",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"keywords": [
|
|
14
|
+
"api",
|
|
15
|
+
"spec",
|
|
16
|
+
"definition",
|
|
17
|
+
"typescript",
|
|
18
|
+
"zod",
|
|
19
|
+
"generator",
|
|
20
|
+
"typeweaver"
|
|
21
|
+
],
|
|
22
|
+
"author": "Dennis Wentzien <dw@rexeus.com>",
|
|
23
|
+
"license": "ISC",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/rexeus/typeweaver.git",
|
|
27
|
+
"directory": "packages/clients"
|
|
28
|
+
},
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/rexeus/typeweaver/issues"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://github.com/rexeus/typeweaver#readme",
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"axios": "^1.7.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"case": "^1.6.3",
|
|
38
|
+
"axios": "^1.9.0",
|
|
39
|
+
"@rexeus/typeweaver-core": "0.0.1",
|
|
40
|
+
"@rexeus/typeweaver-gen": "0.0.1"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"typecheck": "tsc --noEmit",
|
|
44
|
+
"format": "prettier --write .",
|
|
45
|
+
"build": "pkgroll --clean-dist && cp -r ./src/templates ./dist/templates && cp -r ./src/lib ./dist/lib",
|
|
46
|
+
"preversion": "npm run build"
|
|
47
|
+
}
|
|
48
|
+
}
|