diff --git a/.gitignore b/.gitignore index c91577d..089b2c2 100644 --- a/.gitignore +++ b/.gitignore @@ -136,3 +136,4 @@ dist yalc.lock lib yarn.lock +test/tmp/example.ts diff --git a/README.md b/README.md index 5dc7e07..080f4e4 100644 --- a/README.md +++ b/README.md @@ -17,38 +17,124 @@ This is a utility library meant to be used with [RTK Query](https://redux-toolki ### Usage -By default, running the CLI will only log the output to the terminal. You can either pipe this output to a new file, or you can specify an output file via CLI args. +Create an empty api using `createApi` like -#### Piping to a file (including react hooks generation) +```ts +// Or from '@reduxjs/toolkit/query' if not using the auto-generated hooks +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; -```bash -npx @rtk-incubator/rtk-query-codegen-openapi --hooks https://petstore3.swagger.io/api/v3/openapi.json > petstore-api.generated.ts +// initialize an empty api service that we'll inject endpoints into later as needed +export const emptySplitApi = createApi({ + baseQuery: fetchBaseQuery({ baseUrl: '/' }), + endpoints: () => ({}), +}); ``` -#### Specifying an output file (including react hooks generation) +Generate a config file (json, js or ts) with contents like -```bash -npx @rtk-incubator/rtk-query-codegen-openapi --file petstore-api.generated.ts --hooks https://petstore3.swagger.io/api/v3/openapi.json +```ts +import { ConfigFile } from '@rtk-incubator/rtk-query-codegen-openapi'; + +const config: ConfigFile = { + schemaFile: 'https://petstore3.swagger.io/api/v3/openapi.json', + apiFile: './src/store/emptyApi.ts', + apiImport: 'emptyApi', + outputFile: './src/store/petApi.ts', + exportName: 'petApi', + hooks: true, +}; + +export default config; ``` -#### Using a custom baseQuery +and then call the code generator: ```bash -npx @rtk-incubator/rtk-query-codegen-openapi --file generated.api.ts --baseQuery ./customBaseQuery.ts:namedBaseQueryFn --hooks https://petstore3.swagger.io/api/v3/openapi.json +npx @rtk-incubator/rtk-query-codegen-openapi openapi-config.ts +``` + +### Programmatic usage + +```ts +import { generateEndpoints } from '@rtk-incubator/rtk-query-codegen-openapi'; + +const api = await generateEndpoints({ + apiFile: './fixtures/emptyApi.ts', + schemaFile: resolve(__dirname, 'fixtures/petstore.json'), + filterEndpoints: ['getPetById', 'addPet'], + hooks: true, +}); +``` + +### Config file options + +#### Simple usage + +```ts +interface SimpleUsage { + apiFile: string; + schemaFile: string; + apiImport?: string; + exportName?: string; + argSuffix?: string; + responseSuffix?: string; + hooks?: boolean; + outputFile: string; + filterEndpoints?: string | RegExp | (string | RegExp)[]; + endpointOverrides?: EndpointOverrides[]; +} +``` + +#### Filtering endpoints + +If you only want to include a few endpoints, you can use the `filterEndpoints` config option to filter your endpoints. + +```ts +const filteredConfig: ConfigFile = { + // ... + // should only have endpoints loginUser, placeOrder, getOrderById, deleteOrder + filterEndpoints: ['loginUser', /Order/], +}; ``` -### CLI Options +#### Endpoint overrides -- `--exportName ` - change the name of the exported api (default: `api`) -- `--reducerPath ` - change the name of the `reducerPath` (default: `api`) -- `--baseQuery ` - specify a file with a custom `baseQuery` function. Optionally takes a named function in that file. (default: `fetchBaseQuery` - ex: `./customBaseQuery.ts:myCustomBaseQueryFn`) -- `--argSuffix ` - change the suffix of the arg type (default: `ApiArg` - ex: `AddPetApiArg`) -- `--responseSuffix ` - change the suffix of the response type (default: `ApiResponse` - ex: `AddPetApiResponse`) -- `--baseUrl ` - set the `baseUrl` when using `fetchBaseQuery` (will be ignored if you pass `--baseQuery`) -- `--createApiImportPath ` - set the entry point to import `createApi` from. Currently only `react` is available. Defaults to `react` if `--hooks` is passed. -- `--hooks` - include React Hooks in the output (ex: `export const { useGetModelQuery, useUpdateModelMutation } = api`) -- `--file ` - specify a filename to output to (ex: `./generated.api.ts`) +If an endpoint is generated as a mutation instead of a query or the other way round, you can override that: + +```ts +const withOverride: ConfigFile = { + // ... + endpointOverrides: [ + { + pattern: 'loginUser', + type: 'mutation', + }, + ], +}; +``` + +#### Multiple output files + +```ts +const config: ConfigFile = { + schemaFile: 'https://petstore3.swagger.io/api/v3/openapi.json', + apiFile: './src/store/emptyApi.ts', + outputFiles: { + './src/store/user.js': { + filterEndpoints: [/user/i], + }, + './src/store/order.js': { + filterEndpoints: [/order/i], + }, + './src/store/pet.js': { + filterEndpoints: [/pet/i], + }, + }, +}; +``` ### Documentation [View the RTK Query Code Generation docs](https://redux-toolkit.js.org/rtk-query/usage/code-generation) + +TODO these need to be updated! diff --git a/package-lock.json b/package-lock.json index 37ddffe..c23a2d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@rtk-incubator/rtk-query-codegen-openapi", - "version": "0.3.2", + "version": "0.5.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -2029,8 +2029,7 @@ "buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", - "dev": true + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" }, "cache-base": { "version": "1.0.1", @@ -2586,6 +2585,159 @@ "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", "integrity": "sha1-oIzd6EzNvzTQJ6FFG8kdS80ophM=" }, + "esbuild": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.13.10.tgz", + "integrity": "sha512-0NfCsnAh5XatHIx6Cu93wpR2v6opPoOMxONYhaAoZKzGYqAE+INcDeX2wqMdcndvPQdWCuuCmvlnsh0zmbHcSQ==", + "dev": true, + "requires": { + "esbuild-android-arm64": "0.13.10", + "esbuild-darwin-64": "0.13.10", + "esbuild-darwin-arm64": "0.13.10", + "esbuild-freebsd-64": "0.13.10", + "esbuild-freebsd-arm64": "0.13.10", + "esbuild-linux-32": "0.13.10", + "esbuild-linux-64": "0.13.10", + "esbuild-linux-arm": "0.13.10", + "esbuild-linux-arm64": "0.13.10", + "esbuild-linux-mips64le": "0.13.10", + "esbuild-linux-ppc64le": "0.13.10", + "esbuild-netbsd-64": "0.13.10", + "esbuild-openbsd-64": "0.13.10", + "esbuild-sunos-64": "0.13.10", + "esbuild-windows-32": "0.13.10", + "esbuild-windows-64": "0.13.10", + "esbuild-windows-arm64": "0.13.10" + } + }, + "esbuild-android-arm64": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.13.10.tgz", + "integrity": "sha512-1sCdVAq64yMp2Uhlu+97/enFxpmrj31QHtThz7K+/QGjbHa7JZdBdBsZCzWJuntKHZ+EU178tHYkvjaI9z5sGg==", + "dev": true, + "optional": true + }, + "esbuild-darwin-64": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.13.10.tgz", + "integrity": "sha512-XlL+BYZ2h9cz3opHfFgSHGA+iy/mljBFIRU9q++f9SiBXEZTb4gTW/IENAD1l9oKH0FdO9rUpyAfV+lM4uAxrg==", + "dev": true, + "optional": true + }, + "esbuild-darwin-arm64": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.13.10.tgz", + "integrity": "sha512-RZMMqMTyActMrXKkW71IQO8B0tyQm0Bm+ZJQWNaHJchL5LlqazJi7rriwSocP+sKLszHhsyTEBBh6qPdw5g5yQ==", + "dev": true, + "optional": true + }, + "esbuild-freebsd-64": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.13.10.tgz", + "integrity": "sha512-pf4BEN9reF3jvZEZdxljVgOv5JS4kuYFCI78xk+2HWustbLvTP0b9XXfWI/OD0ZLWbyLYZYIA+VbVe4tdAklig==", + "dev": true, + "optional": true + }, + "esbuild-freebsd-arm64": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.13.10.tgz", + "integrity": "sha512-j9PUcuNWmlxr4/ry4dK/s6zKh42Jhh/N5qnAAj7tx3gMbkIHW0JBoVSbbgp97p88X9xgKbXx4lG2sJDhDWmsYQ==", + "dev": true, + "optional": true + }, + "esbuild-linux-32": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.13.10.tgz", + "integrity": "sha512-imtdHG5ru0xUUXuc2ofdtyw0fWlHYXV7JjF7oZHgmn0b+B4o4Nr6ZON3xxoo1IP8wIekW+7b9exIf/MYq0QV7w==", + "dev": true, + "optional": true + }, + "esbuild-linux-64": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.13.10.tgz", + "integrity": "sha512-O7fzQIH2e7GC98dvoTH0rad5BVLm9yU3cRWfEmryCEIFTwbNEWCEWOfsePuoGOHRtSwoVY1hPc21CJE4/9rWxQ==", + "dev": true, + "optional": true + }, + "esbuild-linux-arm": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.13.10.tgz", + "integrity": "sha512-R2Jij4A0K8BcmBehvQeUteQEcf24Y2YZ6mizlNFuJOBPxe3vZNmkZ4mCE7Pf1tbcqA65qZx8J3WSHeGJl9EsJA==", + "dev": true, + "optional": true + }, + "esbuild-linux-arm64": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.13.10.tgz", + "integrity": "sha512-bkGxN67S2n0PF4zhh87/92kBTsH2xXLuH6T5omReKhpXdJZF5SVDSk5XU/nngARzE+e6QK6isK060Dr5uobzNw==", + "dev": true, + "optional": true + }, + "esbuild-linux-mips64le": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.13.10.tgz", + "integrity": "sha512-UDNO5snJYOLWrA2uOUxM/PVbzzh2TR7Zf2i8zCCuFlYgvAb/81XO+Tasp3YAElDpp4VGqqcpBXLtofa9nrnJGA==", + "dev": true, + "optional": true + }, + "esbuild-linux-ppc64le": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.13.10.tgz", + "integrity": "sha512-xu6J9rMWu1TcEGuEmoc8gsTrJCEPsf+QtxK4IiUZNde9r4Q4nlRVah4JVZP3hJapZgZJcxsse0XiKXh1UFdOeA==", + "dev": true, + "optional": true + }, + "esbuild-netbsd-64": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.13.10.tgz", + "integrity": "sha512-d+Gr0ScMC2J83Bfx/ZvJHK0UAEMncctwgjRth9d4zppYGLk/xMfFKxv5z1ib8yZpQThafq8aPm8AqmFIJrEesw==", + "dev": true, + "optional": true + }, + "esbuild-openbsd-64": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.13.10.tgz", + "integrity": "sha512-OuCYc+bNKumBvxflga+nFzZvxsgmWQW+z4rMGIjM5XIW0nNbGgRc5p/0PSDv0rTdxAmwCpV69fezal0xjrDaaA==", + "dev": true, + "optional": true + }, + "esbuild-runner": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/esbuild-runner/-/esbuild-runner-2.2.1.tgz", + "integrity": "sha512-VP0VfJJZiZ3cKzdOH59ZceDxx/GzBKra7tiGM8MfFMLv6CR1/cpsvtQ3IsJI3pz7HyeYxtbPyecj3fHwR+3XcQ==", + "requires": { + "source-map-support": "0.5.19", + "tslib": "2.3.1" + } + }, + "esbuild-sunos-64": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.13.10.tgz", + "integrity": "sha512-gUkgivZK11bD56wDoLsnYrsOHD/zHzzLSdqKcIl3wRMulfHpRBpoX8gL0dbWr+8N9c+1HDdbNdvxSRmZ4RCVwg==", + "dev": true, + "optional": true + }, + "esbuild-windows-32": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.13.10.tgz", + "integrity": "sha512-C1xJ54E56dGWRaYcTnRy7amVZ9n1/D/D2/qVw7e5EtS7p+Fv/yZxxgqyb1hMGKXgtFYX4jMpU5eWBF/AsYrn+A==", + "dev": true, + "optional": true + }, + "esbuild-windows-64": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.13.10.tgz", + "integrity": "sha512-6+EXEXopEs3SvPFAHcps2Krp/FvqXXsOQV33cInmyilb0ZBEQew4MIoZtMIyB3YXoV6//dl3i6YbPrFZaWEinQ==", + "dev": true, + "optional": true + }, + "esbuild-windows-arm64": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.13.10.tgz", + "integrity": "sha512-xTqM/XKhORo6u9S5I0dNJWEdWoemFjogLUTVLkQMVyUV3ZuMChahVA+bCqKHdyX55pCFxD/8v2fm3/sfFMWN+g==", + "dev": true, + "optional": true + }, "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -6159,8 +6311,7 @@ "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" }, "source-map-resolve": { "version": "0.5.3", @@ -6179,7 +6330,6 @@ "version": "0.5.19", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", - "dev": true, "requires": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -6552,6 +6702,11 @@ "yn": "3.1.1" } }, + "tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" + }, "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", diff --git a/package.json b/package.json index b6ae59a..1960e16 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "prepare": "npm run build && chmod +x ./lib/bin/cli.js", "format": "prettier --write \"src/**/*.ts\"", "test:update": "lib/bin/cli.js test/fixtures/petstore.json --file test/fixtures/generated.ts -h", - "test": "jest" + "test": "jest", + "cli": "esr src/bin/cli.ts" }, "files": [ "lib", @@ -34,18 +35,20 @@ "babel-jest": "^26.6.3", "chalk": "^4.1.0", "del": "^6.0.0", + "esbuild": "^0.13.10", "husky": "^4.3.6", "jest": "^26.6.3", "msw": "^0.25.0", "openapi-types": "^9.1.0", "pretty-quick": "^3.1.0", "ts-jest": "^26.4.4", - "ts-node": "^9.1.0", + "ts-node": "^10.4.0", "yalc": "^1.0.0-pre.47" }, "dependencies": { "@apidevtools/swagger-parser": "^10.0.2", "commander": "^6.2.0", + "esbuild-runner": "^2.2.1", "glob-to-regexp": "^0.4.1", "oazapfts": "3.4.0", "prettier": "^2.2.1", diff --git a/src/bin/cli.ts b/src/bin/cli.ts index 8247152..c3a2bc5 100644 --- a/src/bin/cli.ts +++ b/src/bin/cli.ts @@ -1,88 +1,57 @@ #!/usr/bin/env node -import * as path from 'path'; -import * as fs from 'fs'; import program from 'commander'; +import { dirname, resolve } from 'path'; +import { generateEndpoints, parseConfig } from '../'; -// tslint:disable-next-line -const meta = require('../../package.json'); -import { generateApi } from '../generate'; -import { GenerationOptions } from '../types'; -import { isValidUrl, MESSAGES, prettify } from '../utils'; -import { getCompilerOptions } from '../utils/getTsConfig'; +let ts = false; +try { + if (require.resolve('esbuild') && require.resolve('esbuild-runner')) { + require('esbuild-runner/register'); + } + ts = true; +} catch {} -program - .version(meta.version) - .usage('') - .option('--exportName ', 'change RTK Query Tree root name') - .option('--reducerPath ', 'pass reducer path') - .option('--baseQuery ', 'pass baseQuery name') - .option('--argSuffix ', 'pass arg suffix') - .option('--responseSuffix ', 'pass response suffix') - .option('--baseUrl ', 'pass baseUrl') - .option('--createApiImportPath ', 'entry point for createApi import. options: [react]') - .option('-h, --hooks', 'generate React Hooks') - .option('-c, --config ', 'pass tsconfig path for resolve path alias') - .option('-f, --file ', 'output file name (ex: generated.api.ts)') - .parse(process.argv); +try { + if (!ts) { + if (require.resolve('typescript') && require.resolve('ts-node')) { + require('ts-node/register/transpile-only'); + } -if (program.args.length === 0) { - program.help(); -} else { - const schemaLocation = program.args[0]; + ts = true; + } +} catch {} - const schemaAbsPath = isValidUrl(schemaLocation) ? schemaLocation : path.resolve(process.cwd(), schemaLocation); +// tslint:disable-next-line +const meta = require('../../package.json'); - const options = [ - 'exportName', - 'reducerPath', - 'baseQuery', - 'argSuffix', - 'responseSuffix', - 'baseUrl', - 'createApiImportPath', - 'hooks', - 'file', - 'config', - ] as const; +program.version(meta.version).usage('').parse(process.argv); - const outputFile = program['file']; - let tsConfigFilePath = program['config']; +const configFile = program.args[0]; - if (tsConfigFilePath) { - tsConfigFilePath = path.resolve(tsConfigFilePath); - if (!fs.existsSync(tsConfigFilePath)) { - throw Error(MESSAGES.TSCONFIG_FILE_NOT_FOUND); - } +if (program.args.length === 0 || !/\.(jsx?|tsx?|jsonc?)?$/.test(configFile)) { + program.help(); +} else { + if (/\.tsx?$/.test(configFile) && !ts) { + console.error('Encountered a TypeScript configfile, but neither esbuild-runner nor ts-node are installed.'); + process.exit(1); } + run(resolve(process.cwd(), configFile)); +} + +async function run(configFile: string) { + process.chdir(dirname(configFile)); - const compilerOptions = getCompilerOptions(tsConfigFilePath); + const unparsedConfig = require(configFile); - const generateApiOptions = { - ...options.reduce( - (s, key) => - program[key] - ? { - ...s, - [key]: program[key], - } - : s, - {} as GenerationOptions - ), - outputFile, - compilerOptions, - }; - generateApi(schemaAbsPath, generateApiOptions) - .then(async (sourceCode) => { - const outputFile = program['file']; - if (outputFile) { - fs.writeFileSync(path.resolve(process.cwd(), outputFile), await prettify(outputFile, sourceCode)); - } else { - console.log(await prettify(null, sourceCode)); - } - }) - .catch((err) => { + for (const config of parseConfig(unparsedConfig.default ?? unparsedConfig)) { + try { + console.log(`Generating ${config.outputFile}`); + await generateEndpoints(config); + console.log(`Done`); + } catch (err) { console.error(err); process.exit(1); - }); + } + } } diff --git a/src/codegen.ts b/src/codegen.ts index 915666a..feeb714 100644 --- a/src/codegen.ts +++ b/src/codegen.ts @@ -9,13 +9,6 @@ export function generateObjectProperties(obj: ObjectPropertyDefinitions) { return Object.entries(obj).map(([k, v]) => factory.createPropertyAssignment(factory.createIdentifier(k), v)); } -export function generateStringLiteralArray(arr: string[]) { - return factory.createArrayLiteralExpression( - arr.map((elem) => factory.createStringLiteral(elem)), - false - ); -} - export function generateImportNode(pkg: string, namedImports: Record, defaultImportName?: string) { return factory.createImportDeclaration( undefined, @@ -38,57 +31,54 @@ export function generateImportNode(pkg: string, namedImports: Record 0; - - const basePackageImportNode = hasBasePackageImports - ? [generateImportNode(DEFAULT_IMPORT_PATH, getBasePackageImportsFromOptions())] - : []; - - const subPackageImportNode = - entryPoint !== 'base' - ? [generateImportNode(`${DEFAULT_IMPORT_PATH}/${entryPoint}`, { createApi: 'createApi' })] - : []; - - return [...subPackageImportNode, ...basePackageImportNode]; -} diff --git a/src/generate.ts b/src/generate.ts index 32eb19d..2b66e79 100644 --- a/src/generate.ts +++ b/src/generate.ts @@ -1,8 +1,8 @@ import * as ts from 'typescript'; import * as path from 'path'; -import { camelCase } from 'lodash'; +import { camelCase, filter } from 'lodash'; import ApiGenerator, { - getOperationName, + getOperationName as _getOperationName, getReferenceName, isReference, supportDeepObjects, @@ -15,16 +15,21 @@ import { } from 'oazapfts/lib/codegen/tscodegen'; import { OpenAPIV3 } from 'openapi-types'; import { generateReactHooks } from './generators/react-hooks'; -import { GenerationOptions, OperationDefinition } from './types'; -import { capitalize, getOperationDefinitions, getV3Doc, isQuery, MESSAGES, removeUndefined } from './utils'; +import { EndpointOverrides, GenerationOptions, OperationDefinition, OutputFileOptions } from './types'; +import { + capitalize, + getOperationDefinitions, + getV3Doc, + isQuery as testIsQuery, + MESSAGES, + removeUndefined, +} from './utils'; import { generateCreateApiCall, generateEndpointDefinition, - generateStringLiteralArray, - generatePackageImports, ObjectPropertyDefinitions, + generateImportNode, } from './codegen'; -import { generateSmartImportNode } from './generators/smart-import-node'; const { factory } = ts; @@ -33,35 +38,48 @@ function defaultIsDataResponse(code: string) { return !Number.isNaN(parsedCode) && parsedCode >= 200 && parsedCode < 300; } -let customBaseQueryNode: ts.ImportDeclaration | undefined; -let moduleName: string; +function getOperationName({ verb, path, operation }: Pick) { + return _getOperationName(verb, path, operation.operationId); +} + +function patternMatches(pattern?: string | RegExp | (string | RegExp)[]) { + const filters = Array.isArray(pattern) ? pattern : [pattern]; + return function matcher(operationDefinition: OperationDefinition) { + if (!pattern) return true; + const operationName = getOperationName(operationDefinition); + return filters.some((filter) => + typeof filter === 'string' ? filter == operationName : filter?.test(operationName) + ); + }; +} + +export function getOverrides( + operation: OperationDefinition, + endpointOverrides?: EndpointOverrides[] +): EndpointOverrides | undefined { + return endpointOverrides?.find((override) => patternMatches(override.pattern)(operation)); +} export async function generateApi( spec: string, { - exportName = 'api', - reducerPath, - baseQuery = 'fetchBaseQuery', + apiFile, + apiImport = 'api', + exportName = 'enhancedApi', argSuffix = 'ApiArg', responseSuffix = 'ApiResponse', - createApiImportPath = 'base', - baseUrl, - hooks, + hooks = false, outputFile, isDataResponse = defaultIsDataResponse, - compilerOptions, + filterEndpoints, + endpointOverrides, }: GenerationOptions ) { const v3Doc = await getV3Doc(spec); - if (typeof baseUrl !== 'string') { - baseUrl = v3Doc.servers?.[0].url ?? 'https://example.com'; - } else if (baseQuery !== 'fetchBaseQuery') { - console.warn(MESSAGES.BASE_URL_IGNORED); - } const apiGen = new ApiGenerator(v3Doc, {}); - const operationDefinitions = getOperationDefinitions(v3Doc); + const operationDefinitions = getOperationDefinitions(v3Doc).filter(patternMatches(filterEndpoints)); const resultFile = ts.createSourceFile( 'someFileName.ts', @@ -82,76 +100,41 @@ export async function generateApi( return declaration; } - /** - * --baseQuery handling - * 1. If baseQuery is specified, we confirm that the file exists - * 2. If there is a seperator in the path, file presence + named function existence is verified. - * 3. If there is a not a seperator, file presence + default export existence is verified. - */ - - if (outputFile) { + if (outputFile && outputFile !== '-') { outputFile = path.resolve(process.cwd(), outputFile); + apiFile = path.relative(path.dirname(outputFile), apiFile); } - - // If a baseQuery was specified as an arg, we try to parse and resolve it. If not, fallback to `fetchBaseQuery` or throw when appropriate. - - let targetName = 'default'; - if (baseQuery !== 'fetchBaseQuery') { - if (baseQuery.includes(':')) { - // User specified a named function - [moduleName, baseQuery] = baseQuery.split(':'); - - if (!baseQuery) { - throw new Error(MESSAGES.NAMED_EXPORT_MISSING); - } - targetName = baseQuery; - } else { - moduleName = baseQuery; - baseQuery = 'customBaseQuery'; - } - - customBaseQueryNode = generateSmartImportNode({ - moduleName, - containingFile: outputFile, - targetName, - targetAlias: baseQuery, - compilerOptions, - }); - } - - const fetchBaseQueryCall = factory.createCallExpression(factory.createIdentifier('fetchBaseQuery'), undefined, [ - factory.createObjectLiteralExpression( - [factory.createPropertyAssignment(factory.createIdentifier('baseUrl'), factory.createStringLiteral(baseUrl))], - false - ), - ]); - - const isUsingFetchBaseQuery = baseQuery === 'fetchBaseQuery'; + apiFile = apiFile.replace(/\.[jt]sx?$/, ''); const sourceCode = printer.printNode( ts.EmitHint.Unspecified, factory.createSourceFile( [ - ...generatePackageImports({ hooks, isUsingFetchBaseQuery, createApiImportPath }), - ...(customBaseQueryNode ? [customBaseQueryNode] : []), + generateImportNode(apiFile, { api: apiImport }), generateCreateApiCall({ exportName, - reducerPath, - createApiFn: factory.createIdentifier('createApi'), - baseQuery: isUsingFetchBaseQuery ? fetchBaseQueryCall : factory.createIdentifier(baseQuery), - tagTypes: generateTagTypes({ v3Doc, operationDefinitions }), endpointDefinitions: factory.createObjectLiteralExpression( operationDefinitions.map((operationDefinition) => generateEndpoint({ operationDefinition, + overrides: getOverrides(operationDefinition, endpointOverrides), }) ), true ), }), + factory.createExportDeclaration( + undefined, + undefined, + false, + factory.createNamedExports([ + factory.createExportSpecifier(factory.createIdentifier('injectedRtkApi'), factory.createIdentifier('api')), + ]), + undefined + ), ...Object.values(interfaces), ...apiGen['aliases'], - ...(hooks ? [generateReactHooks({ exportName, operationDefinitions })] : []), + ...(hooks ? [generateReactHooks({ exportName, operationDefinitions, endpointOverrides })] : []), ], factory.createToken(ts.SyntaxKind.EndOfFileToken), ts.NodeFlags.None @@ -161,11 +144,13 @@ export async function generateApi( return sourceCode; - function generateTagTypes(_: { operationDefinitions: OperationDefinition[]; v3Doc: OpenAPIV3.Document }) { - return generateStringLiteralArray([]); // TODO - } - - function generateEndpoint({ operationDefinition }: { operationDefinition: OperationDefinition }) { + function generateEndpoint({ + operationDefinition, + overrides, + }: { + operationDefinition: OperationDefinition; + overrides?: EndpointOverrides; + }) { const { verb, path, @@ -173,8 +158,9 @@ export async function generateApi( operation, operation: { responses, requestBody }, } = operationDefinition; + const operationName = getOperationName({ verb, path, operation }); - const _isQuery = isQuery(verb); + const isQuery = testIsQuery(verb, overrides); const returnsJson = apiGen.getResponseType(responses) === 'json'; let ResponseType: ts.TypeNode = factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword); @@ -208,7 +194,7 @@ export async function generateApi( factory.createTypeAliasDeclaration( undefined, [factory.createModifier(ts.SyntaxKind.ExportKeyword)], - capitalize(getOperationName(verb, path, operation.operationId) + responseSuffix), + capitalize(operationName + responseSuffix), undefined, ResponseType ) @@ -273,7 +259,7 @@ export async function generateApi( factory.createTypeAliasDeclaration( undefined, [factory.createModifier(ts.SyntaxKind.ExportKeyword)], - capitalize(getOperationName(verb, path, operation.operationId) + argSuffix), + capitalize(operationName + argSuffix), undefined, queryArgValues.length > 0 ? factory.createTypeLiteralNode( @@ -303,12 +289,12 @@ export async function generateApi( ); return generateEndpointDefinition({ - operationName: getOperationName(verb, path, operation.operationId), - type: _isQuery ? 'query' : 'mutation', + operationName, + type: isQuery ? 'query' : 'mutation', Response: ResponseTypeName, QueryArg, - queryFn: generateQueryFn({ operationDefinition, queryArg }), - extraEndpointsProps: _isQuery + queryFn: generateQueryFn({ operationDefinition, queryArg, isQuery }), + extraEndpointsProps: isQuery ? generateQueryEndpointProps({ operationDefinition }) : generateMutationEndpointProps({ operationDefinition }), }); @@ -317,9 +303,11 @@ export async function generateApi( function generateQueryFn({ operationDefinition, queryArg, + isQuery, }: { operationDefinition: OperationDefinition; queryArg: QueryArgDefinitions; + isQuery: boolean; }) { const { path, verb } = operationDefinition; @@ -360,7 +348,7 @@ export async function generateApi( factory.createIdentifier('url'), generatePathExpression(path, pathParameters, rootObject) ), - isQuery(verb) + isQuery ? undefined : factory.createPropertyAssignment( factory.createIdentifier('method'), diff --git a/src/generators/import-node.ts b/src/generators/import-node.ts deleted file mode 100644 index 2bc8057..0000000 --- a/src/generators/import-node.ts +++ /dev/null @@ -1,25 +0,0 @@ -import * as ts from 'typescript'; - -const { factory } = ts; - -export function generateImportNode(pkg: string, namedImports: Record, defaultImportName?: string) { - return factory.createImportDeclaration( - undefined, - undefined, - factory.createImportClause( - false, - defaultImportName !== undefined ? factory.createIdentifier(defaultImportName) : undefined, - factory.createNamedImports( - Object.entries(namedImports) - .filter((args) => args[1]) - .map(([propertyName, name]) => - factory.createImportSpecifier( - name === propertyName ? undefined : factory.createIdentifier(propertyName), - factory.createIdentifier(name as string) - ) - ) - ) - ), - factory.createStringLiteral(pkg) - ); -} diff --git a/src/generators/react-hooks.ts b/src/generators/react-hooks.ts index a72fb1f..55a7c63 100644 --- a/src/generators/react-hooks.ts +++ b/src/generators/react-hooks.ts @@ -1,36 +1,50 @@ import * as ts from 'typescript'; import { getOperationName } from 'oazapfts/lib/codegen/generate'; import { capitalize, isQuery } from '../utils'; -import { OperationDefinition } from '../types'; +import { OperationDefinition, EndpointOverrides } from '../types'; +import { getOverrides } from '../generate'; const { factory } = ts; type GetReactHookNameParams = { operationDefinition: OperationDefinition; + endpointOverrides: EndpointOverrides[] | undefined; }; -const getReactHookName = ({ operationDefinition: { verb, path, operation } }: GetReactHookNameParams) => - factory.createBindingElement( +const getReactHookName = ({ + operationDefinition: { verb, path, operation }, + operationDefinition, + endpointOverrides, +}: GetReactHookNameParams) => { + const overrides = getOverrides(operationDefinition, endpointOverrides); + + return factory.createBindingElement( undefined, undefined, factory.createIdentifier( - `use${capitalize(getOperationName(verb, path, operation.operationId))}${isQuery(verb) ? 'Query' : 'Mutation'}` + `use${capitalize(getOperationName(verb, path, operation.operationId))}${ + isQuery(verb, overrides) ? 'Query' : 'Mutation' + }` ), undefined ); +}; type GenerateReactHooksParams = { exportName: string; operationDefinitions: OperationDefinition[]; + endpointOverrides: EndpointOverrides[] | undefined; }; -export const generateReactHooks = ({ exportName, operationDefinitions }: GenerateReactHooksParams) => +export const generateReactHooks = ({ exportName, operationDefinitions, endpointOverrides }: GenerateReactHooksParams) => factory.createVariableStatement( [factory.createModifier(ts.SyntaxKind.ExportKeyword)], factory.createVariableDeclarationList( [ factory.createVariableDeclaration( factory.createObjectBindingPattern( - operationDefinitions.map((operationDefinition) => getReactHookName({ operationDefinition })) + operationDefinitions.map((operationDefinition) => + getReactHookName({ operationDefinition, endpointOverrides }) + ) ), undefined, undefined, diff --git a/src/generators/smart-import-node.ts b/src/generators/smart-import-node.ts deleted file mode 100644 index 21b4d70..0000000 --- a/src/generators/smart-import-node.ts +++ /dev/null @@ -1,52 +0,0 @@ -import * as ts from 'typescript'; -import * as fs from 'fs'; - -import { MESSAGES, stripFileExtension } from '../utils'; -import { isModuleInsidePathAlias } from '../utils/isModuleInsidePathAlias'; -import { generateImportNode } from './import-node'; -import { fnExportExists } from '../utils/fnExportExists'; -import { resolveImportPath } from '../utils/resolveImportPath'; - -type SmartGenerateImportNode = { - moduleName: string; - containingFile?: string; - targetName: string; - targetAlias: string; - compilerOptions?: ts.CompilerOptions; -}; -export const generateSmartImportNode = ({ - moduleName, - containingFile, - targetName, - targetAlias, - compilerOptions, -}: SmartGenerateImportNode): ts.ImportDeclaration => { - if (fs.existsSync(moduleName)) { - if (fnExportExists(moduleName, targetName)) { - return generateImportNode( - stripFileExtension(containingFile ? resolveImportPath(moduleName, containingFile) : moduleName), - { - [targetName]: targetAlias, - } - ); - } - - if (targetName === 'default') { - throw new Error(MESSAGES.DEFAULT_EXPORT_MISSING); - } - throw new Error(MESSAGES.NAMED_EXPORT_MISSING); - } - - if (!compilerOptions) { - throw new Error(MESSAGES.FILE_NOT_FOUND); - } - - // maybe moduleName is path alias - if (isModuleInsidePathAlias(compilerOptions, moduleName)) { - return generateImportNode(stripFileExtension(moduleName), { - [targetName]: targetAlias, - }); - } - - throw new Error(MESSAGES.FILE_NOT_FOUND); -}; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..0400093 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,40 @@ +import path from 'path'; +import fs from 'fs'; +import { CommonOptions, ConfigFile, GenerationOptions, OutputFileOptions } from './types'; +import { generateApi } from './generate'; +import { isValidUrl, prettify } from './utils'; +export { ConfigFile } from './types'; + +export async function generateEndpoints(options: GenerationOptions): Promise { + const schemaLocation = options.schemaFile; + + const schemaAbsPath = isValidUrl(options.schemaFile) + ? options.schemaFile + : path.resolve(process.cwd(), schemaLocation); + + const sourceCode = await generateApi(schemaAbsPath, options); + const outputFile = options.outputFile; + if (outputFile) { + fs.writeFileSync(path.resolve(process.cwd(), outputFile), await prettify(outputFile, sourceCode)); + } else { + return await prettify(null, sourceCode); + } +} + +export function parseConfig(fullConfig: ConfigFile) { + const outFiles: (CommonOptions & OutputFileOptions)[] = []; + + if ('outputFiles' in fullConfig) { + const { outputFiles, ...commonConfig } = fullConfig; + for (const [outputFile, specificConfig] of Object.entries(outputFiles)) { + outFiles.push({ + ...commonConfig, + ...specificConfig, + outputFile, + }); + } + } else { + outFiles.push(fullConfig); + } + return outFiles; +} diff --git a/src/types.ts b/src/types.ts index 1e91d03..cfe28b2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -8,18 +8,66 @@ export type OperationDefinition = { operation: OpenAPIV3.OperationObject; }; +type Require = { [k in K]-?: NonNullable } & Omit; +type Optional = { [k in K]?: NonNullable } & Omit; +type Id = { [K in keyof T]: T[K] } & {}; + export const operationKeys = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'] as const; -export type GenerationOptions = { +export type GenerationOptions = Id< + CommonOptions & + Optional & { + isDataResponse?( + code: string, + response: OpenAPIV3.ResponseObject, + allResponses: OpenAPIV3.ResponsesObject + ): boolean; + } +>; + +export interface CommonOptions { + apiFile: string; + /** + * filename or url + */ + schemaFile: string; + /** + * defaults to "api" + */ + apiImport?: string; + /** + * defaults to "enhancedApi" + */ exportName?: string; - reducerPath?: string; - baseQuery?: string; + /** + * defaults to "ApiArg" + */ argSuffix?: string; + /** + * defaults to "ApiResponse" + */ responseSuffix?: string; - baseUrl?: string; - createApiImportPath?: 'base' | 'react'; + /** + * defaults to false + */ hooks?: boolean; - outputFile?: string; - compilerOptions?: ts.CompilerOptions; - isDataResponse?(code: string, response: OpenAPIV3.ResponseObject, allResponses: OpenAPIV3.ResponsesObject): boolean; -}; +} + +export interface OutputFileOptions extends Partial { + outputFile: string; + filterEndpoints?: string | RegExp | (string | RegExp)[]; + endpointOverrides?: EndpointOverrides[]; +} + +export interface EndpointOverrides { + pattern: string | RegExp | (string | RegExp)[]; + type: 'mutation' | 'query'; +} + +export type ConfigFile = + | Id> + | Id< + Omit & { + outputFiles: { [outputFile: string]: Omit }; + } + >; diff --git a/src/utils/fnExportExists.ts b/src/utils/fnExportExists.ts deleted file mode 100644 index 0925980..0000000 --- a/src/utils/fnExportExists.ts +++ /dev/null @@ -1,35 +0,0 @@ -import * as ts from 'typescript'; -import * as fs from 'fs'; -import * as path from 'path'; - -export function fnExportExists(filePath: string, fnName: string) { - const fileName = path.resolve(process.cwd(), filePath); - - const sourceFile = ts.createSourceFile( - fileName, - fs.readFileSync(fileName).toString(), - ts.ScriptTarget.ES2015, - /*setParentNodes */ true - ); - - let found = false; - - ts.forEachChild(sourceFile, (node) => { - const text = node.getText(); - if (ts.isExportAssignment(node)) { - if (text.includes(fnName)) { - found = true; - } - } else if (ts.isVariableStatement(node) || ts.isFunctionDeclaration(node) || ts.isExportDeclaration(node)) { - if (text.includes(fnName) && text.includes('export')) { - found = true; - } - } else if (ts.isExportAssignment(node)) { - if (text.includes(`export ${fnName}`)) { - found = true; - } - } - }); - - return found; -} diff --git a/src/utils/getTsConfig.ts b/src/utils/getTsConfig.ts deleted file mode 100644 index 8b0ebba..0000000 --- a/src/utils/getTsConfig.ts +++ /dev/null @@ -1,49 +0,0 @@ -import * as path from 'path'; -import * as fs from 'fs'; -import * as ts from 'typescript'; - -function readConfig(configPath: string): ts.ParsedCommandLine | undefined { - const result = ts.readConfigFile(configPath, ts.sys.readFile); - - if (result.error) { - return undefined; - } - - return ts.parseJsonConfigFileContent(result.config, ts.sys, path.dirname(configPath), undefined, configPath); -} - -function findConfig(baseDir: string): string | undefined { - const configFileName = 'tsconfig.json'; - - function loop(dir: string): string | undefined { - const parentPath = path.dirname(dir); - // It is root directory if parent and current dirname are the same - if (dir === parentPath) { - return undefined; - } - - const configPath = path.join(dir, configFileName); - if (fs.existsSync(configPath)) { - return configPath; - } - - return loop(parentPath); - } - return loop(baseDir); -} - -export function getCompilerOptions(configPath?: string): ts.CompilerOptions | undefined { - if (!configPath) { - configPath = findConfig(process.cwd()); - } - - if (!configPath) { - return; - } - - const config = readConfig(configPath); - - if (config) { - return config.options; - } -} diff --git a/src/utils/index.ts b/src/utils/index.ts index 968146e..c7d39e0 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -5,5 +5,4 @@ export * from './isQuery'; export * from './isValidUrl'; export * from './prettier'; export * from './messages'; -export * from './stripFileExtension'; export * from './removeUndefined'; diff --git a/src/utils/isModuleInsidePathAlias.ts b/src/utils/isModuleInsidePathAlias.ts deleted file mode 100644 index b1ce59a..0000000 --- a/src/utils/isModuleInsidePathAlias.ts +++ /dev/null @@ -1,46 +0,0 @@ -import * as ts from 'typescript'; -import * as path from 'path'; -import * as fs from 'fs'; -import globToRegExp from 'glob-to-regexp'; - -function isAlias(glob: string, moduleName: string): boolean { - return globToRegExp(glob).test(moduleName); -} - -const ext = ['js', 'ts']; -function existsModule(moduleName: string) { - if (/\.(ts|js)$/.test(moduleName)) { - return fs.existsSync(moduleName); - } - for (let i = 0; i < ext.length; i++) { - if (fs.existsSync(`${moduleName}.${ext[i]}`)) { - return true; - } - } - return false; -} - -export function isModuleInsidePathAlias(options: ts.CompilerOptions, moduleName: string): boolean { - if (!(options.paths && options.baseUrl)) { - return fs.existsSync(moduleName); - } - - let baseUrl = options.baseUrl; - if (!/\/$/.test(baseUrl)) { - baseUrl = `${baseUrl}/`; - } - - for (const glob in options.paths) { - if (isAlias(glob, moduleName)) { - const before = glob.replace('*', ''); - for (let i = 0; i < options.paths[glob].length; i++) { - const after = options.paths[glob][i].replace('*', ''); - if (existsModule(path.resolve(baseUrl, after, moduleName.replace(before, '')))) { - return true; - } - } - } - } - - return false; -} diff --git a/src/utils/isQuery.ts b/src/utils/isQuery.ts index 98350e7..f73a043 100644 --- a/src/utils/isQuery.ts +++ b/src/utils/isQuery.ts @@ -1,5 +1,8 @@ -import { operationKeys } from '../types'; +import { EndpointOverrides, operationKeys } from '../types'; -export function isQuery(verb: typeof operationKeys[number]) { +export function isQuery(verb: typeof operationKeys[number], overrides: EndpointOverrides | undefined) { + if (overrides?.type) { + return overrides.type === 'query'; + } return verb === 'get'; } diff --git a/src/utils/resolveImportPath.ts b/src/utils/resolveImportPath.ts deleted file mode 100644 index 464db6c..0000000 --- a/src/utils/resolveImportPath.ts +++ /dev/null @@ -1,11 +0,0 @@ -import * as path from 'path'; -import { stripFileExtension } from './stripFileExtension'; - -export function resolveImportPath(modulePath: string, containingFile: string) { - containingFile = path.resolve(containingFile, '..'); - const strippedFile = stripFileExtension(path.relative(containingFile, modulePath)); - if (strippedFile.charAt(0) !== '.') { - return `./${strippedFile}`; - } - return strippedFile; -} diff --git a/src/utils/stripFileExtension.ts b/src/utils/stripFileExtension.ts deleted file mode 100644 index b8998dc..0000000 --- a/src/utils/stripFileExtension.ts +++ /dev/null @@ -1 +0,0 @@ -export const stripFileExtension = (path: string) => path.replace(/\.(ts|js)$/, ''); diff --git a/test/__snapshots__/cli.test.ts.snap b/test/__snapshots__/cli.test.ts.snap index cc63e25..a35ec8d 100644 --- a/test/__snapshots__/cli.test.ts.snap +++ b/test/__snapshots__/cli.test.ts.snap @@ -1,883 +1,38 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`CLI options testing should accept a valid url as the target swagger file and generate a client 1`] = ` -import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query"; -export const api = createApi({ - baseQuery: fetchBaseQuery({ baseUrl: "/api/v3" }), - tagTypes: [], - endpoints: (build) => ({ - updatePet: build.mutation({ - query: (queryArg) => ({ url: \`/pet\`, method: "PUT", body: queryArg.pet }), - }), - addPet: build.mutation({ - query: (queryArg) => ({ - url: \`/pet\`, - method: "POST", - body: queryArg.pet, - }), - }), - findPetsByStatus: build.query< - FindPetsByStatusApiResponse, - FindPetsByStatusApiArg - >({ - query: (queryArg) => ({ - url: \`/pet/findByStatus\`, - params: { status: queryArg.status }, - }), - }), - findPetsByTags: build.query< - FindPetsByTagsApiResponse, - FindPetsByTagsApiArg - >({ - query: (queryArg) => ({ - url: \`/pet/findByTags\`, - params: { tags: queryArg.tags }, - }), - }), - getPetById: build.query({ - query: (queryArg) => ({ url: \`/pet/\${queryArg.petId}\` }), - }), - updatePetWithForm: build.mutation< - UpdatePetWithFormApiResponse, - UpdatePetWithFormApiArg - >({ - query: (queryArg) => ({ - url: \`/pet/\${queryArg.petId}\`, - method: "POST", - params: { name: queryArg.name, status: queryArg.status }, - }), - }), - deletePet: build.mutation({ - query: (queryArg) => ({ - url: \`/pet/\${queryArg.petId}\`, - method: "DELETE", - headers: { api_key: queryArg.apiKey }, - }), - }), - uploadFile: build.mutation({ - query: (queryArg) => ({ - url: \`/pet/\${queryArg.petId}/uploadImage\`, - method: "POST", - body: queryArg.body, - params: { additionalMetadata: queryArg.additionalMetadata }, - }), - }), - getInventory: build.query({ - query: () => ({ url: \`/store/inventory\` }), - }), - placeOrder: build.mutation({ - query: (queryArg) => ({ - url: \`/store/order\`, - method: "POST", - body: queryArg.order, - }), - }), - getOrderById: build.query({ - query: (queryArg) => ({ url: \`/store/order/\${queryArg.orderId}\` }), - }), - deleteOrder: build.mutation({ - query: (queryArg) => ({ - url: \`/store/order/\${queryArg.orderId}\`, - method: "DELETE", - }), - }), - createUser: build.mutation({ - query: (queryArg) => ({ - url: \`/user\`, - method: "POST", - body: queryArg.user, - }), - }), - createUsersWithListInput: build.mutation< - CreateUsersWithListInputApiResponse, - CreateUsersWithListInputApiArg - >({ - query: (queryArg) => ({ - url: \`/user/createWithList\`, - method: "POST", - body: queryArg.body, - }), - }), - loginUser: build.query({ - query: (queryArg) => ({ - url: \`/user/login\`, - params: { username: queryArg.username, password: queryArg.password }, - }), - }), - logoutUser: build.query({ - query: () => ({ url: \`/user/logout\` }), - }), - getUserByName: build.query({ - query: (queryArg) => ({ url: \`/user/\${queryArg.username}\` }), - }), - updateUser: build.mutation({ - query: (queryArg) => ({ - url: \`/user/\${queryArg.username}\`, - method: "PUT", - body: queryArg.user, - }), - }), - deleteUser: build.mutation({ - query: (queryArg) => ({ - url: \`/user/\${queryArg.username}\`, - method: "DELETE", - }), - }), - }), -}); -export type UpdatePetApiResponse = /** status 200 Successful operation */ Pet; -export type UpdatePetApiArg = { - /** Update an existent pet in the store */ - pet: Pet; -}; -export type AddPetApiResponse = /** status 200 Successful operation */ Pet; -export type AddPetApiArg = { - /** Create a new pet in the store */ - pet: Pet; -}; -export type FindPetsByStatusApiResponse = - /** status 200 successful operation */ Pet[]; -export type FindPetsByStatusApiArg = { - /** Status values that need to be considered for filter */ - status?: "available" | "pending" | "sold"; -}; -export type FindPetsByTagsApiResponse = - /** status 200 successful operation */ Pet[]; -export type FindPetsByTagsApiArg = { - /** Tags to filter by */ - tags?: string[]; -}; -export type GetPetByIdApiResponse = /** status 200 successful operation */ Pet; -export type GetPetByIdApiArg = { - /** ID of pet to return */ - petId: number; -}; -export type UpdatePetWithFormApiResponse = unknown; -export type UpdatePetWithFormApiArg = { - /** ID of pet that needs to be updated */ - petId: number; - /** Name of pet that needs to be updated */ - name?: string; - /** Status of pet that needs to be updated */ - status?: string; -}; -export type DeletePetApiResponse = unknown; -export type DeletePetApiArg = { - apiKey?: string; - /** Pet id to delete */ - petId: number; -}; -export type UploadFileApiResponse = - /** status 200 successful operation */ ApiResponse; -export type UploadFileApiArg = { - /** ID of pet to update */ - petId: number; - /** Additional Metadata */ - additionalMetadata?: string; - body: Blob; -}; -export type GetInventoryApiResponse = /** status 200 successful operation */ { - [key: string]: number; -}; -export type GetInventoryApiArg = void; -export type PlaceOrderApiResponse = - /** status 200 successful operation */ Order; -export type PlaceOrderApiArg = { - order: Order; -}; -export type GetOrderByIdApiResponse = - /** status 200 successful operation */ Order; -export type GetOrderByIdApiArg = { - /** ID of order that needs to be fetched */ - orderId: number; -}; -export type DeleteOrderApiResponse = unknown; -export type DeleteOrderApiArg = { - /** ID of the order that needs to be deleted */ - orderId: number; -}; -export type CreateUserApiResponse = unknown; -export type CreateUserApiArg = { - /** Created user object */ - user: User; -}; -export type CreateUsersWithListInputApiResponse = - /** status 200 Successful operation */ User; -export type CreateUsersWithListInputApiArg = { - body: User[]; -}; -export type LoginUserApiResponse = - /** status 200 successful operation */ string; -export type LoginUserApiArg = { - /** The user name for login */ - username?: string; - /** The password for login in clear text */ - password?: string; -}; -export type LogoutUserApiResponse = unknown; -export type LogoutUserApiArg = void; -export type GetUserByNameApiResponse = - /** status 200 successful operation */ User; -export type GetUserByNameApiArg = { - /** The name that needs to be fetched. Use user1 for testing. */ - username: string; -}; -export type UpdateUserApiResponse = unknown; -export type UpdateUserApiArg = { - /** name that need to be deleted */ - username: string; - /** Update an existent user in the store */ - user: User; -}; -export type DeleteUserApiResponse = unknown; -export type DeleteUserApiArg = { - /** The name that needs to be deleted */ - username: string; -}; -export type Category = { - id?: number; - name?: string; -}; -export type Tag = { - id?: number; - name?: string; -}; -export type Pet = { - id?: number; - name: string; - category?: Category; - photoUrls: string[]; - tags?: Tag[]; - status?: "available" | "pending" | "sold"; -}; -export type ApiResponse = { - code?: number; - type?: string; - message?: string; -}; -export type Order = { - id?: number; - petId?: number; - quantity?: number; - shipDate?: string; - status?: "placed" | "approved" | "delivered"; - complete?: boolean; -}; -export type User = { - id?: number; - username?: string; - firstName?: string; - lastName?: string; - email?: string; - password?: string; - phone?: string; - userStatus?: number; -}; - - -`; - -exports[`CLI options testing should create a file when --file is specified 1`] = ` -import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'; -export const api = createApi({ - baseQuery: fetchBaseQuery({ baseUrl: '/api/v3' }), - tagTypes: [], - endpoints: (build) => ({ - getHealthcheck: build.query({ - query: () => ({ url: \`/healthcheck\` }), - }), - updatePet: build.mutation({ - query: (queryArg) => ({ url: \`/pet\`, method: 'PUT', body: queryArg.pet }), - }), - addPet: build.mutation({ - query: (queryArg) => ({ url: \`/pet\`, method: 'POST', body: queryArg.pet }), - }), - findPetsByStatus: build.query({ - query: (queryArg) => ({ url: \`/pet/findByStatus\`, params: { status: queryArg.status } }), - }), - findPetsByTags: build.query({ - query: (queryArg) => ({ url: \`/pet/findByTags\`, params: { tags: queryArg.tags } }), - }), - getPetById: build.query({ - query: (queryArg) => ({ url: \`/pet/\${queryArg.petId}\` }), - }), - updatePetWithForm: build.mutation({ - query: (queryArg) => ({ - url: \`/pet/\${queryArg.petId}\`, - method: 'POST', - params: { name: queryArg.name, status: queryArg.status }, - }), - }), - deletePet: build.mutation({ - query: (queryArg) => ({ url: \`/pet/\${queryArg.petId}\`, method: 'DELETE', headers: { api_key: queryArg.apiKey } }), - }), - uploadFile: build.mutation({ - query: (queryArg) => ({ - url: \`/pet/\${queryArg.petId}/uploadImage\`, - method: 'POST', - body: queryArg.body, - params: { additionalMetadata: queryArg.additionalMetadata }, - }), - }), - getInventory: build.query({ - query: () => ({ url: \`/store/inventory\` }), - }), - placeOrder: build.mutation({ - query: (queryArg) => ({ url: \`/store/order\`, method: 'POST', body: queryArg.order }), - }), - getOrderById: build.query({ - query: (queryArg) => ({ url: \`/store/order/\${queryArg.orderId}\` }), - }), - deleteOrder: build.mutation({ - query: (queryArg) => ({ url: \`/store/order/\${queryArg.orderId}\`, method: 'DELETE' }), - }), - createUser: build.mutation({ - query: (queryArg) => ({ url: \`/user\`, method: 'POST', body: queryArg.user }), - }), - createUsersWithListInput: build.mutation({ - query: (queryArg) => ({ url: \`/user/createWithList\`, method: 'POST', body: queryArg.body }), - }), - loginUser: build.query({ - query: (queryArg) => ({ - url: \`/user/login\`, - params: { username: queryArg.username, password: queryArg.password }, - }), - }), - logoutUser: build.query({ - query: () => ({ url: \`/user/logout\` }), - }), - getUserByName: build.query({ - query: (queryArg) => ({ url: \`/user/\${queryArg.username}\` }), - }), - updateUser: build.mutation({ - query: (queryArg) => ({ url: \`/user/\${queryArg.username}\`, method: 'PUT', body: queryArg.user }), - }), - deleteUser: build.mutation({ - query: (queryArg) => ({ url: \`/user/\${queryArg.username}\`, method: 'DELETE' }), - }), - }), -}); -export type GetHealthcheckApiResponse = /** status 200 OK */ { - message: string; -}; -export type GetHealthcheckApiArg = void; -export type UpdatePetApiResponse = /** status 200 Successful operation */ Pet; -export type UpdatePetApiArg = { - /** Update an existent pet in the store */ - pet: Pet; -}; -export type AddPetApiResponse = /** status 200 Successful operation */ Pet; -export type AddPetApiArg = { - /** Create a new pet in the store */ - pet: Pet; -}; -export type FindPetsByStatusApiResponse = /** status 200 successful operation */ Pet[]; -export type FindPetsByStatusApiArg = { - /** Status values that need to be considered for filter */ - status?: 'available' | 'pending' | 'sold'; -}; -export type FindPetsByTagsApiResponse = /** status 200 successful operation */ Pet[]; -export type FindPetsByTagsApiArg = { - /** Tags to filter by */ - tags?: string[]; -}; -export type GetPetByIdApiResponse = /** status 200 successful operation */ Pet; -export type GetPetByIdApiArg = { - /** ID of pet to return */ - petId: number; -}; -export type UpdatePetWithFormApiResponse = unknown; -export type UpdatePetWithFormApiArg = { - /** ID of pet that needs to be updated */ - petId: number; - /** Name of pet that needs to be updated */ - name?: string; - /** Status of pet that needs to be updated */ - status?: string; -}; -export type DeletePetApiResponse = unknown; -export type DeletePetApiArg = { - apiKey?: string; - /** Pet id to delete */ - petId: number; -}; -export type UploadFileApiResponse = /** status 200 successful operation */ ApiResponse; -export type UploadFileApiArg = { - /** ID of pet to update */ - petId: number; - /** Additional Metadata */ - additionalMetadata?: string; - body: Blob; -}; -export type GetInventoryApiResponse = /** status 200 successful operation */ { - [key: string]: number; -}; -export type GetInventoryApiArg = void; -export type PlaceOrderApiResponse = /** status 200 successful operation */ Order; -export type PlaceOrderApiArg = { - order: Order; -}; -export type GetOrderByIdApiResponse = /** status 200 successful operation */ Order; -export type GetOrderByIdApiArg = { - /** ID of order that needs to be fetched */ - orderId: number; -}; -export type DeleteOrderApiResponse = unknown; -export type DeleteOrderApiArg = { - /** ID of the order that needs to be deleted */ - orderId: number; -}; -export type CreateUserApiResponse = unknown; -export type CreateUserApiArg = { - /** Created user object */ - user: User; -}; -export type CreateUsersWithListInputApiResponse = /** status 200 Successful operation */ User; -export type CreateUsersWithListInputApiArg = { - body: User[]; -}; -export type LoginUserApiResponse = /** status 200 successful operation */ string; -export type LoginUserApiArg = { - /** The user name for login */ - username?: string; - /** The password for login in clear text */ - password?: string; -}; -export type LogoutUserApiResponse = unknown; -export type LogoutUserApiArg = void; -export type GetUserByNameApiResponse = /** status 200 successful operation */ User; -export type GetUserByNameApiArg = { - /** The name that needs to be fetched. Use user1 for testing. */ - username: string; -}; -export type UpdateUserApiResponse = unknown; -export type UpdateUserApiArg = { - /** name that need to be deleted */ - username: string; - /** Update an existent user in the store */ - user: User; -}; -export type DeleteUserApiResponse = unknown; -export type DeleteUserApiArg = { - /** The name that needs to be deleted */ - username: string; -}; -export type Category = { - id?: number; - name?: string; -}; -export type Tag = { - id?: number; - name?: string; -}; -export type Pet = { - id?: number; - name: string; - category?: Category; - photoUrls: string[]; - tags?: Tag[]; - status?: 'available' | 'pending' | 'sold'; -}; -export type ApiResponse = { - code?: number; - type?: string; - message?: string; -}; -export type Order = { - id?: number; - petId?: number; - quantity?: number; - shipDate?: string; - status?: 'placed' | 'approved' | 'delivered'; - complete?: boolean; -}; -export type User = { - id?: number; - username?: string; - firstName?: string; - lastName?: string; - email?: string; - password?: string; - phone?: string; - userStatus?: number; -}; - -`; - -exports[`CLI options testing should generate react hooks as a part of the output 1`] = ` -import { createApi } from "@reduxjs/toolkit/query/react"; -import { fetchBaseQuery } from "@reduxjs/toolkit/query"; -export const api = createApi({ - baseQuery: fetchBaseQuery({ baseUrl: "/api/v3" }), - tagTypes: [], - endpoints: (build) => ({ - getHealthcheck: build.query< - GetHealthcheckApiResponse, - GetHealthcheckApiArg - >({ - query: () => ({ url: \`/healthcheck\` }), - }), - updatePet: build.mutation({ - query: (queryArg) => ({ url: \`/pet\`, method: "PUT", body: queryArg.pet }), - }), - addPet: build.mutation({ - query: (queryArg) => ({ - url: \`/pet\`, - method: "POST", - body: queryArg.pet, - }), - }), - findPetsByStatus: build.query< - FindPetsByStatusApiResponse, - FindPetsByStatusApiArg - >({ - query: (queryArg) => ({ - url: \`/pet/findByStatus\`, - params: { status: queryArg.status }, - }), - }), - findPetsByTags: build.query< - FindPetsByTagsApiResponse, - FindPetsByTagsApiArg - >({ - query: (queryArg) => ({ - url: \`/pet/findByTags\`, - params: { tags: queryArg.tags }, - }), - }), - getPetById: build.query({ - query: (queryArg) => ({ url: \`/pet/\${queryArg.petId}\` }), - }), - updatePetWithForm: build.mutation< - UpdatePetWithFormApiResponse, - UpdatePetWithFormApiArg - >({ - query: (queryArg) => ({ - url: \`/pet/\${queryArg.petId}\`, - method: "POST", - params: { name: queryArg.name, status: queryArg.status }, - }), - }), - deletePet: build.mutation({ - query: (queryArg) => ({ - url: \`/pet/\${queryArg.petId}\`, - method: "DELETE", - headers: { api_key: queryArg.apiKey }, - }), - }), - uploadFile: build.mutation({ - query: (queryArg) => ({ - url: \`/pet/\${queryArg.petId}/uploadImage\`, - method: "POST", - body: queryArg.body, - params: { additionalMetadata: queryArg.additionalMetadata }, - }), - }), - getInventory: build.query({ - query: () => ({ url: \`/store/inventory\` }), - }), - placeOrder: build.mutation({ - query: (queryArg) => ({ - url: \`/store/order\`, - method: "POST", - body: queryArg.order, - }), - }), - getOrderById: build.query({ - query: (queryArg) => ({ url: \`/store/order/\${queryArg.orderId}\` }), - }), - deleteOrder: build.mutation({ - query: (queryArg) => ({ - url: \`/store/order/\${queryArg.orderId}\`, - method: "DELETE", - }), - }), - createUser: build.mutation({ - query: (queryArg) => ({ - url: \`/user\`, - method: "POST", - body: queryArg.user, - }), - }), - createUsersWithListInput: build.mutation< - CreateUsersWithListInputApiResponse, - CreateUsersWithListInputApiArg - >({ - query: (queryArg) => ({ - url: \`/user/createWithList\`, - method: "POST", - body: queryArg.body, - }), - }), - loginUser: build.query({ - query: (queryArg) => ({ - url: \`/user/login\`, - params: { username: queryArg.username, password: queryArg.password }, - }), - }), - logoutUser: build.query({ - query: () => ({ url: \`/user/logout\` }), - }), - getUserByName: build.query({ - query: (queryArg) => ({ url: \`/user/\${queryArg.username}\` }), - }), - updateUser: build.mutation({ - query: (queryArg) => ({ - url: \`/user/\${queryArg.username}\`, - method: "PUT", - body: queryArg.user, - }), - }), - deleteUser: build.mutation({ - query: (queryArg) => ({ - url: \`/user/\${queryArg.username}\`, - method: "DELETE", - }), - }), - }), -}); -export type GetHealthcheckApiResponse = /** status 200 OK */ { - message: string; -}; -export type GetHealthcheckApiArg = void; -export type UpdatePetApiResponse = /** status 200 Successful operation */ Pet; -export type UpdatePetApiArg = { - /** Update an existent pet in the store */ - pet: Pet; -}; -export type AddPetApiResponse = /** status 200 Successful operation */ Pet; -export type AddPetApiArg = { - /** Create a new pet in the store */ - pet: Pet; -}; -export type FindPetsByStatusApiResponse = - /** status 200 successful operation */ Pet[]; -export type FindPetsByStatusApiArg = { - /** Status values that need to be considered for filter */ - status?: "available" | "pending" | "sold"; -}; -export type FindPetsByTagsApiResponse = - /** status 200 successful operation */ Pet[]; -export type FindPetsByTagsApiArg = { - /** Tags to filter by */ - tags?: string[]; -}; -export type GetPetByIdApiResponse = /** status 200 successful operation */ Pet; -export type GetPetByIdApiArg = { - /** ID of pet to return */ - petId: number; -}; -export type UpdatePetWithFormApiResponse = unknown; -export type UpdatePetWithFormApiArg = { - /** ID of pet that needs to be updated */ - petId: number; - /** Name of pet that needs to be updated */ - name?: string; - /** Status of pet that needs to be updated */ - status?: string; -}; -export type DeletePetApiResponse = unknown; -export type DeletePetApiArg = { - apiKey?: string; - /** Pet id to delete */ - petId: number; -}; -export type UploadFileApiResponse = - /** status 200 successful operation */ ApiResponse; -export type UploadFileApiArg = { - /** ID of pet to update */ - petId: number; - /** Additional Metadata */ - additionalMetadata?: string; - body: Blob; -}; -export type GetInventoryApiResponse = /** status 200 successful operation */ { - [key: string]: number; -}; -export type GetInventoryApiArg = void; -export type PlaceOrderApiResponse = - /** status 200 successful operation */ Order; -export type PlaceOrderApiArg = { - order: Order; -}; -export type GetOrderByIdApiResponse = - /** status 200 successful operation */ Order; -export type GetOrderByIdApiArg = { - /** ID of order that needs to be fetched */ - orderId: number; -}; -export type DeleteOrderApiResponse = unknown; -export type DeleteOrderApiArg = { - /** ID of the order that needs to be deleted */ - orderId: number; -}; -export type CreateUserApiResponse = unknown; -export type CreateUserApiArg = { - /** Created user object */ - user: User; -}; -export type CreateUsersWithListInputApiResponse = - /** status 200 Successful operation */ User; -export type CreateUsersWithListInputApiArg = { - body: User[]; -}; -export type LoginUserApiResponse = - /** status 200 successful operation */ string; -export type LoginUserApiArg = { - /** The user name for login */ - username?: string; - /** The password for login in clear text */ - password?: string; -}; -export type LogoutUserApiResponse = unknown; -export type LogoutUserApiArg = void; -export type GetUserByNameApiResponse = - /** status 200 successful operation */ User; -export type GetUserByNameApiArg = { - /** The name that needs to be fetched. Use user1 for testing. */ - username: string; -}; -export type UpdateUserApiResponse = unknown; -export type UpdateUserApiArg = { - /** name that need to be deleted */ - username: string; - /** Update an existent user in the store */ - user: User; -}; -export type DeleteUserApiResponse = unknown; -export type DeleteUserApiArg = { - /** The name that needs to be deleted */ - username: string; -}; -export type Category = { - id?: number; - name?: string; -}; -export type Tag = { - id?: number; - name?: string; -}; -export type Pet = { - id?: number; - name: string; - category?: Category; - photoUrls: string[]; - tags?: Tag[]; - status?: "available" | "pending" | "sold"; -}; -export type ApiResponse = { - code?: number; - type?: string; - message?: string; -}; -export type Order = { - id?: number; - petId?: number; - quantity?: number; - shipDate?: string; - status?: "placed" | "approved" | "delivered"; - complete?: boolean; -}; -export type User = { - id?: number; - username?: string; - firstName?: string; - lastName?: string; - email?: string; - password?: string; - phone?: string; - userStatus?: number; -}; -export const { - useGetHealthcheckQuery, - useUpdatePetMutation, - useAddPetMutation, - useFindPetsByStatusQuery, - useFindPetsByTagsQuery, - useGetPetByIdQuery, - useUpdatePetWithFormMutation, - useDeletePetMutation, - useUploadFileMutation, - useGetInventoryQuery, - usePlaceOrderMutation, - useGetOrderByIdQuery, - useDeleteOrderMutation, - useCreateUserMutation, - useCreateUsersWithListInputMutation, - useLoginUserQuery, - useLogoutUserQuery, - useGetUserByNameQuery, - useUpdateUserMutation, - useDeleteUserMutation, -} = api; - - -`; - -exports[`CLI options testing should log output to the console when a filename is not specified 1`] = ` -import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query"; -export const api = createApi({ - baseQuery: fetchBaseQuery({ baseUrl: "/api/v3" }), - tagTypes: [], +exports[`CLI options testing generation with \`config.example.js\` 1`] = ` +import { api } from '../fixtures/emptyApi'; +const injectedRtkApi = api.injectEndpoints({ endpoints: (build) => ({ - getHealthcheck: build.query< - GetHealthcheckApiResponse, - GetHealthcheckApiArg - >({ - query: () => ({ url: \`/healthcheck\` }), - }), updatePet: build.mutation({ - query: (queryArg) => ({ url: \`/pet\`, method: "PUT", body: queryArg.pet }), + query: (queryArg) => ({ url: \`/pet\`, method: 'PUT', body: queryArg.pet }), }), addPet: build.mutation({ - query: (queryArg) => ({ - url: \`/pet\`, - method: "POST", - body: queryArg.pet, - }), + query: (queryArg) => ({ url: \`/pet\`, method: 'POST', body: queryArg.pet }), }), - findPetsByStatus: build.query< - FindPetsByStatusApiResponse, - FindPetsByStatusApiArg - >({ - query: (queryArg) => ({ - url: \`/pet/findByStatus\`, - params: { status: queryArg.status }, - }), + findPetsByStatus: build.query({ + query: (queryArg) => ({ url: \`/pet/findByStatus\`, params: { status: queryArg.status } }), }), - findPetsByTags: build.query< - FindPetsByTagsApiResponse, - FindPetsByTagsApiArg - >({ - query: (queryArg) => ({ - url: \`/pet/findByTags\`, - params: { tags: queryArg.tags }, - }), + findPetsByTags: build.query({ + query: (queryArg) => ({ url: \`/pet/findByTags\`, params: { tags: queryArg.tags } }), }), getPetById: build.query({ query: (queryArg) => ({ url: \`/pet/\${queryArg.petId}\` }), }), - updatePetWithForm: build.mutation< - UpdatePetWithFormApiResponse, - UpdatePetWithFormApiArg - >({ + updatePetWithForm: build.mutation({ query: (queryArg) => ({ url: \`/pet/\${queryArg.petId}\`, - method: "POST", + method: 'POST', params: { name: queryArg.name, status: queryArg.status }, }), }), deletePet: build.mutation({ - query: (queryArg) => ({ - url: \`/pet/\${queryArg.petId}\`, - method: "DELETE", - headers: { api_key: queryArg.apiKey }, - }), + query: (queryArg) => ({ url: \`/pet/\${queryArg.petId}\`, method: 'DELETE', headers: { api_key: queryArg.apiKey } }), }), uploadFile: build.mutation({ query: (queryArg) => ({ url: \`/pet/\${queryArg.petId}/uploadImage\`, - method: "POST", + method: 'POST', body: queryArg.body, params: { additionalMetadata: queryArg.additionalMetadata }, }), @@ -886,37 +41,19 @@ export const api = createApi({ query: () => ({ url: \`/store/inventory\` }), }), placeOrder: build.mutation({ - query: (queryArg) => ({ - url: \`/store/order\`, - method: "POST", - body: queryArg.order, - }), + query: (queryArg) => ({ url: \`/store/order\`, method: 'POST', body: queryArg.order }), }), getOrderById: build.query({ query: (queryArg) => ({ url: \`/store/order/\${queryArg.orderId}\` }), }), deleteOrder: build.mutation({ - query: (queryArg) => ({ - url: \`/store/order/\${queryArg.orderId}\`, - method: "DELETE", - }), + query: (queryArg) => ({ url: \`/store/order/\${queryArg.orderId}\`, method: 'DELETE' }), }), createUser: build.mutation({ - query: (queryArg) => ({ - url: \`/user\`, - method: "POST", - body: queryArg.user, - }), + query: (queryArg) => ({ url: \`/user\`, method: 'POST', body: queryArg.user }), }), - createUsersWithListInput: build.mutation< - CreateUsersWithListInputApiResponse, - CreateUsersWithListInputApiArg - >({ - query: (queryArg) => ({ - url: \`/user/createWithList\`, - method: "POST", - body: queryArg.body, - }), + createUsersWithListInput: build.mutation({ + query: (queryArg) => ({ url: \`/user/createWithList\`, method: 'POST', body: queryArg.body }), }), loginUser: build.query({ query: (queryArg) => ({ @@ -931,24 +68,15 @@ export const api = createApi({ query: (queryArg) => ({ url: \`/user/\${queryArg.username}\` }), }), updateUser: build.mutation({ - query: (queryArg) => ({ - url: \`/user/\${queryArg.username}\`, - method: "PUT", - body: queryArg.user, - }), + query: (queryArg) => ({ url: \`/user/\${queryArg.username}\`, method: 'PUT', body: queryArg.user }), }), deleteUser: build.mutation({ - query: (queryArg) => ({ - url: \`/user/\${queryArg.username}\`, - method: "DELETE", - }), + query: (queryArg) => ({ url: \`/user/\${queryArg.username}\`, method: 'DELETE' }), }), }), + overrideExisting: false, }); -export type GetHealthcheckApiResponse = /** status 200 OK */ { - message: string; -}; -export type GetHealthcheckApiArg = void; +export { injectedRtkApi as api }; export type UpdatePetApiResponse = /** status 200 Successful operation */ Pet; export type UpdatePetApiArg = { /** Update an existent pet in the store */ @@ -959,14 +87,12 @@ export type AddPetApiArg = { /** Create a new pet in the store */ pet: Pet; }; -export type FindPetsByStatusApiResponse = - /** status 200 successful operation */ Pet[]; +export type FindPetsByStatusApiResponse = /** status 200 successful operation */ Pet[]; export type FindPetsByStatusApiArg = { /** Status values that need to be considered for filter */ - status?: "available" | "pending" | "sold"; + status?: 'available' | 'pending' | 'sold'; }; -export type FindPetsByTagsApiResponse = - /** status 200 successful operation */ Pet[]; +export type FindPetsByTagsApiResponse = /** status 200 successful operation */ Pet[]; export type FindPetsByTagsApiArg = { /** Tags to filter by */ tags?: string[]; @@ -991,8 +117,7 @@ export type DeletePetApiArg = { /** Pet id to delete */ petId: number; }; -export type UploadFileApiResponse = - /** status 200 successful operation */ ApiResponse; +export type UploadFileApiResponse = /** status 200 successful operation */ ApiResponse; export type UploadFileApiArg = { /** ID of pet to update */ petId: number; @@ -1004,13 +129,11 @@ export type GetInventoryApiResponse = /** status 200 successful operation */ { [key: string]: number; }; export type GetInventoryApiArg = void; -export type PlaceOrderApiResponse = - /** status 200 successful operation */ Order; +export type PlaceOrderApiResponse = /** status 200 successful operation */ Order; export type PlaceOrderApiArg = { order: Order; }; -export type GetOrderByIdApiResponse = - /** status 200 successful operation */ Order; +export type GetOrderByIdApiResponse = /** status 200 successful operation */ Order; export type GetOrderByIdApiArg = { /** ID of order that needs to be fetched */ orderId: number; @@ -1025,13 +148,11 @@ export type CreateUserApiArg = { /** Created user object */ user: User; }; -export type CreateUsersWithListInputApiResponse = - /** status 200 Successful operation */ User; +export type CreateUsersWithListInputApiResponse = /** status 200 Successful operation */ User; export type CreateUsersWithListInputApiArg = { body: User[]; }; -export type LoginUserApiResponse = - /** status 200 successful operation */ string; +export type LoginUserApiResponse = /** status 200 successful operation */ string; export type LoginUserApiArg = { /** The user name for login */ username?: string; @@ -1040,8 +161,7 @@ export type LoginUserApiArg = { }; export type LogoutUserApiResponse = unknown; export type LogoutUserApiArg = void; -export type GetUserByNameApiResponse = - /** status 200 successful operation */ User; +export type GetUserByNameApiResponse = /** status 200 successful operation */ User; export type GetUserByNameApiArg = { /** The name that needs to be fetched. Use user1 for testing. */ username: string; @@ -1072,7 +192,7 @@ export type Pet = { category?: Category; photoUrls: string[]; tags?: Tag[]; - status?: "available" | "pending" | "sold"; + status?: 'available' | 'pending' | 'sold'; }; export type ApiResponse = { code?: number; @@ -1084,7 +204,7 @@ export type Order = { petId?: number; quantity?: number; shipDate?: string; - status?: "placed" | "approved" | "delivered"; + status?: 'placed' | 'approved' | 'delivered'; complete?: boolean; }; export type User = { @@ -1098,7 +218,6 @@ export type User = { userStatus?: number; }; - `; exports[`yaml parsing should be able to use read a yaml file and create a file with the output when --file is specified 1`] = ` diff --git a/test/__snapshots__/generateEndpoints.test.ts.snap b/test/__snapshots__/generateEndpoints.test.ts.snap new file mode 100644 index 0000000..75027e5 --- /dev/null +++ b/test/__snapshots__/generateEndpoints.test.ts.snap @@ -0,0 +1,446 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`calling without \`outputFile\` returns the generated api 1`] = ` +import { api } from './fixtures/emptyApi'; +const injectedRtkApi = api.injectEndpoints({ + endpoints: (build) => ({ + getHealthcheck: build.query({ + query: () => ({ url: \`/healthcheck\` }), + }), + updatePet: build.mutation({ + query: (queryArg) => ({ url: \`/pet\`, method: 'PUT', body: queryArg.pet }), + }), + addPet: build.mutation({ + query: (queryArg) => ({ + url: \`/pet\`, + method: 'POST', + body: queryArg.pet, + }), + }), + findPetsByStatus: build.query({ + query: (queryArg) => ({ + url: \`/pet/findByStatus\`, + params: { status: queryArg.status }, + }), + }), + findPetsByTags: build.query({ + query: (queryArg) => ({ + url: \`/pet/findByTags\`, + params: { tags: queryArg.tags }, + }), + }), + getPetById: build.query({ + query: (queryArg) => ({ url: \`/pet/\${queryArg.petId}\` }), + }), + updatePetWithForm: build.mutation({ + query: (queryArg) => ({ + url: \`/pet/\${queryArg.petId}\`, + method: 'POST', + params: { name: queryArg.name, status: queryArg.status }, + }), + }), + deletePet: build.mutation({ + query: (queryArg) => ({ + url: \`/pet/\${queryArg.petId}\`, + method: 'DELETE', + headers: { api_key: queryArg.apiKey }, + }), + }), + uploadFile: build.mutation({ + query: (queryArg) => ({ + url: \`/pet/\${queryArg.petId}/uploadImage\`, + method: 'POST', + body: queryArg.body, + params: { additionalMetadata: queryArg.additionalMetadata }, + }), + }), + getInventory: build.query({ + query: () => ({ url: \`/store/inventory\` }), + }), + placeOrder: build.mutation({ + query: (queryArg) => ({ + url: \`/store/order\`, + method: 'POST', + body: queryArg.order, + }), + }), + getOrderById: build.query({ + query: (queryArg) => ({ url: \`/store/order/\${queryArg.orderId}\` }), + }), + deleteOrder: build.mutation({ + query: (queryArg) => ({ + url: \`/store/order/\${queryArg.orderId}\`, + method: 'DELETE', + }), + }), + createUser: build.mutation({ + query: (queryArg) => ({ + url: \`/user\`, + method: 'POST', + body: queryArg.user, + }), + }), + createUsersWithListInput: build.mutation({ + query: (queryArg) => ({ + url: \`/user/createWithList\`, + method: 'POST', + body: queryArg.body, + }), + }), + loginUser: build.query({ + query: (queryArg) => ({ + url: \`/user/login\`, + params: { username: queryArg.username, password: queryArg.password }, + }), + }), + logoutUser: build.query({ + query: () => ({ url: \`/user/logout\` }), + }), + getUserByName: build.query({ + query: (queryArg) => ({ url: \`/user/\${queryArg.username}\` }), + }), + updateUser: build.mutation({ + query: (queryArg) => ({ + url: \`/user/\${queryArg.username}\`, + method: 'PUT', + body: queryArg.user, + }), + }), + deleteUser: build.mutation({ + query: (queryArg) => ({ + url: \`/user/\${queryArg.username}\`, + method: 'DELETE', + }), + }), + }), + overrideExisting: false, +}); +export { injectedRtkApi as api }; +export type GetHealthcheckApiResponse = /** status 200 OK */ { + message: string; +}; +export type GetHealthcheckApiArg = void; +export type UpdatePetApiResponse = /** status 200 Successful operation */ Pet; +export type UpdatePetApiArg = { + /** Update an existent pet in the store */ + pet: Pet; +}; +export type AddPetApiResponse = /** status 200 Successful operation */ Pet; +export type AddPetApiArg = { + /** Create a new pet in the store */ + pet: Pet; +}; +export type FindPetsByStatusApiResponse = /** status 200 successful operation */ Pet[]; +export type FindPetsByStatusApiArg = { + /** Status values that need to be considered for filter */ + status?: 'available' | 'pending' | 'sold'; +}; +export type FindPetsByTagsApiResponse = /** status 200 successful operation */ Pet[]; +export type FindPetsByTagsApiArg = { + /** Tags to filter by */ + tags?: string[]; +}; +export type GetPetByIdApiResponse = /** status 200 successful operation */ Pet; +export type GetPetByIdApiArg = { + /** ID of pet to return */ + petId: number; +}; +export type UpdatePetWithFormApiResponse = unknown; +export type UpdatePetWithFormApiArg = { + /** ID of pet that needs to be updated */ + petId: number; + /** Name of pet that needs to be updated */ + name?: string; + /** Status of pet that needs to be updated */ + status?: string; +}; +export type DeletePetApiResponse = unknown; +export type DeletePetApiArg = { + apiKey?: string; + /** Pet id to delete */ + petId: number; +}; +export type UploadFileApiResponse = /** status 200 successful operation */ ApiResponse; +export type UploadFileApiArg = { + /** ID of pet to update */ + petId: number; + /** Additional Metadata */ + additionalMetadata?: string; + body: Blob; +}; +export type GetInventoryApiResponse = /** status 200 successful operation */ { + [key: string]: number; +}; +export type GetInventoryApiArg = void; +export type PlaceOrderApiResponse = /** status 200 successful operation */ Order; +export type PlaceOrderApiArg = { + order: Order; +}; +export type GetOrderByIdApiResponse = /** status 200 successful operation */ Order; +export type GetOrderByIdApiArg = { + /** ID of order that needs to be fetched */ + orderId: number; +}; +export type DeleteOrderApiResponse = unknown; +export type DeleteOrderApiArg = { + /** ID of the order that needs to be deleted */ + orderId: number; +}; +export type CreateUserApiResponse = unknown; +export type CreateUserApiArg = { + /** Created user object */ + user: User; +}; +export type CreateUsersWithListInputApiResponse = /** status 200 Successful operation */ User; +export type CreateUsersWithListInputApiArg = { + body: User[]; +}; +export type LoginUserApiResponse = /** status 200 successful operation */ string; +export type LoginUserApiArg = { + /** The user name for login */ + username?: string; + /** The password for login in clear text */ + password?: string; +}; +export type LogoutUserApiResponse = unknown; +export type LogoutUserApiArg = void; +export type GetUserByNameApiResponse = /** status 200 successful operation */ User; +export type GetUserByNameApiArg = { + /** The name that needs to be fetched. Use user1 for testing. */ + username: string; +}; +export type UpdateUserApiResponse = unknown; +export type UpdateUserApiArg = { + /** name that need to be deleted */ + username: string; + /** Update an existent user in the store */ + user: User; +}; +export type DeleteUserApiResponse = unknown; +export type DeleteUserApiArg = { + /** The name that needs to be deleted */ + username: string; +}; +export type Category = { + id?: number; + name?: string; +}; +export type Tag = { + id?: number; + name?: string; +}; +export type Pet = { + id?: number; + name: string; + category?: Category; + photoUrls: string[]; + tags?: Tag[]; + status?: 'available' | 'pending' | 'sold'; +}; +export type ApiResponse = { + code?: number; + type?: string; + message?: string; +}; +export type Order = { + id?: number; + petId?: number; + quantity?: number; + shipDate?: string; + status?: 'placed' | 'approved' | 'delivered'; + complete?: boolean; +}; +export type User = { + id?: number; + username?: string; + firstName?: string; + lastName?: string; + email?: string; + password?: string; + phone?: string; + userStatus?: number; +}; + +`; + +exports[`endpoint filtering: should only have endpoints loginUser, placeOrder, getOrderById, deleteOrder 1`] = ` +import { api } from './fixtures/emptyApi'; +const injectedRtkApi = api.injectEndpoints({ + endpoints: (build) => ({ + placeOrder: build.mutation({ + query: (queryArg) => ({ + url: \`/store/order\`, + method: 'POST', + body: queryArg.order, + }), + }), + getOrderById: build.query({ + query: (queryArg) => ({ url: \`/store/order/\${queryArg.orderId}\` }), + }), + deleteOrder: build.mutation({ + query: (queryArg) => ({ + url: \`/store/order/\${queryArg.orderId}\`, + method: 'DELETE', + }), + }), + loginUser: build.query({ + query: (queryArg) => ({ + url: \`/user/login\`, + params: { username: queryArg.username, password: queryArg.password }, + }), + }), + }), + overrideExisting: false, +}); +export { injectedRtkApi as api }; +export type PlaceOrderApiResponse = /** status 200 successful operation */ Order; +export type PlaceOrderApiArg = { + order: Order; +}; +export type GetOrderByIdApiResponse = /** status 200 successful operation */ Order; +export type GetOrderByIdApiArg = { + /** ID of order that needs to be fetched */ + orderId: number; +}; +export type DeleteOrderApiResponse = unknown; +export type DeleteOrderApiArg = { + /** ID of the order that needs to be deleted */ + orderId: number; +}; +export type LoginUserApiResponse = /** status 200 successful operation */ string; +export type LoginUserApiArg = { + /** The user name for login */ + username?: string; + /** The password for login in clear text */ + password?: string; +}; +export type Order = { + id?: number; + petId?: number; + quantity?: number; + shipDate?: string; + status?: 'placed' | 'approved' | 'delivered'; + complete?: boolean; +}; + +`; + +exports[`endpoint overrides: loginUser should be a mutation 1`] = ` +import { api } from './fixtures/emptyApi'; +const injectedRtkApi = api.injectEndpoints({ + endpoints: (build) => ({ + loginUser: build.mutation({ + query: (queryArg) => ({ + url: \`/user/login\`, + method: 'GET', + params: { username: queryArg.username, password: queryArg.password }, + }), + }), + }), + overrideExisting: false, +}); +export { injectedRtkApi as api }; +export type LoginUserApiResponse = /** status 200 successful operation */ string; +export type LoginUserApiArg = { + /** The user name for login */ + username?: string; + /** The password for login in clear text */ + password?: string; +}; + +`; + +exports[`hooks generation uses overrides: should generate an \`useLoginMutation\` mutation hook 1`] = ` +import { api } from './fixtures/emptyApi'; +const injectedRtkApi = api.injectEndpoints({ + endpoints: (build) => ({ + loginUser: build.mutation({ + query: (queryArg) => ({ + url: \`/user/login\`, + method: 'GET', + params: { username: queryArg.username, password: queryArg.password }, + }), + }), + }), + overrideExisting: false, +}); +export { injectedRtkApi as api }; +export type LoginUserApiResponse = /** status 200 successful operation */ string; +export type LoginUserApiArg = { + /** The user name for login */ + username?: string; + /** The password for login in clear text */ + password?: string; +}; +export const { useLoginUserMutation } = enhancedApi; + +`; + +exports[`hooks generation: should generate an \`useGetPetByIdQuery\` query hook and an \`useAddPetMutation\` mutation hook 1`] = ` +import { api } from './fixtures/emptyApi'; +const injectedRtkApi = api.injectEndpoints({ + endpoints: (build) => ({ + addPet: build.mutation({ + query: (queryArg) => ({ + url: \`/pet\`, + method: 'POST', + body: queryArg.pet, + }), + }), + getPetById: build.query({ + query: (queryArg) => ({ url: \`/pet/\${queryArg.petId}\` }), + }), + }), + overrideExisting: false, +}); +export { injectedRtkApi as api }; +export type AddPetApiResponse = /** status 200 Successful operation */ Pet; +export type AddPetApiArg = { + /** Create a new pet in the store */ + pet: Pet; +}; +export type GetPetByIdApiResponse = /** status 200 successful operation */ Pet; +export type GetPetByIdApiArg = { + /** ID of pet to return */ + petId: number; +}; +export type Category = { + id?: number; + name?: string; +}; +export type Tag = { + id?: number; + name?: string; +}; +export type Pet = { + id?: number; + name: string; + category?: Category; + photoUrls: string[]; + tags?: Tag[]; + status?: 'available' | 'pending' | 'sold'; +}; +export const { useAddPetMutation, useGetPetByIdQuery } = enhancedApi; + +`; + +exports[`should use brackets in a querystring urls arg, when the arg contains full stops 1`] = ` +import { api } from './fixtures/emptyApi'; +const injectedRtkApi = api.injectEndpoints({ + endpoints: (build) => ({ + patchApiV1ListByItemId: build.mutation({ + query: (queryArg) => ({ + url: \`/api/v1/list/\${queryArg['item.id']}\`, + method: 'PATCH', + }), + }), + }), + overrideExisting: false, +}); +export { injectedRtkApi as api }; +export type PatchApiV1ListByItemIdApiResponse = /** status 200 A successful response. */ string; +export type PatchApiV1ListByItemIdApiArg = { + 'item.id': string; +}; + +`; diff --git a/test/cli.test.ts b/test/cli.test.ts index db8fd7c..4b06cd8 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -3,22 +3,13 @@ import * as fs from 'fs'; import path from 'path'; import del from 'del'; -import { MESSAGES } from '../src/utils'; - let id = 0; -const tmpDir = 'test/tmp'; +const tmpDir = path.resolve(__dirname, 'tmp'); function getTmpFileName() { return path.resolve(tmpDir, `${++id}.test.generated.ts`); } -function copyAndGetTmpFileName(fileToCopyPath: string, newFileName: string): string { - newFileName = `${++id}.test.${newFileName}`; - const newFilePath = path.resolve(tmpDir, newFileName); - fs.copyFileSync(fileToCopyPath, newFilePath, fs.constants.COPYFILE_EXCL); - return newFileName; -} - function cli(args: string[], cwd: string): Promise<{ error: ExecException | null; stdout: string; stderr: string }> { return new Promise((resolve) => { exec( @@ -46,317 +37,25 @@ afterAll(() => { }); describe('CLI options testing', () => { - it('should log output to the console when a filename is not specified', async () => { - const result = await cli([`./test/fixtures/petstore.json`], '.'); - expect(result.stdout).toMatchSnapshot(); - }); - - it('should accept a valid url as the target swagger file and generate a client', async () => { - const result = await cli([`https://petstore3.swagger.io/api/v3/openapi.json`], '.'); - expect(result.stdout).toMatchSnapshot(); - }); - - it('should generate react hooks as a part of the output', async () => { - const result = await cli(['-h', `./test/fixtures/petstore.json`], '.'); - expect(result.stdout).toMatchSnapshot(); - - // These are all of the hooks that we expect the petstore schema to output - const expectedHooks = [ - 'useGetHealthcheckQuery', - 'useUpdatePetMutation', - 'useAddPetMutation', - 'useFindPetsByStatusQuery', - 'useFindPetsByTagsQuery', - 'useGetPetByIdQuery', - 'useUpdatePetWithFormMutation', - 'useDeletePetMutation', - 'useUploadFileMutation', - 'useGetInventoryQuery', - 'usePlaceOrderMutation', - 'useGetOrderByIdQuery', - 'useDeleteOrderMutation', - 'useCreateUserMutation', - 'useCreateUsersWithListInputMutation', - 'useLoginUserQuery', - 'useLogoutUserQuery', - 'useGetUserByNameQuery', - 'useUpdateUserMutation', - 'useDeleteUserMutation', - ]; - - const numberOfHooks = expectedHooks.filter((name) => result.stdout.indexOf(name) > -1).length; - expect(numberOfHooks).toEqual(expectedHooks.length); - }); - - it('should contain the right imports when using custom import path', async () => { - const result = await cli(['--createApiImportPath', 'react', `./test/fixtures/petstore.json`], '.'); - expect(result.stdout).toContain(`import { createApi } from \"@reduxjs/toolkit/query/react\";`); - expect(result.stdout).not.toContain('useGetHealthcheckQuery'); // hooks not exported - }); - - it('should contain the right imports when using hooks and a custom base query', async () => { - const result = await cli( - ['-h', `--baseQuery`, `test/fixtures/customBaseQuery.ts:anotherNamedBaseQuery`, `./test/fixtures/petstore.json`], - '.' - ); - - expect(result.stdout).toContain(`import { createApi } from \"@reduxjs/toolkit/query/react\";`); - expect(result.stdout).toContain(`import { anotherNamedBaseQuery } from \"test/fixtures/customBaseQuery\";`); - }); - - it('should call fetchBaseQuery with the url provided to --baseUrl', async () => { - const result = await cli([`--baseUrl`, `http://swagger.io`, `./test/fixtures/petstore.json`], '.'); - - const output = result.stdout; - - expect(output).toContain('baseQuery: fetchBaseQuery({ baseUrl: "http://swagger.io" }),'); - }); - - it('should assign the specified baseQueryFn provided to --baseQuery', async () => { - const result = await cli( - [`--baseQuery`, `test/fixtures/customBaseQuery.ts:anotherNamedBaseQuery`, `./test/fixtures/petstore.json`], - '.' - ); - - const output = result.stdout; - - expect(output).not.toContain('fetchBaseQuery'); - expect(output).toContain('baseQuery: anotherNamedBaseQuery,'); - }); - - it('should show a warning and ignore --baseUrl when specified along with --baseQuery', async () => { - const result = await cli( - [ - `--baseQuery`, - `test/fixtures/customBaseQuery.ts:anotherNamedBaseQuery`, - `--baseUrl`, - `http://swagger.io`, - `./test/fixtures/petstore.json`, - ], - '.' - ); - - const output = result.stdout; - - const expectedWarnings = [MESSAGES.BASE_URL_IGNORED]; - - const numberOfWarnings = expectedWarnings.filter((msg) => result.stderr.indexOf(msg) > -1).length; - expect(numberOfWarnings).toEqual(expectedWarnings.length); - - expect(output).not.toContain('fetchBaseQuery'); - expect(output).toContain('baseQuery: anotherNamedBaseQuery,'); - }); - - it('should error out when the specified filename provided to --baseQuery is not found', async () => { - const result = await cli( - ['-h', `--baseQuery`, `test/fixtures/nonExistantFile.ts`, `./test/fixtures/petstore.json`], - '.' - ); - const expectedErrors = [MESSAGES.FILE_NOT_FOUND]; - - const numberOfErrors = expectedErrors.filter((msg) => result.stderr.indexOf(msg) > -1).length; - expect(numberOfErrors).toEqual(expectedErrors.length); - }); - - it('should error out when the specified filename provided to --baseQuery has no default export', async () => { - const result = await cli( - ['-h', `--baseQuery`, `test/fixtures/customBaseQueryWithoutDefault.ts`, `./test/fixtures/petstore.json`], - '.' - ); - - const expectedErrors = [MESSAGES.DEFAULT_EXPORT_MISSING]; - - const numberOfErrors = expectedErrors.filter((msg) => result.stderr.indexOf(msg) > -1).length; - expect(numberOfErrors).toEqual(expectedErrors.length); - }); - - it('should error out when the named function provided to --baseQuery is not found', async () => { - const result = await cli( - ['-h', `--baseQuery`, `test/fixtures/customBaseQuery.ts:missingFunctionName`, `./test/fixtures/petstore.json`], - '.' - ); - - const expectedErrors = [MESSAGES.NAMED_EXPORT_MISSING]; - - const numberOfErrors = expectedErrors.filter((msg) => result.stderr.indexOf(msg) > -1).length; - expect(numberOfErrors).toEqual(expectedErrors.length); - }); - - it('should not error when a valid named export is provided to --baseQuery', async () => { - const result = await cli( - ['-h', `--baseQuery`, `test/fixtures/customBaseQuery.ts:anotherNamedBaseQuery`, `./test/fixtures/petstore.json`], - '.' - ); - - expect(result.stdout).not.toContain('fetchBaseQuery'); - expect(result.stdout).toContain(`import { anotherNamedBaseQuery } from \"test/fixtures/customBaseQuery\"`); - - const expectedErrors = [MESSAGES.NAMED_EXPORT_MISSING]; - - const numberOfErrors = expectedErrors.filter((msg) => result.stderr.indexOf(msg) > -1).length; - expect(numberOfErrors).toEqual(0); - }); - - it('should not error when a valid named export is provided to --baseQuery with --file option', async () => { - const fileName = getTmpFileName(); - const result = await cli( - [ - '-h', - `--baseQuery`, - `test/fixtures/customBaseQuery.ts:anotherNamedBaseQuery`, - '--file', - fileName, - `./test/fixtures/petstore.json`, - ], - '.' - ); - - const output = fs.readFileSync(fileName, { encoding: 'utf-8' }); - - expect(output).not.toContain('fetchBaseQuery'); - expect(output).toContain(`import { anotherNamedBaseQuery } from '../fixtures/customBaseQuery'`); - - const expectedErrors = [MESSAGES.NAMED_EXPORT_MISSING]; - - const numberOfErrors = expectedErrors.filter((msg) => result.stderr.indexOf(msg) > -1).length; - expect(numberOfErrors).toEqual(0); + test('generation with `config.example.js`', async () => { + await cli([`./config.example.js`], __dirname); + expect(fs.readFileSync(path.resolve(tmpDir, 'example.ts'), 'utf-8')).toMatchSnapshot(); }); - it('should import { default as customBaseQuery } when a file with a default export is provided to --baseQuery', async () => { - const result = await cli( - ['-h', `--baseQuery`, `test/fixtures/customBaseQuery.ts`, `./test/fixtures/petstore.json`], - '.' - ); - - expect(result.stdout).not.toContain('fetchBaseQuery'); - expect(result.stdout).toContain(`import { default as customBaseQuery } from \"test/fixtures/customBaseQuery\"`); + test('ts, js and json all work the same', async () => { + await cli([`./config.example.js`], __dirname); + const fromJs = fs.readFileSync(path.resolve(tmpDir, 'example.ts'), 'utf-8'); + await cli([`./config.example.ts`], __dirname); + const fromTs = fs.readFileSync(path.resolve(tmpDir, 'example.ts'), 'utf-8'); + await cli([`./config.example.json`], __dirname); + const fromJson = fs.readFileSync(path.resolve(tmpDir, 'example.ts'), 'utf-8'); - const expectedErrors = [MESSAGES.NAMED_EXPORT_MISSING]; - - const numberOfErrors = expectedErrors.filter((msg) => result.stderr.indexOf(msg) > -1).length; - expect(numberOfErrors).toEqual(0); - }); - - it("should import { default as customBaseQuery } from './customBaseQuery' when a local customBaseQuery is provided to --baseQuery", async () => { - const localBaseQueryName = copyAndGetTmpFileName('./test/fixtures/customBaseQuery.ts', 'localCustomBaseQuery.ts'); - const fileName = getTmpFileName(); - const result = await cli( - [ - '-h', - `--baseQuery`, - `./test/tmp/${localBaseQueryName}:namedBaseQuery`, - '--file', - fileName, - `./test/fixtures/petstore.json`, - ], - '.' - ); - - const output = fs.readFileSync(fileName, { encoding: 'utf-8' }); - - const strippedLocalBaseQueryName = path.parse(localBaseQueryName).name; - - expect(output).not.toContain('fetchBaseQuery'); - expect(output).toContain(`import { namedBaseQuery } from './${strippedLocalBaseQueryName}'`); - - const expectedErrors = [MESSAGES.NAMED_EXPORT_MISSING]; - - const numberOfErrors = expectedErrors.filter((msg) => result.stderr.indexOf(msg) > -1).length; - expect(numberOfErrors).toEqual(0); - }); - - it('should error out when the specified with path alias is not found', async () => { - const result = await cli( - ['-h', `--baseQuery`, `@/hoge/fuga/nonExistantFile`, `./test/fixtures/petstore.json`], - '.' - ); - const expectedErrors = [MESSAGES.FILE_NOT_FOUND]; - - const numberOfErrors = expectedErrors.filter((msg) => result.stderr.indexOf(msg) > -1).length; - expect(numberOfErrors).toEqual(expectedErrors.length); - }); - - it('should throw the correct error when a specified tsconfig is not found', async () => { - const pathAlias = '@/customBaseQuery'; - const result = await cli( - [ - '-h', - `--baseQuery`, - `${pathAlias}:anotherNamedBaseQuery`, - '-c', - 'test/missing/tsconfig.json', - `./test/fixtures/petstore.json`, - ], - '.' - ); - - const expectedErrors = [MESSAGES.TSCONFIG_FILE_NOT_FOUND]; - - const numberOfErrors = expectedErrors.filter((msg) => result.stderr.indexOf(msg) > -1).length; - expect(numberOfErrors).toEqual(expectedErrors.length); - }); - - it('should work with path alias', async () => { - const pathAlias = '@/customBaseQuery'; - const result = await cli( - [ - '-h', - `--baseQuery`, - `${pathAlias}:anotherNamedBaseQuery`, - '-c', - 'test/tsconfig.json', - `./test/fixtures/petstore.json`, - ], - '.' - ); - - expect(result.stdout).not.toContain('fetchBaseQuery'); - expect(result.stdout).toContain(`import { anotherNamedBaseQuery } from \"${pathAlias}\"`); - - const expectedErrors = [MESSAGES.NAMED_EXPORT_MISSING]; - - const numberOfErrors = expectedErrors.filter((msg) => result.stderr.indexOf(msg) > -1).length; - expect(numberOfErrors).toEqual(0); - }); - - it('should work with path alias with file extension', async () => { - const pathAlias = '@/customBaseQuery'; - const result = await cli( - [ - '-h', - `--baseQuery`, - `${pathAlias}.ts:anotherNamedBaseQuery`, - '-c', - 'test/tsconfig.json', - `./test/fixtures/petstore.json`, - ], - '.' - ); - - expect(result.stdout).not.toContain('fetchBaseQuery'); - expect(result.stdout).toContain(`import { anotherNamedBaseQuery } from \"${pathAlias}\"`); - - const expectedErrors = [MESSAGES.NAMED_EXPORT_MISSING]; - - const numberOfErrors = expectedErrors.filter((msg) => result.stderr.indexOf(msg) > -1).length; - expect(numberOfErrors).toEqual(0); - }); - - it('should create a file when --file is specified', async () => { - const fileName = getTmpFileName(); - await cli([`--file ${fileName}`, `../fixtures/petstore.json`], tmpDir); - - expect(fs.readFileSync(fileName, { encoding: 'utf-8' })).toMatchSnapshot(); - }); - - it('should use brackets in a querystring urls arg, when the arg contains full stops', async () => { - const fileName = getTmpFileName(); - const result = await cli(['-h', `./test/fixtures/params.json`], '.'); - expect(result.stdout).toContain('`/api/v1/list/${queryArg["item.id"]}`'); + expect(fromTs).toEqual(fromJs); + expect(fromJson).toEqual(fromJs); }); }); -describe('yaml parsing', () => { +describe.skip('yaml parsing', () => { it('should parse a yaml schema from a URL', async () => { const result = await cli([`https://petstore3.swagger.io/api/v3/openapi.yaml`], '.'); expect(result.stdout).toMatchSnapshot(); diff --git a/test/config.example.js b/test/config.example.js new file mode 100644 index 0000000..2fe3d69 --- /dev/null +++ b/test/config.example.js @@ -0,0 +1,8 @@ +/** + * @type {import("@rtk-incubator/rtk-query-codegen-openapi").ConfigFile} + */ +module.exports = { + schemaFile: './fixtures/petstore.yaml', + apiFile: './fixtures/emptyApi.ts', + outputFile: './tmp/example.ts', +}; diff --git a/test/config.example.json b/test/config.example.json new file mode 100644 index 0000000..39f9db1 --- /dev/null +++ b/test/config.example.json @@ -0,0 +1,5 @@ +{ + "schemaFile": "./fixtures/petstore.yaml", + "apiFile": "./fixtures/emptyApi.ts", + "outputFile": "./tmp/example.ts" +} diff --git a/test/config.example.ts b/test/config.example.ts new file mode 100644 index 0000000..632b8ce --- /dev/null +++ b/test/config.example.ts @@ -0,0 +1,9 @@ +import { ConfigFile } from '@rtk-incubator/rtk-query-codegen-openapi'; + +const config: ConfigFile = { + schemaFile: './fixtures/petstore.yaml', + apiFile: './fixtures/emptyApi.ts', + outputFile: './tmp/example.ts', +}; + +export default config; diff --git a/test/fixtures/emptyApi.ts b/test/fixtures/emptyApi.ts new file mode 100644 index 0000000..60cbd48 --- /dev/null +++ b/test/fixtures/emptyApi.ts @@ -0,0 +1,6 @@ +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; + +export const api = createApi({ + baseQuery: fetchBaseQuery({}), + endpoints: () => ({}), +}); diff --git a/test/generateEndpoints.test.ts b/test/generateEndpoints.test.ts new file mode 100644 index 0000000..8dbca52 --- /dev/null +++ b/test/generateEndpoints.test.ts @@ -0,0 +1,77 @@ +import { resolve } from 'path'; +import { generateEndpoints } from '../src'; + +test('calling without `outputFile` returns the generated api', async () => { + const api = await generateEndpoints({ + apiFile: './fixtures/emptyApi.ts', + schemaFile: resolve(__dirname, 'fixtures/petstore.json'), + }); + expect(api).toMatchSnapshot(); +}); + +test('endpoint filtering', async () => { + const api = await generateEndpoints({ + apiFile: './fixtures/emptyApi.ts', + schemaFile: resolve(__dirname, 'fixtures/petstore.json'), + filterEndpoints: ['loginUser', /Order/], + }); + expect(api).toMatchSnapshot('should only have endpoints loginUser, placeOrder, getOrderById, deleteOrder'); +}); + +test('endpoint overrides', async () => { + const api = await generateEndpoints({ + apiFile: './fixtures/emptyApi.ts', + schemaFile: resolve(__dirname, 'fixtures/petstore.json'), + filterEndpoints: 'loginUser', + endpointOverrides: [ + { + pattern: 'loginUser', + type: 'mutation', + }, + ], + }); + expect(api).not.toMatch(/loginUser: build.query/); + expect(api).toMatch(/loginUser: build.mutation/); + expect(api).toMatchSnapshot('loginUser should be a mutation'); +}); + +test('hooks generation', async () => { + const api = await generateEndpoints({ + apiFile: './fixtures/emptyApi.ts', + schemaFile: resolve(__dirname, 'fixtures/petstore.json'), + filterEndpoints: ['getPetById', 'addPet'], + hooks: true, + }); + expect(api).toContain('useGetPetByIdQuery'); + expect(api).toContain('useAddPetMutation'); + expect(api).toMatchSnapshot( + 'should generate an `useGetPetByIdQuery` query hook and an `useAddPetMutation` mutation hook' + ); +}); + +test('hooks generation uses overrides', async () => { + const api = await generateEndpoints({ + apiFile: './fixtures/emptyApi.ts', + schemaFile: resolve(__dirname, 'fixtures/petstore.json'), + filterEndpoints: 'loginUser', + endpointOverrides: [ + { + pattern: 'loginUser', + type: 'mutation', + }, + ], + hooks: true, + }); + expect(api).not.toContain('useLoginUserQuery'); + expect(api).toContain('useLoginUserMutation'); + expect(api).toMatchSnapshot('should generate an `useLoginMutation` mutation hook'); +}); + +test('should use brackets in a querystring urls arg, when the arg contains full stops', async () => { + const api = await generateEndpoints({ + apiFile: './fixtures/emptyApi.ts', + schemaFile: resolve(__dirname, 'fixtures/params.json'), + }); + expect(api).toContain('`/api/v1/list/${queryArg["item.id"]}`'); + expect(api).toMatchSnapshot(); +}); diff --git a/test/jest.setup.ts b/test/jest.setup.ts index 36f92d8..464008f 100644 --- a/test/jest.setup.ts +++ b/test/jest.setup.ts @@ -1,5 +1,5 @@ global.fetch = require('node-fetch'); - +const { format } = require('prettier'); const { server } = require('./mocks/server'); beforeAll(() => server.listen({ onUnhandledRequest: 'error' })); @@ -12,3 +12,17 @@ expect.addSnapshotSerializer({ return val as string; }, }); + +expect.addSnapshotSerializer({ + serialize(val) { + return format(val, { + parser: 'typescript', + endOfLine: 'auto', + printWidth: 120, + semi: true, + singleQuote: true, + trailingComma: 'es5', + }); + }, + test: (val) => /injectEndpoints/.test(val), +}); diff --git a/test/tsconfig.json b/test/tsconfig.json index 4157c87..5ba34e0 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -1,7 +1,8 @@ { "compilerOptions": { "paths": { - "@/*": ["./test/fixtures/*"] + "@/*": ["./test/fixtures/*"], + "@rtk-incubator/rtk-query-codegen-openapi": ["./src"] }, "allowSyntheticDefaultImports": true, "esModuleInterop": true, @@ -13,6 +14,8 @@ "jsx": "react", "baseUrl": "..", "resolveJsonModule": true, - "types": ["node", "jest"] + "types": ["node", "jest"], + "allowJs": true, + "checkJs": true } } diff --git a/test/utils.test.ts b/test/utils.test.ts deleted file mode 100644 index ce0b50a..0000000 --- a/test/utils.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { stripFileExtension } from '../src/utils'; - -describe('stripFileExtension', () => { - it('should strip js and ts file extensions from possible input paths', async () => { - const testFileInputs = [ - './whatever/output.ts', - './whatever/output.js', - 'whatever/output.ts', - './whatever.ts/output.ts', - './no/file/name', - '../hello.ts', - 'banana.js', - 'banana', - './banana', - ]; - - for (const path of testFileInputs) { - const stripped = stripFileExtension(path); - const hasExt = stripped.endsWith('.js') || stripped.endsWith('.ts'); - - expect(hasExt).toBeFalsy(); - } - }); -});