From e273b2f12c0cde3ad10257730f020793d14320df Mon Sep 17 00:00:00 2001 From: Justin Calleja Date: Mon, 18 Sep 2023 11:17:51 +0200 Subject: [PATCH 1/2] chore: add nextjs_fetch client --- README.md | 2 +- bin/index.js | 2 +- src/HttpClient.ts | 1 + src/index.ts | 2 +- src/templates/core/ApiRequestOptions.hbs | 19 ++++ .../core/nextjs_fetch/getHeaders.hbs | 40 ++++++++ .../core/nextjs_fetch/getRequestBody.hbs | 12 +++ .../core/nextjs_fetch/getResponseBody.hbs | 19 ++++ .../core/nextjs_fetch/getResponseHeader.hbs | 9 ++ src/templates/core/nextjs_fetch/request.hbs | 94 +++++++++++++++++++ .../core/nextjs_fetch/sendRequest.hbs | 30 ++++++ src/templates/core/request.hbs | 1 + src/templates/exportService.hbs | 17 ++++ src/templates/partials/base.hbs | 1 + src/utils/getHttpRequestName.ts | 2 + src/utils/registerHandlebarTemplates.ts | 14 +++ 16 files changed, 262 insertions(+), 3 deletions(-) create mode 100644 src/templates/core/nextjs_fetch/getHeaders.hbs create mode 100644 src/templates/core/nextjs_fetch/getRequestBody.hbs create mode 100644 src/templates/core/nextjs_fetch/getResponseBody.hbs create mode 100644 src/templates/core/nextjs_fetch/getResponseHeader.hbs create mode 100644 src/templates/core/nextjs_fetch/request.hbs create mode 100644 src/templates/core/nextjs_fetch/sendRequest.hbs diff --git a/README.md b/README.md index b851329a8..52599b85a 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ $ openapi --help -V, --version output the version number -i, --input OpenAPI specification, can be a path, url or string content (required) -o, --output Output directory (required) - -c, --client HTTP client to generate [fetch, xhr, node, axios, angular] (default: "fetch") + -c, --client HTTP client to generate [nextjs_fetch, fetch, xhr, node, axios, angular] (default: "fetch") --name Custom client class name --useOptions Use options instead of arguments --useUnionTypes Use union types instead of enums diff --git a/bin/index.js b/bin/index.js index 32f2fecbc..8a99d8e64 100755 --- a/bin/index.js +++ b/bin/index.js @@ -12,7 +12,7 @@ const params = program .version(pkg.version) .requiredOption('-i, --input ', 'OpenAPI specification, can be a path, url or string content (required)') .requiredOption('-o, --output ', 'Output directory (required)') - .option('-c, --client ', 'HTTP client to generate [fetch, xhr, node, axios, angular]', 'fetch') + .option('-c, --client ', 'HTTP client to generate [nextjs_fetch, fetch, xhr, node, axios, angular]', 'fetch') .option('--name ', 'Custom client class name') .option('--useOptions', 'Use options instead of arguments') .option('--useUnionTypes', 'Use union types instead of enums') diff --git a/src/HttpClient.ts b/src/HttpClient.ts index 40c77c7c9..f15d18743 100644 --- a/src/HttpClient.ts +++ b/src/HttpClient.ts @@ -1,6 +1,7 @@ export enum HttpClient { FETCH = 'fetch', XHR = 'xhr', + NEXTJS_FETCH = 'nextjs_fetch', NODE = 'node', AXIOS = 'axios', ANGULAR = 'angular', diff --git a/src/index.ts b/src/index.ts index e63919085..5a166a78f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -36,7 +36,7 @@ export type Options = { * service layer, etc. * @param input The relative location of the OpenAPI spec * @param output The relative location of the output directory - * @param httpClient The selected httpClient (fetch, xhr, node or axios) + * @param httpClient The selected httpClient (nextjs_fetch, fetch, xhr, node or axios) * @param clientName Custom client class name * @param useOptions Use options or arguments functions * @param useUnionTypes Use union types instead of enums diff --git a/src/templates/core/ApiRequestOptions.hbs b/src/templates/core/ApiRequestOptions.hbs index 355929a71..58bdce945 100644 --- a/src/templates/core/ApiRequestOptions.hbs +++ b/src/templates/core/ApiRequestOptions.hbs @@ -1,5 +1,23 @@ {{>header}} + +{{#equals @root.httpClient 'nextjs_fetch'}} +export type ApiRequestOptions = { + readonly method: 'GET' | 'PUT' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD' | 'PATCH'; + readonly url: string; + readonly path?: Record; + readonly cookies?: Record; + readonly headers?: Record; + readonly query?: Record; + readonly formData?: Record; + readonly body?: any; + readonly mediaType?: string; + readonly responseHeader?: string; + readonly errors?: Record; + readonly cache?: RequestInit['cache'] + readonly next?: { revalidate: number } +}; +{{else}} export type ApiRequestOptions = { readonly method: 'GET' | 'PUT' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD' | 'PATCH'; readonly url: string; @@ -13,3 +31,4 @@ export type ApiRequestOptions = { readonly responseHeader?: string; readonly errors?: Record; }; +{{/equals}} diff --git a/src/templates/core/nextjs_fetch/getHeaders.hbs b/src/templates/core/nextjs_fetch/getHeaders.hbs new file mode 100644 index 000000000..3aca7aef3 --- /dev/null +++ b/src/templates/core/nextjs_fetch/getHeaders.hbs @@ -0,0 +1,40 @@ +export const getHeaders = async (config: OpenAPIConfig, options: ApiRequestOptions): Promise => { + const token = await resolve(options, config.TOKEN); + const username = await resolve(options, config.USERNAME); + const password = await resolve(options, config.PASSWORD); + const additionalHeaders = await resolve(options, config.HEADERS); + + const headers = Object.entries({ + Accept: 'application/json', + ...additionalHeaders, + ...options.headers, + }) + .filter(([_, value]) => isDefined(value)) + .reduce((headers, [key, value]) => ({ + ...headers, + [key]: String(value), + }), {} as Record); + + if (isStringWithValue(token)) { + headers['Authorization'] = `Bearer ${token}`; + } + + if (isStringWithValue(username) && isStringWithValue(password)) { + const credentials = base64(`${username}:${password}`); + headers['Authorization'] = `Basic ${credentials}`; + } + + if (options.body) { + if (options.mediaType) { + headers['Content-Type'] = options.mediaType; + } else if (isBlob(options.body)) { + headers['Content-Type'] = options.body.type || 'application/octet-stream'; + } else if (isString(options.body)) { + headers['Content-Type'] = 'text/plain'; + } else if (!isFormData(options.body)) { + headers['Content-Type'] = 'application/json'; + } + } + + return new Headers(headers); +}; diff --git a/src/templates/core/nextjs_fetch/getRequestBody.hbs b/src/templates/core/nextjs_fetch/getRequestBody.hbs new file mode 100644 index 000000000..a82b2719d --- /dev/null +++ b/src/templates/core/nextjs_fetch/getRequestBody.hbs @@ -0,0 +1,12 @@ +export const getRequestBody = (options: ApiRequestOptions): any => { + if (options.body !== undefined) { + if (options.mediaType?.includes('/json')) { + return JSON.stringify(options.body) + } else if (isString(options.body) || isBlob(options.body) || isFormData(options.body)) { + return options.body; + } else { + return JSON.stringify(options.body); + } + } + return undefined; +}; diff --git a/src/templates/core/nextjs_fetch/getResponseBody.hbs b/src/templates/core/nextjs_fetch/getResponseBody.hbs new file mode 100644 index 000000000..1011380ee --- /dev/null +++ b/src/templates/core/nextjs_fetch/getResponseBody.hbs @@ -0,0 +1,19 @@ +export const getResponseBody = async (response: Response): Promise => { + if (response.status !== 204) { + try { + const contentType = response.headers.get('Content-Type'); + if (contentType) { + const jsonTypes = ['application/json', 'application/problem+json'] + const isJSON = jsonTypes.some(type => contentType.toLowerCase().startsWith(type)); + if (isJSON) { + return await response.json(); + } else { + return await response.text(); + } + } + } catch (error) { + console.error(error); + } + } + return undefined; +}; diff --git a/src/templates/core/nextjs_fetch/getResponseHeader.hbs b/src/templates/core/nextjs_fetch/getResponseHeader.hbs new file mode 100644 index 000000000..cc415c0a7 --- /dev/null +++ b/src/templates/core/nextjs_fetch/getResponseHeader.hbs @@ -0,0 +1,9 @@ +export const getResponseHeader = (response: Response, responseHeader?: string): string | undefined => { + if (responseHeader) { + const content = response.headers.get(responseHeader); + if (isString(content)) { + return content; + } + } + return undefined; +}; diff --git a/src/templates/core/nextjs_fetch/request.hbs b/src/templates/core/nextjs_fetch/request.hbs new file mode 100644 index 000000000..4af6f9440 --- /dev/null +++ b/src/templates/core/nextjs_fetch/request.hbs @@ -0,0 +1,94 @@ +{{>header}} + +import { ApiError } from './ApiError'; +import type { ApiRequestOptions } from './ApiRequestOptions'; +import type { ApiResult } from './ApiResult'; +import { CancelablePromise } from './CancelablePromise'; +import type { OnCancel } from './CancelablePromise'; +import type { OpenAPIConfig } from './OpenAPI'; + +{{>functions/isDefined}} + + +{{>functions/isString}} + + +{{>functions/isStringWithValue}} + + +{{>functions/isBlob}} + + +{{>functions/isFormData}} + + +{{>functions/base64}} + + +{{>functions/getQueryString}} + + +{{>functions/getUrl}} + + +{{>functions/getFormData}} + + +{{>functions/resolve}} + + +{{>fetch/getHeaders}} + + +{{>fetch/getRequestBody}} + + +{{>fetch/sendRequest}} + + +{{>fetch/getResponseHeader}} + + +{{>fetch/getResponseBody}} + + +{{>functions/catchErrorCodes}} + + +/** + * Request method + * @param config The OpenAPI configuration object + * @param options The request options from the service + * @returns CancelablePromise + * @throws ApiError + */ +export const request = (config: OpenAPIConfig, options: ApiRequestOptions): CancelablePromise => { + return new CancelablePromise(async (resolve, reject, onCancel) => { + try { + const url = getUrl(config, options); + const formData = getFormData(options); + const body = getRequestBody(options); + const headers = await getHeaders(config, options); + + if (!onCancel.isCancelled) { + const response = await sendRequest(config, options, url, body, formData, headers, onCancel); + const responseBody = await getResponseBody(response); + const responseHeader = getResponseHeader(response, options.responseHeader); + + const result: ApiResult = { + url, + ok: response.ok, + status: response.status, + statusText: response.statusText, + body: responseHeader ?? responseBody, + }; + + catchErrorCodes(options, result); + + resolve(result.body); + } + } catch (error) { + reject(error); + } + }); +}; diff --git a/src/templates/core/nextjs_fetch/sendRequest.hbs b/src/templates/core/nextjs_fetch/sendRequest.hbs new file mode 100644 index 000000000..540a06ea8 --- /dev/null +++ b/src/templates/core/nextjs_fetch/sendRequest.hbs @@ -0,0 +1,30 @@ +export const sendRequest = async ( + config: OpenAPIConfig, + options: ApiRequestOptions, + url: string, + body: any, + formData: FormData | undefined, + headers: Headers, + onCancel: OnCancel +): Promise => { + const controller = new AbortController(); + + const request: RequestInit = { + headers, + body: body ?? formData, + method: options.method, + signal: controller.signal, +{{#equals @root.httpClient 'nextjs_fetch'}} + cache: options.cache, + next: options.next, +{{/equals}} + }; + + if (config.WITH_CREDENTIALS) { + request.credentials = config.CREDENTIALS; + } + + onCancel(() => controller.abort()); + + return await fetch(url, request); +}; diff --git a/src/templates/core/request.hbs b/src/templates/core/request.hbs index c4e2478dd..0439fb395 100644 --- a/src/templates/core/request.hbs +++ b/src/templates/core/request.hbs @@ -1,3 +1,4 @@ +{{~#equals @root.httpClient 'nextjs_fetch'}}{{>nextjs_fetch/request}}{{/equals~}} {{~#equals @root.httpClient 'fetch'}}{{>fetch/request}}{{/equals~}} {{~#equals @root.httpClient 'xhr'}}{{>xhr/request}}{{/equals~}} {{~#equals @root.httpClient 'axios'}}{{>axios/request}}{{/equals~}} diff --git a/src/templates/exportService.hbs b/src/templates/exportService.hbs index d6bccbbeb..c3ad8f498 100644 --- a/src/templates/exportService.hbs +++ b/src/templates/exportService.hbs @@ -30,6 +30,9 @@ import type { BaseHttpRequest } from '../core/BaseHttpRequest'; import { OpenAPI } from '../core/OpenAPI'; import { request as __request } from '../core/request'; {{/if}} +{{#equals @root.httpClient 'nextjs_fetch'}} +import type { ApiRequestOptions } from '../core/ApiRequestOptions' +{{/equals}} {{#equals @root.httpClient 'angular'}} @Injectable({ @@ -75,17 +78,27 @@ export class {{{name}}}{{{@root.postfix}}} { public {{{name}}}({{>parameters}}): Observable<{{>result}}> { return this.httpRequest.request({ {{else}} + {{#equals @root.httpClient 'nextjs_fetch'}} + public {{{name}}}({{>parameters}}{{#if parameters}}, {{/if}}{ cache, next }: { cache?: ApiRequestOptions['cache'], next?: ApiRequestOptions['next'] } = {}): CancelablePromise<{{>result}}> { + return this.httpRequest.request({ + {{else}} public {{{name}}}({{>parameters}}): CancelablePromise<{{>result}}> { return this.httpRequest.request({ {{/equals}} + {{/equals}} {{else}} {{#equals @root.httpClient 'angular'}} public {{{name}}}({{>parameters}}): Observable<{{>result}}> { return __request(OpenAPI, this.http, { {{else}} + {{#equals @root.httpClient 'nextjs_fetch'}} + public static {{{name}}}({{>parameters}}{{#if parameters}}, {{/if}}{ cache, next }: { cache?: ApiRequestOptions['cache'], next?: ApiRequestOptions['next'] } = {}): CancelablePromise<{{>result}}> { + return __request(OpenAPI, { + {{else}} public static {{{name}}}({{>parameters}}): CancelablePromise<{{>result}}> { return __request(OpenAPI, { {{/equals}} + {{/equals}} {{/if}} method: '{{{method}}}', url: '{{{path}}}', @@ -145,6 +158,10 @@ export class {{{name}}}{{{@root.postfix}}} { {{/each}} }, {{/if}} + {{#equals @root.httpClient 'nextjs_fetch'}} + cache, + next, + {{/equals}} }); } diff --git a/src/templates/partials/base.hbs b/src/templates/partials/base.hbs index 1799e7d2a..0d4034eb1 100644 --- a/src/templates/partials/base.hbs +++ b/src/templates/partials/base.hbs @@ -1,5 +1,6 @@ {{~#equals base 'binary'~}} {{~#equals @root.httpClient 'fetch'}}Blob{{/equals~}} +{{~#equals @root.httpClient 'nextjs_fetch'}}Blob{{/equals~}} {{~#equals @root.httpClient 'xhr'}}Blob{{/equals~}} {{~#equals @root.httpClient 'axios'}}Blob{{/equals~}} {{~#equals @root.httpClient 'angular'}}Blob{{/equals~}} diff --git a/src/utils/getHttpRequestName.ts b/src/utils/getHttpRequestName.ts index 53b7ad05a..0b66f977c 100644 --- a/src/utils/getHttpRequestName.ts +++ b/src/utils/getHttpRequestName.ts @@ -8,6 +8,8 @@ export const getHttpRequestName = (httpClient: HttpClient): string => { switch (httpClient) { case HttpClient.FETCH: return 'FetchHttpRequest'; + case HttpClient.NEXTJS_FETCH: + return 'NextjsFetchHttpRequest'; case HttpClient.XHR: return 'XHRHttpRequest'; case HttpClient.NODE: diff --git a/src/utils/registerHandlebarTemplates.ts b/src/utils/registerHandlebarTemplates.ts index bf77cbdc1..eeda3dec9 100644 --- a/src/utils/registerHandlebarTemplates.ts +++ b/src/utils/registerHandlebarTemplates.ts @@ -38,6 +38,12 @@ import functionIsStringWithValue from '../templates/core/functions/isStringWithV import functionIsSuccess from '../templates/core/functions/isSuccess.hbs'; import functionResolve from '../templates/core/functions/resolve.hbs'; import templateCoreHttpRequest from '../templates/core/HttpRequest.hbs'; +import nextjsFetchGetHeaders from '../templates/core/nextjs_fetch/getHeaders.hbs'; +import nextjsFetchGetRequestBody from '../templates/core/nextjs_fetch/getRequestBody.hbs'; +import nextjsFetchGetResponseBody from '../templates/core/nextjs_fetch/getResponseBody.hbs'; +import nextjsFetchGetResponseHeader from '../templates/core/nextjs_fetch/getResponseHeader.hbs'; +import nextjsFetchRequest from '../templates/core/nextjs_fetch/request.hbs'; +import nextjsFetchSendRequest from '../templates/core/nextjs_fetch/sendRequest.hbs'; import nodeGetHeaders from '../templates/core/node/getHeaders.hbs'; import nodeGetRequestBody from '../templates/core/node/getRequestBody.hbs'; import nodeGetResponseBody from '../templates/core/node/getResponseBody.hbs'; @@ -188,6 +194,14 @@ export const registerHandlebarTemplates = (root: { Handlebars.registerPartial('fetch/sendRequest', Handlebars.template(fetchSendRequest)); Handlebars.registerPartial('fetch/request', Handlebars.template(fetchRequest)); + // Specific files for the nextjs_fetch client implementation + Handlebars.registerPartial('nextjs_fetch/getHeaders', Handlebars.template(nextjsFetchGetHeaders)); + Handlebars.registerPartial('nextjs_fetch/getRequestBody', Handlebars.template(nextjsFetchGetRequestBody)); + Handlebars.registerPartial('nextjs_fetch/getResponseBody', Handlebars.template(nextjsFetchGetResponseBody)); + Handlebars.registerPartial('nextjs_fetch/getResponseHeader', Handlebars.template(nextjsFetchGetResponseHeader)); + Handlebars.registerPartial('nextjs_fetch/sendRequest', Handlebars.template(nextjsFetchSendRequest)); + Handlebars.registerPartial('nextjs_fetch/request', Handlebars.template(nextjsFetchRequest)); + // Specific files for the xhr client implementation Handlebars.registerPartial('xhr/getHeaders', Handlebars.template(xhrGetHeaders)); Handlebars.registerPartial('xhr/getRequestBody', Handlebars.template(xhrGetRequestBody)); From b107e8e8322a7ad16137b1937160b7eb2489701a Mon Sep 17 00:00:00 2001 From: Justin Calleja Date: Tue, 2 Jan 2024 13:57:40 +0100 Subject: [PATCH 2/2] chore: publish to npm public registry under diff name --- README.md | 4 ++++ package-lock.json | 4 ++-- package.json | 6 +++--- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 52599b85a..590b6f268 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ +⚠️ this is just a fork to add some options to `fetch` client. See last few commits on this branch: + +https://github.com/justin-calleja/openapi-typescript-codegen/tree/nextjsfetch + # OpenAPI Typescript Codegen [![NPM][npm-image]][npm-url] diff --git a/package-lock.json b/package-lock.json index 544523784..ce34b9805 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "openapi-typescript-codegen", + "name": "nextjsfetch-openapi-typescript-codegen", "version": "0.25.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "openapi-typescript-codegen", + "name": "nextjsfetch-openapi-typescript-codegen", "version": "0.25.0", "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 8a588aa92..aec65c5b5 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { - "name": "openapi-typescript-codegen", + "name": "nextjsfetch-openapi-typescript-codegen", "version": "0.25.0", "description": "Library that generates Typescript clients based on the OpenAPI specification.", "author": "Ferdi Koomen", - "homepage": "https://github.com/ferdikoomen/openapi-typescript-codegen", + "homepage": "https://github.com/justin-calleja/openapi-typescript-codegen/tree/nextjsfetch", "repository": { "type": "git", - "url": "git+https://github.com/ferdikoomen/openapi-typescript-codegen.git" + "url": "git+http/github.com/ferdikoomen/openapi-typescript-codegen.git" }, "bugs": { "url": "https://github.com/ferdikoomen/openapi-typescript-codegen/issues"