diff --git a/.gitignore b/.gitignore index 2b7422568..7388c0c45 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ test/e2e/generated samples/generated samples/swagger-codegen-cli-v2.jar samples/swagger-codegen-cli-v3.jar +.npmrc \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 867f75c96..5e8361852 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "openapi-typescript-codegen", - "version": "0.27.0", + "version": "0.27.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "openapi-typescript-codegen", - "version": "0.27.0", + "version": "0.27.3", "license": "MIT", "dependencies": { "@apidevtools/json-schema-ref-parser": "^10.1.0", diff --git a/package.json b/package.json index 92a2e6263..b0a8c7fb1 100644 --- a/package.json +++ b/package.json @@ -1,17 +1,12 @@ { "name": "openapi-typescript-codegen", - "version": "0.27.0", + "version": "0.27.3", "description": "Library that generates Typescript clients based on the OpenAPI specification.", - "author": "Ferdi Koomen", - "homepage": "https://github.com/ferdikoomen/openapi-typescript-codegen", - "repository": { - "type": "git", - "url": "git+https://github.com/ferdikoomen/openapi-typescript-codegen.git" - }, - "bugs": { - "url": "https://github.com/ferdikoomen/openapi-typescript-codegen/issues" - }, + "homepage": "https://github.com/kimiyori/openapi-typescript-codegen", "license": "MIT", + "publishConfig": { + "registry": "https://nexus.capitalgroup.ru/repository/npm-release/" + }, "keywords": [ "openapi", "swagger", @@ -25,12 +20,6 @@ "angular", "node" ], - "maintainers": [ - { - "name": "Ferdi Koomen", - "email": "info@madebyferdi.com" - } - ], "main": "dist/index.js", "types": "types/index.d.ts", "bin": { @@ -57,7 +46,7 @@ "eslint:fix": "eslint . --fix", "prepare": "npm run clean && npm run release", "codecov": "codecov --token=66c30c23-8954-4892-bef9-fbaed0a2e42b", - "docker": "docker build -t eeelenbaas/openapi-typescript-codegen ." + "publish-to-nexus": "npm --no-git-tag-version version patch && npm publish" }, "dependencies": { "@apidevtools/json-schema-ref-parser": "^10.1.0", diff --git a/rollup.config.mjs b/rollup.config.mjs index b25ca8442..85fa11808 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -30,6 +30,7 @@ const handlebarsPlugin = () => ({ knownHelpersOnly: true, knownHelpers: { ifdef: true, + isAnyRequired: true, equals: true, notEquals: true, containsSpaces: true, diff --git a/src/client/interfaces/OperationParameter.d.ts b/src/client/interfaces/OperationParameter.d.ts index 77c0da771..341c27e0a 100644 --- a/src/client/interfaces/OperationParameter.d.ts +++ b/src/client/interfaces/OperationParameter.d.ts @@ -1,7 +1,7 @@ import type { Model } from './Model'; export interface OperationParameter extends Model { - in: 'path' | 'query' | 'header' | 'formData' | 'body' | 'cookie'; + in: 'path' | 'query' | 'header' | 'formData' | 'body' | 'cookie' | 'axios'; prop: string; mediaType: string | null; } diff --git a/src/index.ts b/src/index.ts index e63919085..8f8cea445 100644 --- a/src/index.ts +++ b/src/index.ts @@ -74,7 +74,6 @@ export const generate = async ({ useUnionTypes, useOptions, }); - switch (openApiVersion) { case OpenApiVersion.V2: { const client = parseV2(openApi); diff --git a/src/openApi/v2/parser/getServices.ts b/src/openApi/v2/parser/getServices.ts index d8fe411bb..8bc9e1127 100644 --- a/src/openApi/v2/parser/getServices.ts +++ b/src/openApi/v2/parser/getServices.ts @@ -31,8 +31,27 @@ export const getServices = (openApi: OpenApi): Service[] => { const tags = op.tags?.length ? op.tags.filter(unique) : ['Default']; tags.forEach(tag => { const operation = getOperation(openApi, url, method, tag, op, pathParams); - - // If we have already declared a service, then we should fetch that and + operation.parameters.push({ + in: 'axios', + prop: 'axiosConfig', + export: 'generic', + name: 'axiosConfig', + type: 'AxiosRequestConfig', + base: 'AxiosRequestConfig', + template: null, + link: null, + description: 'AxiosConfig', + isDefinition: false, + isReadOnly: false, + isRequired: false, + isNullable: false, + imports: [], + enum: [], + enums: [], + properties: [], + mediaType: null, + }); + // If we have already de clared a service, then we should fetch that and // append the new method to it. Otherwise we should create a new service object. const service: Service = services.get(operation.service) || { name: operation.service, diff --git a/src/openApi/v3/index.ts b/src/openApi/v3/index.ts index 9dbdadb34..99dfdf960 100644 --- a/src/openApi/v3/index.ts +++ b/src/openApi/v3/index.ts @@ -1,4 +1,5 @@ import type { Client } from '../../client/interfaces/Client'; +import { Model } from '../../client/interfaces/Model'; import type { OpenApi } from './interfaces/OpenApi'; import { getModels } from './parser/getModels'; import { getServer } from './parser/getServer'; @@ -10,10 +11,11 @@ import { getServiceVersion } from './parser/getServiceVersion'; * all the models, services and schema's we should output. * @param openApi The OpenAPI spec that we have loaded from disk. */ +export let models: Model[] = []; export const parse = (openApi: OpenApi): Client => { const version = getServiceVersion(openApi.info.version); const server = getServer(openApi); - const models = getModels(openApi); + models = getModels(openApi); const services = getServices(openApi); return { version, server, models, services }; diff --git a/src/openApi/v3/parser/getEnum.ts b/src/openApi/v3/parser/getEnum.ts index 64c7ca8b5..6e19e5cdd 100644 --- a/src/openApi/v3/parser/getEnum.ts +++ b/src/openApi/v3/parser/getEnum.ts @@ -18,12 +18,14 @@ export const getEnum = (values?: (string | number)[]): Enum[] => { description: null, }; } + return { - name: String(value) - .replace(/\W+/g, '_') + name: `'${value + .replace(/(\W+)/g, '_$1') .replace(/^(\d+)/g, '_$1') + .replace(/\s+/g, '_') .replace(/([a-z])([A-Z]+)/g, '$1_$2') - .toUpperCase(), + .toUpperCase()}'`, value: `'${value.replace(/'/g, "\\'")}'`, type: 'string', description: null, diff --git a/src/openApi/v3/parser/getModelDefault.ts b/src/openApi/v3/parser/getModelDefault.ts index 1736bf1c0..a5c570152 100644 --- a/src/openApi/v3/parser/getModelDefault.ts +++ b/src/openApi/v3/parser/getModelDefault.ts @@ -1,4 +1,5 @@ import type { Model } from '../../../client/interfaces/Model'; +import { models } from '..'; import type { OpenApiSchema } from '../interfaces/OpenApiSchema'; export const getModelDefault = (definition: OpenApiSchema, model?: Model): string | undefined => { @@ -25,6 +26,14 @@ export const getModelDefault = (definition: OpenApiSchema, model?: Model): strin return JSON.stringify(definition.default); case 'string': + const modelName = definition.$ref?.split('/').pop(); + const foundModel = models.find(m => m.name === modelName); + if (foundModel) { + const foundDefault = foundModel.enum.find(en => en.value === `'${definition.default}'`); + if (foundDefault) { + return `${modelName}.${foundDefault.name.replace(/^'(.*)'$/, '$1')}`; + } + } return `'${definition.default}'`; case 'object': diff --git a/src/openApi/v3/parser/getServices.ts b/src/openApi/v3/parser/getServices.ts index d8fe411bb..2621d4230 100644 --- a/src/openApi/v3/parser/getServices.ts +++ b/src/openApi/v3/parser/getServices.ts @@ -31,7 +31,26 @@ export const getServices = (openApi: OpenApi): Service[] => { const tags = op.tags?.length ? op.tags.filter(unique) : ['Default']; tags.forEach(tag => { const operation = getOperation(openApi, url, method, tag, op, pathParams); - + operation.parameters.push({ + in: 'axios', + prop: 'axiosConfig', + export: 'generic', + name: 'axiosConfig', + type: 'AxiosRequestConfig', + base: 'AxiosRequestConfig', + template: null, + link: null, + description: 'AxiosConfig', + isDefinition: false, + isReadOnly: false, + isRequired: false, + isNullable: false, + imports: [], + enum: [], + enums: [], + properties: [], + mediaType: null, + }); // If we have already declared a service, then we should fetch that and // append the new method to it. Otherwise we should create a new service object. const service: Service = services.get(operation.service) || { diff --git a/src/templates/core/BaseHttpRequest.hbs b/src/templates/core/BaseHttpRequest.hbs index 43ff79cbb..45ef2fa14 100644 --- a/src/templates/core/BaseHttpRequest.hbs +++ b/src/templates/core/BaseHttpRequest.hbs @@ -7,6 +7,7 @@ import type { Observable } from 'rxjs'; import type { ApiRequestOptions } from './ApiRequestOptions'; import type { OpenAPIConfig } from './OpenAPI'; {{else}} +import { AxiosInstance, AxiosRequestConfig } from 'axios'; import type { ApiRequestOptions } from './ApiRequestOptions'; import type { CancelablePromise } from './CancelablePromise'; import type { OpenAPIConfig } from './OpenAPI'; @@ -26,6 +27,7 @@ export abstract class BaseHttpRequest { {{#equals @root.httpClient 'angular'}} public abstract request(options: ApiRequestOptions): Observable; {{else}} - public abstract request(options: ApiRequestOptions): CancelablePromise; + public axiosInstance: AxiosInstance; + public abstract request(options: ApiRequestOptions, axiosConfig?: AxiosRequestConfig): CancelablePromise; {{/equals}} } diff --git a/src/templates/core/axios/request.hbs b/src/templates/core/axios/request.hbs index 6612f1614..7073eaec8 100644 --- a/src/templates/core/axios/request.hbs +++ b/src/templates/core/axios/request.hbs @@ -70,7 +70,7 @@ import type { OpenAPIConfig } from './OpenAPI'; * @returns CancelablePromise * @throws ApiError */ -export const request = (config: OpenAPIConfig, options: ApiRequestOptions, axiosClient: AxiosInstance = axios): CancelablePromise => { +export const request = (config: OpenAPIConfig, options: ApiRequestOptions, axiosClient: AxiosInstance = axios, axiosConfig?: AxiosRequestConfig,): CancelablePromise => { return new CancelablePromise(async (resolve, reject, onCancel) => { try { const url = getUrl(config, options); @@ -79,7 +79,7 @@ export const request = (config: OpenAPIConfig, options: ApiRequestOptions, ax const headers = await getHeaders(config, options, formData); if (!onCancel.isCancelled) { - const response = await sendRequest(config, options, url, body, formData, headers, onCancel, axiosClient); + const response = await sendRequest(config, options, url, body, formData, headers, onCancel, axiosClient, axiosConfig); const responseBody = getResponseBody(response); const responseHeader = getResponseHeader(response, options.responseHeader); diff --git a/src/templates/core/axios/sendRequest.hbs b/src/templates/core/axios/sendRequest.hbs index 51492bf3f..6d2953db5 100644 --- a/src/templates/core/axios/sendRequest.hbs +++ b/src/templates/core/axios/sendRequest.hbs @@ -6,7 +6,8 @@ export const sendRequest = async ( formData: FormData | undefined, headers: Record, onCancel: OnCancel, - axiosClient: AxiosInstance + axiosClient: AxiosInstance, + axiosConfig?: AxiosRequestConfig, ): Promise> => { const source = axios.CancelToken.source(); @@ -17,6 +18,7 @@ export const sendRequest = async ( method: options.method, withCredentials: config.WITH_CREDENTIALS, cancelToken: source.token, + ...axiosConfig, }; onCancel(() => source.cancel('The user aborted a request.')); diff --git a/src/templates/exportService.hbs b/src/templates/exportService.hbs index d6bccbbeb..3d5f44ecb 100644 --- a/src/templates/exportService.hbs +++ b/src/templates/exportService.hbs @@ -1,5 +1,5 @@ {{>header}} - +import { AxiosRequestConfig } from "axios"; {{#equals @root.httpClient 'angular'}} {{#if @root.exportClient}} import { Injectable } from '@angular/core'; @@ -13,7 +13,7 @@ import type { Observable } from 'rxjs'; {{/equals}} {{#if imports}} {{#each imports}} -import type { {{{this}}} } from '../models/{{{this}}}'; +import { {{{this}}} } from '../models/{{{this}}}'; {{/each}} {{/if}} @@ -75,7 +75,7 @@ export class {{{name}}}{{{@root.postfix}}} { public {{{name}}}({{>parameters}}): Observable<{{>result}}> { return this.httpRequest.request({ {{else}} - public {{{name}}}({{>parameters}}): CancelablePromise<{{>result}}> { + public {{{name}}}({{>parameters}}{{isAnyRequired parameters ' = {}'}}): CancelablePromise<{{>result}}> { return this.httpRequest.request({ {{/equals}} {{else}} @@ -145,7 +145,7 @@ export class {{{name}}}{{{@root.postfix}}} { {{/each}} }, {{/if}} - }); + }, axiosConfig); } {{/each}} diff --git a/src/templates/index.hbs b/src/templates/index.hbs index 6f5b27d8c..0a45f883d 100644 --- a/src/templates/index.hbs +++ b/src/templates/index.hbs @@ -2,6 +2,7 @@ {{#if @root.exportClient}} export { {{{clientName}}} } from './{{{clientName}}}'; +export { {{{clientName}}}Client } from "./instance"; {{/if}} {{#if @root.exportCore}} diff --git a/src/templates/instance.hbs b/src/templates/instance.hbs new file mode 100644 index 000000000..e89c1f763 --- /dev/null +++ b/src/templates/instance.hbs @@ -0,0 +1,36 @@ +import Axios, { AxiosRequestConfig } from 'axios'; +import axiosRetry from 'axios-retry'; + +import { BaseHttpRequest } from './core/BaseHttpRequest'; +import { OpenAPIConfig } from './core/OpenAPI'; +import { ApiRequestOptions } from './core/ApiRequestOptions'; +import { CancelablePromise } from './core/CancelablePromise'; +import { request as __request } from './core/request'; + +import { {{clientName}} } from './{{clientName}}'; + +type HttpRequestConstructor = new (config: OpenAPIConfig) => BaseHttpRequest; + +export class AxiosHttpRequestWithRetry extends BaseHttpRequest { + axiosInstance = Axios.create(); + + constructor(config: OpenAPIConfig) { + super(config); + axiosRetry(this.axiosInstance, { + retryCondition: (error) => { + if (!error.response) return false; + + return error.response.status >= 400; + }, + }); + } + + public override request(options: ApiRequestOptions, axiosConfig?: AxiosRequestConfig): CancelablePromise { + return __request(this.config, options, this.axiosInstance, axiosConfig); + } +} + +export const {{clientName}}Client = ( + config?: Partial, + HttpRequest: HttpRequestConstructor = AxiosHttpRequestWithRetry +) => new {{clientName}}(config, HttpRequest); diff --git a/src/utils/registerHandlebarHelpers.ts b/src/utils/registerHandlebarHelpers.ts index 88f47c19b..558b90e22 100644 --- a/src/utils/registerHandlebarHelpers.ts +++ b/src/utils/registerHandlebarHelpers.ts @@ -4,6 +4,7 @@ import { EOL } from 'os'; import type { Enum } from '../client/interfaces/Enum'; import type { Model } from '../client/interfaces/Model'; +import { OperationParameter } from '../client/interfaces/OperationParameter'; import type { HttpClient } from '../HttpClient'; import { unique } from './unique'; @@ -19,7 +20,9 @@ export const registerHandlebarHelpers = (root: { } return options.inverse(this); }); - + Handlebars.registerHelper('isAnyRequired', (value: OperationParameter[], returnValue: string) => { + return value.some(v => v.isRequired) ? '' : returnValue; + }); Handlebars.registerHelper( 'equals', function (this: any, a: string, b: string, options: Handlebars.HelperOptions): string { diff --git a/src/utils/registerHandlebarTemplates.ts b/src/utils/registerHandlebarTemplates.ts index bf77cbdc1..866a332ee 100644 --- a/src/utils/registerHandlebarTemplates.ts +++ b/src/utils/registerHandlebarTemplates.ts @@ -56,6 +56,7 @@ import templateExportModel from '../templates/exportModel.hbs'; import templateExportSchema from '../templates/exportSchema.hbs'; import templateExportService from '../templates/exportService.hbs'; import templateIndex from '../templates/index.hbs'; +import templateInstance from '../templates/instance.hbs'; import partialBase from '../templates/partials/base.hbs'; import partialExportComposition from '../templates/partials/exportComposition.hbs'; import partialExportEnum from '../templates/partials/exportEnum.hbs'; @@ -88,6 +89,7 @@ import { registerHandlebarHelpers } from './registerHandlebarHelpers'; export interface Templates { index: Handlebars.TemplateDelegate; client: Handlebars.TemplateDelegate; + instance: Handlebars.TemplateDelegate; exports: { model: Handlebars.TemplateDelegate; schema: Handlebars.TemplateDelegate; @@ -119,6 +121,7 @@ export const registerHandlebarTemplates = (root: { // Main templates (entry points for the files we write to disk) const templates: Templates = { index: Handlebars.template(templateIndex), + instance: Handlebars.template(templateInstance), client: Handlebars.template(templateClient), exports: { model: Handlebars.template(templateExportModel), diff --git a/src/utils/writeClient.spec.ts b/src/utils/writeClient.spec.ts index 3c06a95a5..5031b2359 100644 --- a/src/utils/writeClient.spec.ts +++ b/src/utils/writeClient.spec.ts @@ -19,6 +19,7 @@ describe('writeClient', () => { const templates: Templates = { index: () => 'index', client: () => 'client', + instance: () => 'instance', exports: { model: () => 'model', schema: () => 'schema', diff --git a/src/utils/writeClient.ts b/src/utils/writeClient.ts index cea2f3d88..05b071d9a 100644 --- a/src/utils/writeClient.ts +++ b/src/utils/writeClient.ts @@ -13,6 +13,7 @@ import { writeClientIndex } from './writeClientIndex'; import { writeClientModels } from './writeClientModels'; import { writeClientSchemas } from './writeClientSchemas'; import { writeClientServices } from './writeClientServices'; +import { writeInstance } from './writeInstance'; /** * Write our OpenAPI client, using the given templates at the given output @@ -97,6 +98,7 @@ export const writeClient = async ( if (isDefined(clientName)) { await mkdir(outputPath); await writeClientClass(client, templates, outputPath, httpClient, clientName, indent, postfixServices); + await writeInstance(templates, outputPath, clientName, indent); } if (exportCore || exportServices || exportSchemas || exportModels) { diff --git a/src/utils/writeClientClass.spec.ts b/src/utils/writeClientClass.spec.ts index 102f2eb57..7ddd3b4b2 100644 --- a/src/utils/writeClientClass.spec.ts +++ b/src/utils/writeClientClass.spec.ts @@ -19,6 +19,7 @@ describe('writeClientClass', () => { const templates: Templates = { index: () => 'index', client: () => 'client', + instance: () => 'instance', exports: { model: () => 'model', schema: () => 'schema', diff --git a/src/utils/writeClientCore.spec.ts b/src/utils/writeClientCore.spec.ts index 7db71f59b..133fe04d0 100644 --- a/src/utils/writeClientCore.spec.ts +++ b/src/utils/writeClientCore.spec.ts @@ -22,6 +22,7 @@ describe('writeClientCore', () => { const templates: Templates = { index: () => 'index', client: () => 'client', + instance: () => 'instance', exports: { model: () => 'model', schema: () => 'schema', diff --git a/src/utils/writeClientIndex.spec.ts b/src/utils/writeClientIndex.spec.ts index a7d5b610a..921841ece 100644 --- a/src/utils/writeClientIndex.spec.ts +++ b/src/utils/writeClientIndex.spec.ts @@ -19,6 +19,7 @@ describe('writeClientIndex', () => { const templates: Templates = { index: () => 'index', client: () => 'client', + instance: () => 'instance', exports: { model: () => 'model', schema: () => 'schema', diff --git a/src/utils/writeClientModels.spec.ts b/src/utils/writeClientModels.spec.ts index ee0f2b4f6..f47399eff 100644 --- a/src/utils/writeClientModels.spec.ts +++ b/src/utils/writeClientModels.spec.ts @@ -35,6 +35,7 @@ describe('writeClientModels', () => { const templates: Templates = { index: () => 'index', client: () => 'client', + instance: () => 'instance', exports: { model: () => 'model', schema: () => 'schema', diff --git a/src/utils/writeClientSchemas.spec.ts b/src/utils/writeClientSchemas.spec.ts index e75928b8c..40c37ba7b 100644 --- a/src/utils/writeClientSchemas.spec.ts +++ b/src/utils/writeClientSchemas.spec.ts @@ -35,6 +35,7 @@ describe('writeClientSchemas', () => { const templates: Templates = { index: () => 'index', client: () => 'client', + instance: () => 'instance', exports: { model: () => 'model', schema: () => 'schema', diff --git a/src/utils/writeClientServices.spec.ts b/src/utils/writeClientServices.spec.ts index f936d6609..7b058f261 100644 --- a/src/utils/writeClientServices.spec.ts +++ b/src/utils/writeClientServices.spec.ts @@ -23,6 +23,7 @@ describe('writeClientServices', () => { const templates: Templates = { index: () => 'index', client: () => 'client', + instance: () => 'instance', exports: { model: () => 'model', schema: () => 'schema', diff --git a/src/utils/writeInstance.ts b/src/utils/writeInstance.ts new file mode 100644 index 000000000..57d9ebc3c --- /dev/null +++ b/src/utils/writeInstance.ts @@ -0,0 +1,27 @@ +import { resolve } from 'path'; + +import type { Indent } from '../Indent'; +import { writeFile } from './fileSystem'; +import { formatCode as f } from './formatCode'; +import { formatIndentation as i } from './formatIndentation'; +import type { Templates } from './registerHandlebarTemplates'; + +/** + * Generate the OpenAPI instance client index file using the Handlebar template and write it to disk. + * The index file just contains all the exports you need to use the client as a standalone + * library. But yuo can also import individual models and services directly. + * @param templates The loaded handlebar templates + * @param outputPath Directory to write the generated files to + * @param clientName Custom client class name + * @param indent Indentation options (4, 2 or tab) + */ +export const writeInstance = async ( + templates: Templates, + outputPath: string, + clientName: string, + indent: Indent +): Promise => { + const templateResult = templates.instance({ clientName }); + + await writeFile(resolve(outputPath, `instance.ts`), i(f(templateResult), indent)); +};