Skip to content

Commit 3f401bf

Browse files
authored
Node fetch (#136)
* Update templates Add Agents md * Update node version in readmin and package.json * Update templates * Update templates * Set submodule to main
1 parent 8dc3036 commit 3f401bf

6 files changed

Lines changed: 305 additions & 10 deletions

File tree

Agents.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Aspose.BarCode Cloud SDK and Codegen repository
2+
3+
This is repository with swagger generated SDK for Aspose.Barcode.Cloud service and code generating scripts. All sdk are submodules of this repo and located in `submodules` directory. Swagger specification of service API is located in `spec/aspose-barcode-cloud.json` file. Custom mustashe templates are located in `codegen/Templates` dir. Original templates for all languages are in <https://github.com/OpenAPITools/openapi-generator/tree/master/modules/openapi-generator/src/main/resources/> github. Sripts for generating SDK `codegen` directory. Some post processing are in Makefiles in all submodules repo.
4+
5+
## Common requirements for making changes in SDK code
6+
7+
0. Don't commit or push in repo by youreself. But you can pull and stage in git.
8+
1. To run any scripts, use WSL if you're on Windows.
9+
2. After making changes run tests with `make test` or similar comand in Makefile in SDK submodule repo.
10+
3. Add changes to mustache templates after changes in SDK code are made. If you changed some code and template for it not in codegen/Templates dir. Download this template and made changes in local copy in codegen/Templates dir.
11+
4. Enshure that generated code is the same as you new code. For it:
12+
4.1 Stage your changes in sdk submodule repo.
13+
4.2 Run `make <skd-name>` command in main repo. See Makefile for it.
14+
4.3 After generating script are end it work. Enshure there is no unstaged changes in sdk submodule.
15+
4.4 Fix templates if generated code are not the same as you new code.
16+
5. After templates fixed, you can end your task.

codegen/Templates/nodejs/api.mustache

Lines changed: 225 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { Configuration } from './Configuration';
2-
import { HttpClient, HttpOptions, HttpResponse, HttpResult } from './httpClient';
3-
import { Multipart, RequestFile, FormParamsType } from './multipart';
2+
import { Multipart, RequestFile, FormParamPairs } from './multipart';
43

54
export * from './models';
65

@@ -9,6 +8,224 @@ import { {{#models}} {{#model}}{{classname}},{{/model}} {{/models}} } from './mo
98
import { {{#apiInfo}}{{#apis}}{{#operations}}{{#operation}} {{operationIdCamelCase}}RequestWrapper, {{/operation}}{{/operations}}{{/apis}}{{/apiInfo}} } from './models';
109

1110

11+
type StringMap = Record<string, string>;
12+
13+
type ApiRequestOptions = {
14+
uri: string;
15+
body?: any;
16+
encoding?: BufferEncoding | null;
17+
form?: StringMap;
18+
headers?: StringMap;
19+
json?: boolean;
20+
method?: string;
21+
qs?: StringMap;
22+
};
23+
24+
type ApiResponse = {
25+
statusCode: number;
26+
statusMessage: string;
27+
headers: NodeJS.Dict<string | string[]>;
28+
body: any;
29+
};
30+
31+
type ApiResult<T = any> = {
32+
response: ApiResponse;
33+
body: T;
34+
};
35+
36+
type ApiRejectType = {
37+
response: ApiResponse | null;
38+
errorResponse: ApiErrorResponse | null;
39+
error: Error;
40+
};
41+
42+
export class ApiClient {
43+
private readonly _fetcher: typeof fetch;
44+
45+
constructor() {
46+
const resolvedFetch = (globalThis as { fetch?: typeof fetch }).fetch;
47+
if (!resolvedFetch) {
48+
throw new Error('Global fetch API is not available. Please use Node.js 18+.');
49+
}
50+
51+
this._fetcher = resolvedFetch;
52+
}
53+
54+
public requestAsync(options: ApiRequestOptions): Promise<ApiResult> {
55+
const url: URL = options.qs
56+
? new URL(`?${new URLSearchParams(options.qs).toString()}`, options.uri)
57+
: new URL(options.uri);
58+
59+
const requestBody = this.buildRequestBody(options);
60+
61+
const responseEncoding: BufferEncoding | null = options.encoding === null ? null : options.encoding || 'utf-8';
62+
63+
const requestOptions: RequestInit = {
64+
method: options.method || 'GET',
65+
headers: options.headers,
66+
};
67+
68+
if (requestBody) {
69+
requestOptions.body = requestBody;
70+
}
71+
72+
return this.doFetchRequest(url, requestOptions, responseEncoding);
73+
}
74+
75+
private buildRequestBody(options: ApiRequestOptions) {
76+
let requestBody = options.body;
77+
if (options.form) {
78+
// Override requestBody for form with form content
79+
requestBody = new URLSearchParams(options.form).toString();
80+
options.headers = Object.assign(
81+
{
82+
'Content-Type': 'application/x-www-form-urlencoded',
83+
},
84+
options.headers
85+
);
86+
}
87+
if (options.json) {
88+
// Override requestBody with JSON value
89+
requestBody = JSON.stringify(options.body);
90+
options.headers = Object.assign(
91+
{
92+
'Content-Type': 'application/json',
93+
},
94+
options.headers
95+
);
96+
}
97+
return requestBody;
98+
}
99+
100+
private async doFetchRequest(
101+
url: URL,
102+
requestOptions: RequestInit,
103+
responseEncoding: BufferEncoding | null
104+
): Promise<ApiResult> {
105+
let response: Response;
106+
try {
107+
response = await this._fetcher(url.toString(), requestOptions);
108+
} catch (error) {
109+
return Promise.reject({
110+
response: null,
111+
error: this.normalizeFetchError(error),
112+
errorResponse: null,
113+
});
114+
}
115+
116+
const respBody = await this.readResponseBody(response, responseEncoding);
117+
const responseHeaders = this.toHeaderDict(response.headers);
118+
119+
const httpResponse: ApiResponse = {
120+
statusCode: response.status,
121+
statusMessage: response.statusText,
122+
headers: responseHeaders,
123+
body: respBody,
124+
};
125+
126+
if (response.ok) {
127+
return {
128+
response: httpResponse,
129+
body: respBody,
130+
};
131+
}
132+
133+
const rejectObject: ApiRejectType = {
134+
response: httpResponse,
135+
error: new Error(`Error on '${url}': ${response.status} ${response.statusText}`),
136+
errorResponse: null,
137+
};
138+
let errorResponse = null;
139+
try {
140+
errorResponse = JSON.parse(respBody.toString()) as ApiErrorResponse;
141+
} catch (parseError) {}
142+
143+
if (errorResponse) {
144+
rejectObject.errorResponse = errorResponse;
145+
} else {
146+
rejectObject.error.message += `. ${respBody}`;
147+
}
148+
149+
return Promise.reject(rejectObject);
150+
}
151+
152+
private async readResponseBody(
153+
response: Response,
154+
responseEncoding: BufferEncoding | null
155+
): Promise<string | Buffer> {
156+
const arrayBuffer = await response.arrayBuffer();
157+
const buffer = Buffer.from(arrayBuffer);
158+
159+
if (responseEncoding === null) {
160+
return buffer;
161+
}
162+
163+
return buffer.toString(responseEncoding);
164+
}
165+
166+
private toHeaderDict(headers: Headers): NodeJS.Dict<string | string[]> {
167+
const normalizedHeaders: NodeJS.Dict<string | string[]> = {};
168+
169+
headers.forEach((value, key) => {
170+
const existing = normalizedHeaders[key];
171+
if (existing === undefined) {
172+
normalizedHeaders[key] = value;
173+
return;
174+
}
175+
176+
if (Array.isArray(existing)) {
177+
existing.push(value);
178+
normalizedHeaders[key] = existing;
179+
return;
180+
}
181+
182+
normalizedHeaders[key] = [existing, value];
183+
});
184+
185+
return normalizedHeaders;
186+
}
187+
188+
private normalizeFetchError(error: unknown): Error {
189+
if (error instanceof Error) {
190+
const mutableError = error as Error & { code?: string; cause?: unknown; name: string };
191+
let normalizedCode = mutableError.code;
192+
193+
if (!normalizedCode) {
194+
const cause = mutableError.cause;
195+
if (cause && typeof cause === 'object' && 'code' in (cause as { code?: string })) {
196+
const code = (cause as { code?: string }).code;
197+
if (code) {
198+
normalizedCode = String(code);
199+
}
200+
}
201+
}
202+
203+
if (!normalizedCode) {
204+
normalizedCode = mutableError.name || 'FETCH_ERROR';
205+
}
206+
207+
try {
208+
if (!mutableError.code) {
209+
mutableError.code = normalizedCode;
210+
}
211+
} catch (assignError) {}
212+
213+
if (mutableError.code) {
214+
return mutableError;
215+
}
216+
217+
const wrapped = new Error(mutableError.message);
218+
wrapped.name = mutableError.name;
219+
(wrapped as { code?: string }).code = normalizedCode;
220+
return wrapped;
221+
}
222+
223+
const wrapped = new Error(String(error));
224+
(wrapped as { code?: string }).code = 'FETCH_ERROR';
225+
return wrapped;
226+
}
227+
}
228+
12229
let primitives = ['string', 'boolean', 'double', 'integer', 'long', 'float', 'number', 'any'];
13230

14231
class ObjectSerializer {
@@ -184,11 +401,11 @@ export class {{classname}} {
184401
'x-aspose-client-version': '{{npmVersion}}'
185402
};
186403
protected _configuration: Configuration;
187-
private _client: HttpClient;
404+
private _client: ApiClient;
188405

189406
constructor(configuration: Configuration) {
190407
this._configuration = configuration;
191-
this._client = new HttpClient();
408+
this._client = new ApiClient();
192409
}
193410

194411
{{#operation}}
@@ -200,7 +417,7 @@ export class {{classname}} {
200417
{{/summary}}
201418
* @param request {{operationIdCamelCase}}RequestWrapper
202419
*/
203-
public async {{nickname}}(request: {{operationIdCamelCase}}RequestWrapper): Promise<{ response: HttpResponse; {{#returnType}}body: {{{returnType}}}; {{/returnType}}{{^returnType}}body?: any; {{/returnType}} }> {
420+
public async {{nickname}}(request: {{operationIdCamelCase}}RequestWrapper): Promise<{ response: ApiResponse; {{#returnType}}body: {{{returnType}}}; {{/returnType}}{{^returnType}}body?: any; {{/returnType}} }> {
204421
const requestPath =
205422
this._configuration.getApiBaseUrl() +
206423
'{{{path}}}'{{#pathParams}}.replace(
@@ -211,7 +428,7 @@ export class {{classname}} {
211428
let queryParameters: any = {};
212429
let headerParams: any = (Object as any).assign({}, this.defaultHeaders);
213430
{{#hasFormParams}}
214-
const formParams: FormParamsType = [];
431+
const formParams: FormParamPairs = [];
215432
{{/hasFormParams}}
216433

217434
{{#allParams}}
@@ -247,7 +464,7 @@ export class {{classname}} {
247464
}
248465
{{/isFile}}
249466
{{/formParams}}
250-
const requestOptions: HttpOptions = {
467+
const requestOptions: ApiRequestOptions = {
251468
method: '{{httpMethod}}',
252469
qs: queryParameters,
253470
headers: headerParams,
@@ -272,7 +489,7 @@ export class {{classname}} {
272489

273490
await this._configuration.authentication.applyToRequestAsync(requestOptions);
274491

275-
const result: HttpResult = await this._client.requestAsync(requestOptions);
492+
const result: ApiResult = await this._client.requestAsync(requestOptions);
276493

277494
{{#returnType}}
278495
return {

codegen/Templates/nodejs/docs/tsconfig.mustache

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ This repository contains Aspose.BarCode Cloud SDK for Node.js source code.
2727

2828
To use these SDKs, you will need Client Id and Client Secret which can be looked up at [Aspose Cloud Dashboard](https://dashboard.aspose.cloud/applications) (free registration in Aspose Cloud is required for this).
2929

30+
## Requirements
31+
32+
- Node.js 18 or later (native `fetch` required).
33+
3034
## How to use the SDK
3135

3236
The complete source code is available in this repository folder. You can either directly use it in your project via source code or get [nmpjs distribution](https://www.npmjs.com/package/aspose-barcode-cloud-node) (recommended).
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import crypto from 'crypto';
2+
3+
type StringMap = Record<string, string>;
4+
5+
export interface FormParamPairs extends Array<Array<string>> {}
6+
7+
interface IRequestFile {
8+
name: string;
9+
filename: string;
10+
data: Buffer;
11+
contentType?: string;
12+
}
13+
14+
export class RequestFile implements IRequestFile {
15+
constructor(
16+
readonly name: string,
17+
readonly filename: string,
18+
readonly data: Buffer,
19+
readonly contentType?: string
20+
) {}
21+
}
22+
23+
export class Multipart {
24+
readonly boundary: string;
25+
readonly body: Buffer;
26+
readonly headers: StringMap;
27+
28+
constructor(textFields: FormParamPairs, files?: IRequestFile[]) {
29+
const random = crypto.randomUUID();
30+
this.boundary = '------------------------' + random.replace(/-/g, '');
31+
32+
const bodyLines = [];
33+
for (const tuple of textFields) {
34+
bodyLines.push(`--${this.boundary}`);
35+
bodyLines.push(`Content-Disposition: form-data; name="${tuple[0]}"`);
36+
bodyLines.push('');
37+
bodyLines.push(tuple[1]);
38+
}
39+
for (const file of files || []) {
40+
bodyLines.push(`--${this.boundary}`);
41+
bodyLines.push(
42+
`Content-Disposition: form-data; name="${file.name}"; filename="${file.filename || 'filename'}"`
43+
);
44+
bodyLines.push(`Content-Type: ${file.contentType || 'application/octet-stream'}`);
45+
bodyLines.push('');
46+
bodyLines.push(file.data.toString('binary'));
47+
}
48+
bodyLines.push(`--${this.boundary}--`);
49+
50+
this.body = Buffer.from(bodyLines.join('\r\n'), 'binary');
51+
52+
this.headers = {
53+
'Content-Type': `multipart/form-data; boundary=${this.boundary}`,
54+
'Content-Length': this.body.length.toString(),
55+
};
56+
}
57+
}

codegen/Templates/nodejs/package.mustache

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,10 +129,11 @@
129129
"dist"
130130
],
131131
"engines": {
132-
"node": ">=16"
132+
"node": ">=18"
133133
},
134134
"scripts": {
135135
"test": "npx jest",
136+
"typecheck": "npx tsc --noEmit",
136137
"cover": "npx jest --coverage",
137138
"lint": "npx eslint src test snippets",
138139
"format": "npx eslint src test snippets eslint.config.mjs --fix",

submodules/node

Submodule node updated from d13cbe9 to 48afd98

0 commit comments

Comments
 (0)