From d1155502553f249d5b39e9637b997416038faca8 Mon Sep 17 00:00:00 2001 From: soartec-lab Date: Fri, 3 May 2024 04:39:57 +0000 Subject: [PATCH 1/9] feat: add `@orval/fetch` package --- packages/core/src/types.ts | 1 + packages/fetch/README.md | 29 ++++++++ packages/fetch/package.json | 18 +++++ packages/fetch/src/index.ts | 124 +++++++++++++++++++++++++++++++++++ packages/fetch/tsconfig.json | 4 ++ packages/orval/package.json | 1 + packages/orval/src/client.ts | 2 + 7 files changed, 179 insertions(+) create mode 100644 packages/fetch/README.md create mode 100644 packages/fetch/package.json create mode 100644 packages/fetch/src/index.ts create mode 100644 packages/fetch/tsconfig.json diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 6d972694b..6c9072b72 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -212,6 +212,7 @@ export const OutputClient = { SWR: 'swr', ZOD: 'zod', HONO: 'hono', + FETCH: 'fetch', } as const; export type OutputClient = (typeof OutputClient)[keyof typeof OutputClient]; diff --git a/packages/fetch/README.md b/packages/fetch/README.md new file mode 100644 index 000000000..4364bb223 --- /dev/null +++ b/packages/fetch/README.md @@ -0,0 +1,29 @@ +[![npm version](https://badge.fury.io/js/orval.svg)](https://badge.fury.io/js/orval) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![tests](https://github.com/anymaniax/orval/actions/workflows/tests.yaml/badge.svg)](https://github.com/anymaniax/orval/actions/workflows/tests.yaml) + +

+ orval - Restfull Client Generator +

+

+ Visit orval.dev for docs, guides, API and beer! +

+ +### Code Generation + +`orval` is able to generate client with appropriate type-signatures (TypeScript) from any valid OpenAPI v3 or Swagger v2 specification, either in `yaml` or `json` formats. + +`Generate`, `valid`, `cache` and `mock` in your React, Vue, Svelte and Angular applications all with your OpenAPI specification. + +### Samples + +You can find below some samples + +- [react app](https://github.com/anymaniax/orval/tree/master/samples/react-app) +- [react query](https://github.com/anymaniax/orval/tree/master/samples/react-query) +- [svelte query](https://github.com/anymaniax/orval/tree/master/samples/svelte-query) +- [vue query](https://github.com/anymaniax/orval/tree/master/samples/vue-query) +- [react app with swr](https://github.com/anymaniax/orval/tree/master/samples/react-app-with-swr) +- [nx fastify react](https://github.com/anymaniax/orval/tree/master/samples/nx-fastify-react) +- [angular app](https://github.com/anymaniax/orval/tree/master/samples/angular-app) +- [hono](https://github.com/anymaniax/orval/tree/master/samples/hono) diff --git a/packages/fetch/package.json b/packages/fetch/package.json new file mode 100644 index 000000000..52189947a --- /dev/null +++ b/packages/fetch/package.json @@ -0,0 +1,18 @@ +{ + "name": "@orval/fetch", + "version": "6.28.2", + "license": "MIT", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "tsup ./src/index.ts --target node12 --clean --sourcemap --dts", + "dev": "tsup ./src/index.ts --target node12 --clean --sourcemap --watch src", + "lint": "eslint src/**/*.ts" + }, + "dependencies": { + "@orval/core": "6.28.2" + } +} diff --git a/packages/fetch/src/index.ts b/packages/fetch/src/index.ts new file mode 100644 index 000000000..efdbc9105 --- /dev/null +++ b/packages/fetch/src/index.ts @@ -0,0 +1,124 @@ +import { + ClientBuilder, + ClientDependenciesBuilder, + ClientGeneratorsBuilder, + generateFormDataAndUrlEncodedFunction, + generateVerbImports, + GeneratorDependency, + GeneratorOptions, + GeneratorVerbOptions, + stringify, + toObjectString, + generateBodyOptions, + isObject, +} from '@orval/core'; + +const PARAMS_SERIALIZER_DEPENDENCIES: GeneratorDependency[] = [ + { + exports: [ + { + name: 'qs', + default: true, + values: true, + syntheticDefaultImport: true, + }, + ], + dependency: 'qs', + }, +]; + +export const getDependencies: ClientDependenciesBuilder = ( + hasParamsSerializerOptions: boolean, +) => [...(hasParamsSerializerOptions ? PARAMS_SERIALIZER_DEPENDENCIES : [])]; + +const generateRequestFunction = ( + { + queryParams, + operationName, + response, + body, + props, + verb, + formData, + formUrlEncoded, + override, + }: GeneratorVerbOptions, + { route }: GeneratorOptions, +) => { + const isRequestOptions = override?.requestOptions !== false; + const isFormData = override?.formData !== false; + const isFormUrlEncoded = override?.formUrlEncoded !== false; + + const bodyForm = generateFormDataAndUrlEncodedFunction({ + formData, + formUrlEncoded, + body, + isFormData, + isFormUrlEncoded, + }); + + const args = `${toObjectString(props, 'implementation')} ${isRequestOptions ? `options?: RequestInit` : ''}`; + const retrunType = `Promise<${response.definition.success || 'unknown'}>`; + + const globalFetchOptions = isObject(override?.requestOptions) + ? `${stringify(override?.requestOptions)?.slice(1, -1)?.trim()}` + : ''; + const fetchMethodOption = `method: '${verb.toUpperCase()}'`; + + const requestBodyParams = generateBodyOptions( + body, + isFormData, + isFormUrlEncoded, + ); + const mergeRequestBodyImplementation = + requestBodyParams && queryParams + ? `const body = {...${requestBodyParams} ...params}` + : ''; + + let fetchBodyOption = ''; + if (requestBodyParams && queryParams) { + fetchBodyOption = 'body: JSON.stringify(body)'; + } else if (requestBodyParams) { + fetchBodyOption = `body: JSON.stringify(${requestBodyParams})`; + } else if (queryParams) { + fetchBodyOption = `body: JSON.stringify(params)`; + } + + const fetchResponseImplementation = `const res = await fetch( + \`${route}\`, + {${globalFetchOptions ? '\n' : ''} ${globalFetchOptions} + ${isRequestOptions ? '...options,' : ''} + ${fetchMethodOption}${fetchBodyOption ? ',' : ''} + ${fetchBodyOption} + } + ) + + return res.json() +`; + + const implementationBody = + `${bodyForm ? ` ${bodyForm}\n` : ''}` + + `${mergeRequestBodyImplementation ? ` ${mergeRequestBodyImplementation}\n` : ''}` + + ` ${fetchResponseImplementation}`; + + return `export const ${operationName} = async (${args}): ${retrunType} => {\n${implementationBody}}`; +}; + +export const generateClient: ClientBuilder = (verbOptions, options) => { + const imports = generateVerbImports(verbOptions); + const functionImplementation = generateRequestFunction(verbOptions, options); + + return { + implementation: `${functionImplementation}\n`, + imports, + }; +}; + +const fetchClientBuilder: ClientGeneratorsBuilder = { + client: generateClient, + dependencies: getDependencies, +}; + +export const builder = () => () => fetchClientBuilder; + +export default builder; diff --git a/packages/fetch/tsconfig.json b/packages/fetch/tsconfig.json new file mode 100644 index 000000000..9e25e6ece --- /dev/null +++ b/packages/fetch/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src/**/*.ts"] +} diff --git a/packages/orval/package.json b/packages/orval/package.json index 13896429a..dcf698f6b 100644 --- a/packages/orval/package.json +++ b/packages/orval/package.json @@ -63,6 +63,7 @@ "@orval/query": "6.29.1", "@orval/swr": "6.29.1", "@orval/zod": "6.29.1", + "@orval/fetch": "6.28.2", "ajv": "^8.12.0", "cac": "^6.7.14", "chalk": "^4.1.2", diff --git a/packages/orval/src/client.ts b/packages/orval/src/client.ts index 0c8ac506d..ce5ee7e1c 100644 --- a/packages/orval/src/client.ts +++ b/packages/orval/src/client.ts @@ -25,6 +25,7 @@ import query from '@orval/query'; import swr from '@orval/swr'; import zod from '@orval/zod'; import hono from '@orval/hono'; +import fetchClient from '@orval/fetch'; const DEFAULT_CLIENT = OutputClient.AXIOS; @@ -42,6 +43,7 @@ const getGeneratorClient = ( swr: swr()(), zod: zod()(), hono: hono()(), + fetch: fetchClient()(), }; const generator = isFunction(outputClient) From 73b459fe1c6243f750d06c9b323c41416e583dd5 Mon Sep 17 00:00:00 2001 From: soartec-lab Date: Fri, 3 May 2024 04:47:11 +0000 Subject: [PATCH 2/9] chore: add `fetch` tests --- tests/configs/fetch.config.ts | 75 +++++++++++++++++++++++++++++++++++ tests/package.json | 1 + 2 files changed, 76 insertions(+) create mode 100644 tests/configs/fetch.config.ts diff --git a/tests/configs/fetch.config.ts b/tests/configs/fetch.config.ts new file mode 100644 index 000000000..cd18f75e0 --- /dev/null +++ b/tests/configs/fetch.config.ts @@ -0,0 +1,75 @@ +import { defineConfig } from 'orval'; + +export default defineConfig({ + petstore: { + output: { + target: '../generated/fetch/petstore/endpoints.ts', + schemas: '../generated/fetch/petstore/model', + mock: true, + client: 'axios', + }, + input: { + target: '../specifications/petstore.yaml', + }, + }, + multiArguments: { + output: { + target: '../generated/fetch/multi-arguments/endpoints.ts', + schemas: '../generated/fetch/multi-arguments/model', + mock: true, + client: 'fetch', + }, + input: { + target: '../specifications/petstore.yaml', + }, + }, + petstoreTagsSplit: { + output: { + target: '../generated/fetch/petstore-tags-split/endpoints.ts', + schemas: '../generated/fetch/petstore-tags-split/model', + mock: true, + mode: 'tags-split', + client: 'fetch', + }, + input: { + target: '../specifications/petstore.yaml', + }, + }, + petstoreSplit: { + output: { + target: '../generated/fetch/split/endpoints.ts', + schemas: '../generated/fetch/split/model', + mock: true, + mode: 'split', + client: 'fetch', + }, + input: { + target: '../specifications/petstore.yaml', + }, + }, + petstoreTags: { + output: { + target: '../generated/fetch/tags/endpoints.ts', + schemas: '../generated/fetch/tags/model', + mock: true, + mode: 'tags', + client: 'fetch', + }, + input: { + target: '../specifications/petstore.yaml', + }, + }, + namedParameters: { + output: { + target: '../generated/fetch/named-parameters/endpoints.ts', + schemas: '../generated/fetch/named-parameters/model', + client: 'fetch', + override: { + useNamedParameters: true, + }, + }, + input: { + target: '../specifications/petstore.yaml', + }, + }, +}); diff --git a/tests/package.json b/tests/package.json index 6b7eca718..07d0b1cd4 100644 --- a/tests/package.json +++ b/tests/package.json @@ -17,6 +17,7 @@ "generate:multi-file": "yarn orval --config ./configs/multi-file.config.ts", "generate:zod": "yarn orval --config ./configs/zod.config.ts", "generate:mock": "yarn orval --config ./configs/mock.config.ts", + "generate:fetch": "yarn orval --config ./configs/fetch.config.ts", "build": "tsc" }, "author": "Victor Bury", From 710df83acccfee9ca7b1862e03243d2f101ce83a Mon Sep 17 00:00:00 2001 From: soartec-lab Date: Fri, 3 May 2024 04:50:35 +0000 Subject: [PATCH 3/9] docs: add `fetch` in valid values of `client` option on guide --- docs/src/pages/reference/configuration/output.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/pages/reference/configuration/output.md b/docs/src/pages/reference/configuration/output.md index d9ee72569..f6225672e 100644 --- a/docs/src/pages/reference/configuration/output.md +++ b/docs/src/pages/reference/configuration/output.md @@ -23,7 +23,7 @@ module.exports = { Type: `String | Function`. -Valid values: `angular`, `axios`, `axios-functions`, `react-query`, `svelte-query`, `vue-query`, `swr`, `zod`. +Valid values: `angular`, `axios`, `axios-functions`, `react-query`, `svelte-query`, `vue-query`, `swr`, `zod`, `fetch`. Default Value: `axios-functions`. From e0ac3b60a508ec2a82daaf9f6602a34bdff62c47 Mon Sep 17 00:00:00 2001 From: soartec-lab Date: Sun, 19 May 2024 00:37:21 +0000 Subject: [PATCH 4/9] chore: update `fetch` package to `6.29.1` --- packages/fetch/package.json | 4 ++-- packages/orval/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/fetch/package.json b/packages/fetch/package.json index 52189947a..247fbc0d9 100644 --- a/packages/fetch/package.json +++ b/packages/fetch/package.json @@ -1,6 +1,6 @@ { "name": "@orval/fetch", - "version": "6.28.2", + "version": "6.29.1", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -13,6 +13,6 @@ "lint": "eslint src/**/*.ts" }, "dependencies": { - "@orval/core": "6.28.2" + "@orval/core": "6.29.1" } } diff --git a/packages/orval/package.json b/packages/orval/package.json index dcf698f6b..374d285fe 100644 --- a/packages/orval/package.json +++ b/packages/orval/package.json @@ -63,7 +63,7 @@ "@orval/query": "6.29.1", "@orval/swr": "6.29.1", "@orval/zod": "6.29.1", - "@orval/fetch": "6.28.2", + "@orval/fetch": "6.29.1", "ajv": "^8.12.0", "cac": "^6.7.14", "chalk": "^4.1.2", From ee37dd9f61cf726eee0395a717cfd4010f031b0d Mon Sep 17 00:00:00 2001 From: soartec-lab Date: Sun, 19 May 2024 02:48:48 +0000 Subject: [PATCH 5/9] fix: query params join fetch url --- packages/fetch/src/index.ts | 88 ++++++++++++++++++++++++++----------- 1 file changed, 62 insertions(+), 26 deletions(-) diff --git a/packages/fetch/src/index.ts b/packages/fetch/src/index.ts index efdbc9105..3d6549d00 100644 --- a/packages/fetch/src/index.ts +++ b/packages/fetch/src/index.ts @@ -1,4 +1,5 @@ import { + camel, ClientBuilder, ClientDependenciesBuilder, ClientGeneratorsBuilder, @@ -7,6 +8,7 @@ import { GeneratorDependency, GeneratorOptions, GeneratorVerbOptions, + GetterPropType, stringify, toObjectString, generateBodyOptions, @@ -49,13 +51,47 @@ const generateRequestFunction = ( const isFormData = override?.formData !== false; const isFormUrlEncoded = override?.formUrlEncoded !== false; - const bodyForm = generateFormDataAndUrlEncodedFunction({ - formData, - formUrlEncoded, - body, - isFormData, - isFormUrlEncoded, - }); + const getUrlFnName = camel(`get-${operationName}-url`); + const getUrlFnProps = toObjectString( + props.filter( + (prop) => + prop.type === GetterPropType.PARAM || + prop.type === GetterPropType.NAMED_PATH_PARAMS || + prop.type === GetterPropType.QUERY_PARAM, + ), + 'implementation', + ); + const getUrlFnImplementation = `export const ${getUrlFnName} = (${getUrlFnProps}) => { +${ + queryParams + ? ` + const normalizedParams = new URLSearchParams(); + + Object.entries(params || {}).forEach(([key, value]) => { + if (value !== null && value !== undefined) { + normalizedParams.append(key, value.toString()); + } + });` + : '' +} + + return \`${route}${queryParams ? '?${new URLSearchParams(normalizedParams).toString()}' : ''}\` +}\n`; + const getUrlFnProperties = props + .filter( + (prop) => + prop.type === GetterPropType.PARAM || + prop.type === GetterPropType.QUERY_PARAM || + prop.type === GetterPropType.NAMED_PATH_PARAMS, + ) + .map((param) => { + if (param.type === GetterPropType.NAMED_PATH_PARAMS) { + return param.destructured; + } else { + return param.name; + } + }) + .join(','); const args = `${toObjectString(props, 'implementation')} ${isRequestOptions ? `options?: RequestInit` : ''}`; const retrunType = `Promise<${response.definition.success || 'unknown'}>`; @@ -70,22 +106,12 @@ const generateRequestFunction = ( isFormData, isFormUrlEncoded, ); - const mergeRequestBodyImplementation = - requestBodyParams && queryParams - ? `const body = {...${requestBodyParams} ...params}` - : ''; - - let fetchBodyOption = ''; - if (requestBodyParams && queryParams) { - fetchBodyOption = 'body: JSON.stringify(body)'; - } else if (requestBodyParams) { - fetchBodyOption = `body: JSON.stringify(${requestBodyParams})`; - } else if (queryParams) { - fetchBodyOption = `body: JSON.stringify(params)`; - } + const fetchBodyOption = requestBodyParams + ? `body: JSON.stringify(${requestBodyParams})` + : ''; const fetchResponseImplementation = `const res = await fetch( - \`${route}\`, + ${getUrlFnName}(${getUrlFnProperties}), {${globalFetchOptions ? '\n' : ''} ${globalFetchOptions} ${isRequestOptions ? '...options,' : ''} ${fetchMethodOption}${fetchBodyOption ? ',' : ''} @@ -96,12 +122,22 @@ const generateRequestFunction = ( return res.json() `; - const implementationBody = - `${bodyForm ? ` ${bodyForm}\n` : ''}` + - `${mergeRequestBodyImplementation ? ` ${mergeRequestBodyImplementation}\n` : ''}` + - ` ${fetchResponseImplementation}`; + const bodyForm = generateFormDataAndUrlEncodedFunction({ + formData, + formUrlEncoded, + body, + isFormData, + isFormUrlEncoded, + }); + + const fetchImplementationBody = + `${bodyForm ? ` ${bodyForm}\n` : ''}` + ` ${fetchResponseImplementation}`; + const fetchImplementation = `export const ${operationName} = async (${args}): ${retrunType} => {\n${fetchImplementationBody}}`; + + const implementation = + `${getUrlFnImplementation}\n` + `${fetchImplementation}\n`; - return `export const ${operationName} = async (${args}): ${retrunType} => {\n${implementationBody}}`; + return implementation; }; export const generateClient: ClientBuilder = (verbOptions, options) => { From 133392080b4689afd29bfaff3af960d99838da56 Mon Sep 17 00:00:00 2001 From: soartec-lab Date: Sun, 19 May 2024 23:49:06 +0000 Subject: [PATCH 6/9] fix: considering the case of intentional null specification --- packages/fetch/src/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/fetch/src/index.ts b/packages/fetch/src/index.ts index 3d6549d00..c41cc2fa4 100644 --- a/packages/fetch/src/index.ts +++ b/packages/fetch/src/index.ts @@ -68,7 +68,9 @@ ${ const normalizedParams = new URLSearchParams(); Object.entries(params || {}).forEach(([key, value]) => { - if (value !== null && value !== undefined) { + if (value === null) { + normalizedParams.append(key, 'null'); + } else if (value !== undefined) { normalizedParams.append(key, value.toString()); } });` From b7d51ab5e6d3247cd69e361bdf60b19812248d47 Mon Sep 17 00:00:00 2001 From: soartec-lab Date: Tue, 4 Jun 2024 23:59:51 +0000 Subject: [PATCH 7/9] fix: removed `qs` import to reduce dependent libraries --- packages/fetch/src/index.ts | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/packages/fetch/src/index.ts b/packages/fetch/src/index.ts index c41cc2fa4..26d2a7393 100644 --- a/packages/fetch/src/index.ts +++ b/packages/fetch/src/index.ts @@ -15,24 +15,6 @@ import { isObject, } from '@orval/core'; -const PARAMS_SERIALIZER_DEPENDENCIES: GeneratorDependency[] = [ - { - exports: [ - { - name: 'qs', - default: true, - values: true, - syntheticDefaultImport: true, - }, - ], - dependency: 'qs', - }, -]; - -export const getDependencies: ClientDependenciesBuilder = ( - hasParamsSerializerOptions: boolean, -) => [...(hasParamsSerializerOptions ? PARAMS_SERIALIZER_DEPENDENCIES : [])]; - const generateRequestFunction = ( { queryParams, @@ -154,7 +136,7 @@ export const generateClient: ClientBuilder = (verbOptions, options) => { const fetchClientBuilder: ClientGeneratorsBuilder = { client: generateClient, - dependencies: getDependencies, + dependencies: () => [], }; export const builder = () => () => fetchClientBuilder; From 0de4bf789972972eeb27f570266828179053b90f Mon Sep 17 00:00:00 2001 From: soartec-lab Date: Wed, 5 Jun 2024 00:05:20 +0000 Subject: [PATCH 8/9] fix: remove unnecessary object creation processes --- packages/fetch/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/fetch/src/index.ts b/packages/fetch/src/index.ts index 26d2a7393..e8aefdb04 100644 --- a/packages/fetch/src/index.ts +++ b/packages/fetch/src/index.ts @@ -59,7 +59,7 @@ ${ : '' } - return \`${route}${queryParams ? '?${new URLSearchParams(normalizedParams).toString()}' : ''}\` + return \`${route}${queryParams ? '?${normalizedParams.toString()}' : ''}\` }\n`; const getUrlFnProperties = props .filter( From ec07c15b8bc062ac645a3ba60c0384d06879d1af Mon Sep 17 00:00:00 2001 From: soartec-lab Date: Wed, 5 Jun 2024 00:12:17 +0000 Subject: [PATCH 9/9] chore: update `yarn.lock` and deps --- packages/orval/package.json | 2 +- yarn.lock | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/orval/package.json b/packages/orval/package.json index 374d285fe..1cb0bb26e 100644 --- a/packages/orval/package.json +++ b/packages/orval/package.json @@ -58,12 +58,12 @@ "@orval/angular": "6.29.1", "@orval/axios": "6.29.1", "@orval/core": "6.29.1", + "@orval/fetch": "6.29.1", "@orval/hono": "6.29.1", "@orval/mock": "6.29.1", "@orval/query": "6.29.1", "@orval/swr": "6.29.1", "@orval/zod": "6.29.1", - "@orval/fetch": "6.29.1", "ajv": "^8.12.0", "cac": "^6.7.14", "chalk": "^4.1.2", diff --git a/yarn.lock b/yarn.lock index b09cc296c..8b22e9e5c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -909,6 +909,14 @@ __metadata: languageName: unknown linkType: soft +"@orval/fetch@npm:6.29.1, @orval/fetch@workspace:packages/fetch": + version: 0.0.0-use.local + resolution: "@orval/fetch@workspace:packages/fetch" + dependencies: + "@orval/core": "npm:6.29.1" + languageName: unknown + linkType: soft + "@orval/hono@npm:6.29.1, @orval/hono@workspace:packages/hono": version: 0.0.0-use.local resolution: "@orval/hono@workspace:packages/hono" @@ -7136,6 +7144,7 @@ __metadata: "@orval/angular": "npm:6.29.1" "@orval/axios": "npm:6.29.1" "@orval/core": "npm:6.29.1" + "@orval/fetch": "npm:6.29.1" "@orval/hono": "npm:6.29.1" "@orval/mock": "npm:6.29.1" "@orval/query": "npm:6.29.1"