diff --git a/Agents.md b/Agents.md new file mode 100644 index 0000000..5b3175f --- /dev/null +++ b/Agents.md @@ -0,0 +1,16 @@ +# Aspose.BarCode Cloud SDK and Codegen repository + +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 github. Sripts for generating SDK `codegen` directory. Some post processing are in Makefiles in all submodules repo. + +## Common requirements for making changes in SDK code + +0. Don't commit or push in repo by youreself. But you can pull and stage in git. +1. To run any scripts, use WSL if you're on Windows. +2. After making changes run tests with `make test` or similar comand in Makefile in SDK submodule repo. +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. +4. Enshure that generated code is the same as you new code. For it: + 4.1 Stage your changes in sdk submodule repo. + 4.2 Run `make ` command in main repo. See Makefile for it. + 4.3 After generating script are end it work. Enshure there is no unstaged changes in sdk submodule. + 4.4 Fix templates if generated code are not the same as you new code. +5. After templates fixed, you can end your task. diff --git a/codegen/Templates/nodejs/api.mustache b/codegen/Templates/nodejs/api.mustache index 3561e1e..8be1046 100644 --- a/codegen/Templates/nodejs/api.mustache +++ b/codegen/Templates/nodejs/api.mustache @@ -1,6 +1,5 @@ import { Configuration } from './Configuration'; -import { HttpClient, HttpOptions, HttpResponse, HttpResult } from './httpClient'; -import { Multipart, RequestFile, FormParamsType } from './multipart'; +import { Multipart, RequestFile, FormParamPairs } from './multipart'; export * from './models'; @@ -9,6 +8,224 @@ import { {{#models}} {{#model}}{{classname}},{{/model}} {{/models}} } from './mo import { {{#apiInfo}}{{#apis}}{{#operations}}{{#operation}} {{operationIdCamelCase}}RequestWrapper, {{/operation}}{{/operations}}{{/apis}}{{/apiInfo}} } from './models'; +type StringMap = Record; + +type ApiRequestOptions = { + uri: string; + body?: any; + encoding?: BufferEncoding | null; + form?: StringMap; + headers?: StringMap; + json?: boolean; + method?: string; + qs?: StringMap; +}; + +type ApiResponse = { + statusCode: number; + statusMessage: string; + headers: NodeJS.Dict; + body: any; +}; + +type ApiResult = { + response: ApiResponse; + body: T; +}; + +type ApiRejectType = { + response: ApiResponse | null; + errorResponse: ApiErrorResponse | null; + error: Error; +}; + +export class ApiClient { + private readonly _fetcher: typeof fetch; + + constructor() { + const resolvedFetch = (globalThis as { fetch?: typeof fetch }).fetch; + if (!resolvedFetch) { + throw new Error('Global fetch API is not available. Please use Node.js 18+.'); + } + + this._fetcher = resolvedFetch; + } + + public requestAsync(options: ApiRequestOptions): Promise { + const url: URL = options.qs + ? new URL(`?${new URLSearchParams(options.qs).toString()}`, options.uri) + : new URL(options.uri); + + const requestBody = this.buildRequestBody(options); + + const responseEncoding: BufferEncoding | null = options.encoding === null ? null : options.encoding || 'utf-8'; + + const requestOptions: RequestInit = { + method: options.method || 'GET', + headers: options.headers, + }; + + if (requestBody) { + requestOptions.body = requestBody; + } + + return this.doFetchRequest(url, requestOptions, responseEncoding); + } + + private buildRequestBody(options: ApiRequestOptions) { + let requestBody = options.body; + if (options.form) { + // Override requestBody for form with form content + requestBody = new URLSearchParams(options.form).toString(); + options.headers = Object.assign( + { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + options.headers + ); + } + if (options.json) { + // Override requestBody with JSON value + requestBody = JSON.stringify(options.body); + options.headers = Object.assign( + { + 'Content-Type': 'application/json', + }, + options.headers + ); + } + return requestBody; + } + + private async doFetchRequest( + url: URL, + requestOptions: RequestInit, + responseEncoding: BufferEncoding | null + ): Promise { + let response: Response; + try { + response = await this._fetcher(url.toString(), requestOptions); + } catch (error) { + return Promise.reject({ + response: null, + error: this.normalizeFetchError(error), + errorResponse: null, + }); + } + + const respBody = await this.readResponseBody(response, responseEncoding); + const responseHeaders = this.toHeaderDict(response.headers); + + const httpResponse: ApiResponse = { + statusCode: response.status, + statusMessage: response.statusText, + headers: responseHeaders, + body: respBody, + }; + + if (response.ok) { + return { + response: httpResponse, + body: respBody, + }; + } + + const rejectObject: ApiRejectType = { + response: httpResponse, + error: new Error(`Error on '${url}': ${response.status} ${response.statusText}`), + errorResponse: null, + }; + let errorResponse = null; + try { + errorResponse = JSON.parse(respBody.toString()) as ApiErrorResponse; + } catch (parseError) {} + + if (errorResponse) { + rejectObject.errorResponse = errorResponse; + } else { + rejectObject.error.message += `. ${respBody}`; + } + + return Promise.reject(rejectObject); + } + + private async readResponseBody( + response: Response, + responseEncoding: BufferEncoding | null + ): Promise { + const arrayBuffer = await response.arrayBuffer(); + const buffer = Buffer.from(arrayBuffer); + + if (responseEncoding === null) { + return buffer; + } + + return buffer.toString(responseEncoding); + } + + private toHeaderDict(headers: Headers): NodeJS.Dict { + const normalizedHeaders: NodeJS.Dict = {}; + + headers.forEach((value, key) => { + const existing = normalizedHeaders[key]; + if (existing === undefined) { + normalizedHeaders[key] = value; + return; + } + + if (Array.isArray(existing)) { + existing.push(value); + normalizedHeaders[key] = existing; + return; + } + + normalizedHeaders[key] = [existing, value]; + }); + + return normalizedHeaders; + } + + private normalizeFetchError(error: unknown): Error { + if (error instanceof Error) { + const mutableError = error as Error & { code?: string; cause?: unknown; name: string }; + let normalizedCode = mutableError.code; + + if (!normalizedCode) { + const cause = mutableError.cause; + if (cause && typeof cause === 'object' && 'code' in (cause as { code?: string })) { + const code = (cause as { code?: string }).code; + if (code) { + normalizedCode = String(code); + } + } + } + + if (!normalizedCode) { + normalizedCode = mutableError.name || 'FETCH_ERROR'; + } + + try { + if (!mutableError.code) { + mutableError.code = normalizedCode; + } + } catch (assignError) {} + + if (mutableError.code) { + return mutableError; + } + + const wrapped = new Error(mutableError.message); + wrapped.name = mutableError.name; + (wrapped as { code?: string }).code = normalizedCode; + return wrapped; + } + + const wrapped = new Error(String(error)); + (wrapped as { code?: string }).code = 'FETCH_ERROR'; + return wrapped; + } +} + let primitives = ['string', 'boolean', 'double', 'integer', 'long', 'float', 'number', 'any']; class ObjectSerializer { @@ -184,11 +401,11 @@ export class {{classname}} { 'x-aspose-client-version': '{{npmVersion}}' }; protected _configuration: Configuration; - private _client: HttpClient; + private _client: ApiClient; constructor(configuration: Configuration) { this._configuration = configuration; - this._client = new HttpClient(); + this._client = new ApiClient(); } {{#operation}} @@ -200,7 +417,7 @@ export class {{classname}} { {{/summary}} * @param request {{operationIdCamelCase}}RequestWrapper */ - public async {{nickname}}(request: {{operationIdCamelCase}}RequestWrapper): Promise<{ response: HttpResponse; {{#returnType}}body: {{{returnType}}}; {{/returnType}}{{^returnType}}body?: any; {{/returnType}} }> { + public async {{nickname}}(request: {{operationIdCamelCase}}RequestWrapper): Promise<{ response: ApiResponse; {{#returnType}}body: {{{returnType}}}; {{/returnType}}{{^returnType}}body?: any; {{/returnType}} }> { const requestPath = this._configuration.getApiBaseUrl() + '{{{path}}}'{{#pathParams}}.replace( @@ -211,7 +428,7 @@ export class {{classname}} { let queryParameters: any = {}; let headerParams: any = (Object as any).assign({}, this.defaultHeaders); {{#hasFormParams}} - const formParams: FormParamsType = []; + const formParams: FormParamPairs = []; {{/hasFormParams}} {{#allParams}} @@ -247,7 +464,7 @@ export class {{classname}} { } {{/isFile}} {{/formParams}} - const requestOptions: HttpOptions = { + const requestOptions: ApiRequestOptions = { method: '{{httpMethod}}', qs: queryParameters, headers: headerParams, @@ -272,7 +489,7 @@ export class {{classname}} { await this._configuration.authentication.applyToRequestAsync(requestOptions); - const result: HttpResult = await this._client.requestAsync(requestOptions); + const result: ApiResult = await this._client.requestAsync(requestOptions); {{#returnType}} return { diff --git a/codegen/Templates/nodejs/docs/tsconfig.mustache b/codegen/Templates/nodejs/docs/tsconfig.mustache index 403750e..3963bc2 100644 --- a/codegen/Templates/nodejs/docs/tsconfig.mustache +++ b/codegen/Templates/nodejs/docs/tsconfig.mustache @@ -27,6 +27,10 @@ This repository contains Aspose.BarCode Cloud SDK for Node.js source code. 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). +## Requirements + +- Node.js 18 or later (native `fetch` required). + ## How to use the SDK 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). diff --git a/codegen/Templates/nodejs/multipart.mustache b/codegen/Templates/nodejs/multipart.mustache new file mode 100644 index 0000000..5a4aaa3 --- /dev/null +++ b/codegen/Templates/nodejs/multipart.mustache @@ -0,0 +1,57 @@ +import crypto from 'crypto'; + +type StringMap = Record; + +export interface FormParamPairs extends Array> {} + +interface IRequestFile { + name: string; + filename: string; + data: Buffer; + contentType?: string; +} + +export class RequestFile implements IRequestFile { + constructor( + readonly name: string, + readonly filename: string, + readonly data: Buffer, + readonly contentType?: string + ) {} +} + +export class Multipart { + readonly boundary: string; + readonly body: Buffer; + readonly headers: StringMap; + + constructor(textFields: FormParamPairs, files?: IRequestFile[]) { + const random = crypto.randomUUID(); + this.boundary = '------------------------' + random.replace(/-/g, ''); + + const bodyLines = []; + for (const tuple of textFields) { + bodyLines.push(`--${this.boundary}`); + bodyLines.push(`Content-Disposition: form-data; name="${tuple[0]}"`); + bodyLines.push(''); + bodyLines.push(tuple[1]); + } + for (const file of files || []) { + bodyLines.push(`--${this.boundary}`); + bodyLines.push( + `Content-Disposition: form-data; name="${file.name}"; filename="${file.filename || 'filename'}"` + ); + bodyLines.push(`Content-Type: ${file.contentType || 'application/octet-stream'}`); + bodyLines.push(''); + bodyLines.push(file.data.toString('binary')); + } + bodyLines.push(`--${this.boundary}--`); + + this.body = Buffer.from(bodyLines.join('\r\n'), 'binary'); + + this.headers = { + 'Content-Type': `multipart/form-data; boundary=${this.boundary}`, + 'Content-Length': this.body.length.toString(), + }; + } +} diff --git a/codegen/Templates/nodejs/package.mustache b/codegen/Templates/nodejs/package.mustache index c8111a0..2211d12 100644 --- a/codegen/Templates/nodejs/package.mustache +++ b/codegen/Templates/nodejs/package.mustache @@ -129,10 +129,11 @@ "dist" ], "engines": { - "node": ">=16" + "node": ">=18" }, "scripts": { "test": "npx jest", + "typecheck": "npx tsc --noEmit", "cover": "npx jest --coverage", "lint": "npx eslint src test snippets", "format": "npx eslint src test snippets eslint.config.mjs --fix", diff --git a/submodules/node b/submodules/node index d13cbe9..48afd98 160000 --- a/submodules/node +++ b/submodules/node @@ -1 +1 @@ -Subproject commit d13cbe939349f8cf1853dbd8f762423f66168504 +Subproject commit 48afd98ac89bc4100fc468b5c9b38256bfcd2c45