diff --git a/.github/workflows/release-please.yaml b/.github/workflows/release-please.yaml index 2ff7e294..3ad22c79 100644 --- a/.github/workflows/release-please.yaml +++ b/.github/workflows/release-please.yaml @@ -6,6 +6,7 @@ on: permissions: contents: write pull-requests: write + packages: write name: Run Release Please jobs: @@ -27,7 +28,8 @@ jobs: if: ${{ steps.release.outputs.releases_created }} with: node-version: latest - registry-url: "https://registry.npmjs.org" + cache: npm + registry-url: 'https://npm.pkg.github.com' - name: Build Packages if: ${{ steps.release.outputs.releases_created }} run: | diff --git a/.gitignore b/.gitignore index d8b55c13..514d4243 100644 --- a/.gitignore +++ b/.gitignore @@ -184,4 +184,6 @@ $RECYCLE.BIN/ # End of https://www.toptal.com/developers/gitignore/api/windows,macos,node -.history \ No newline at end of file +.history + +.idea \ No newline at end of file diff --git a/cli/package.json b/cli/package.json index 275f9a00..501b4711 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,5 +1,5 @@ { - "name": "@openapi-codegen/cli", + "name": "@chatloop/openapi-codegen-cli", "version": "2.0.2", "description": "OpenAPI Codegen cli", "main": "lib/index.js", @@ -9,7 +9,7 @@ "openapi-codegen": "lib/cli.js" }, "repository": { - "url": "https://github.com/fabien0102/openapi-codegen", + "url": "https://github.com/chatloop/openapi-codegen", "directory": "cli" }, "publishConfig": { diff --git a/cli/src/core/parseOpenAPISourceFile.ts b/cli/src/core/parseOpenAPISourceFile.ts index 8240c67f..675169be 100644 --- a/cli/src/core/parseOpenAPISourceFile.ts +++ b/cli/src/core/parseOpenAPISourceFile.ts @@ -1,4 +1,4 @@ -import { OpenAPIObject } from "openapi3-ts"; +import { OpenAPIObject } from "openapi3-ts/oas31"; import type { OpenAPISourceFile } from "../types"; @@ -20,7 +20,7 @@ export const parseOpenAPISourceFile = ({ const schema = format === "yaml" ? YAML.load(text) : JSON.parse(text); return new Promise((resolve, reject) => { - if (!schema.openapi || !schema.openapi.startsWith("3.0")) { + if (!schema.openapi || !schema.openapi.startsWith("3")) { swagger2openapi.convertObj(schema, {}, (err, convertedObj) => { if (err) { reject(err); diff --git a/cli/src/fixtures/petstore-expanded.ts b/cli/src/fixtures/petstore-expanded.ts index be77c503..6c1251b9 100644 --- a/cli/src/fixtures/petstore-expanded.ts +++ b/cli/src/fixtures/petstore-expanded.ts @@ -1,7 +1,7 @@ -import { OpenAPIObject } from "openapi3-ts"; +import { OpenAPIObject } from "openapi3-ts/oas31"; export const petstore: OpenAPIObject = { - openapi: "3.0.0", + openapi: "3.1.0", info: { version: "1.0.0", title: "Swagger Petstore", diff --git a/cli/src/types/index.ts b/cli/src/types/index.ts index f3493736..5a17f484 100644 --- a/cli/src/types/index.ts +++ b/cli/src/types/index.ts @@ -1,4 +1,4 @@ -import { OpenAPIObject } from "openapi3-ts"; +import { OpenAPIObject } from "openapi3-ts/oas31"; /** * OpenAPI source file. diff --git a/cli/src/types/swagger2openapi.d.ts b/cli/src/types/swagger2openapi.d.ts index 68a3f784..d78b5301 100644 --- a/cli/src/types/swagger2openapi.d.ts +++ b/cli/src/types/swagger2openapi.d.ts @@ -1,11 +1,11 @@ declare module "swagger2openapi" { - import { OpenAPIObject } from "openapi3-ts"; + import { OpenAPIObject } from "openapi3-ts/oas31"; interface ConverObjCallbackData { openapi: OpenAPIObject; } function convertObj( schema: unknown, options: {}, - callback: (err: Error, data: ConverObjCallbackData) => void + callback: (err: Error, data: ConverObjCallbackData) => void, ): void; } diff --git a/package-lock.json b/package-lock.json index dbf0db1e..f486dd88 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,10 +12,14 @@ "cli", "plugins/typescript" ], + "dependencies": { + "openapi3-ts": "^4.3.1" + }, "devDependencies": { "@swc/jest": "^0.2.36", "@types/jest": "^29.5.12", "@types/node": "^20.11.30", + "@typescript-eslint/eslint-plugin": "^7.9.0", "all-contributors-cli": "^6.26.1", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", @@ -26,7 +30,8 @@ "lerna": "^8.1.2", "prettier": "^3.2.5", "pretty-quick": "^4.0.0", - "typescript": "5.4.3" + "typescript": "5.4.3", + "typescript-eslint": "^7.9.0" }, "engines": { "node": ">=16" @@ -34,7 +39,7 @@ }, "cli": { "name": "@openapi-codegen/cli", - "version": "2.0.1", + "version": "2.0.2", "license": "MIT", "dependencies": { "@apollo/client": "^3.5.10", @@ -137,6 +142,14 @@ "url": "https://github.com/sindresorhus/got?sponsor=1" } }, + "cli/node_modules/openapi3-ts": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-2.0.2.tgz", + "integrity": "sha512-TxhYBMoqx9frXyOgnRHufjQfPXomTIHYKhSKJ6jHfj13kS8OEIhvmE8CTuQyKtjjWttAjX5DPxM1vmalEpo8Qw==", + "dependencies": { + "yaml": "^1.10.2" + } + }, "cli/node_modules/rxjs": { "version": "7.5.5", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.5.tgz", @@ -6437,6 +6450,215 @@ "resolved": "https://registry.npmjs.org/@types/yoga-layout/-/yoga-layout-1.9.2.tgz", "integrity": "sha512-S9q47ByT2pPvD65IvrWp7qppVMpk9WGMbVq9wbWZOHg6tnXSD4vyhao6nOSBwwfDdV2p3Kx9evA9vI+XWTfDvw==" }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.9.0.tgz", + "integrity": "sha512-6e+X0X3sFe/G/54aC3jt0txuMTURqLyekmEHViqyA2VnxhLMpvA6nqmcjIy+Cr9tLDHPssA74BP5Mx9HQIxBEA==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.9.0", + "@typescript-eslint/type-utils": "7.9.0", + "@typescript-eslint/utils": "7.9.0", + "@typescript-eslint/visitor-keys": "7.9.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.9.0.tgz", + "integrity": "sha512-qHMJfkL5qvgQB2aLvhUSXxbK7OLnDkwPzFalg458pxQgfxKDfT1ZDbHQM/I6mDIf/svlMkj21kzKuQ2ixJlatQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "7.9.0", + "@typescript-eslint/types": "7.9.0", + "@typescript-eslint/typescript-estree": "7.9.0", + "@typescript-eslint/visitor-keys": "7.9.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.9.0.tgz", + "integrity": "sha512-ZwPK4DeCDxr3GJltRz5iZejPFAAr4Wk3+2WIBaj1L5PYK5RgxExu/Y68FFVclN0y6GGwH8q+KgKRCvaTmFBbgQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.9.0", + "@typescript-eslint/visitor-keys": "7.9.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.9.0.tgz", + "integrity": "sha512-6Qy8dfut0PFrFRAZsGzuLoM4hre4gjzWJB6sUvdunCYZsYemTkzZNwF1rnGea326PHPT3zn5Lmg32M/xfJfByA==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "7.9.0", + "@typescript-eslint/utils": "7.9.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.9.0.tgz", + "integrity": "sha512-oZQD9HEWQanl9UfsbGVcZ2cGaR0YT5476xfWE0oE5kQa2sNK2frxOlkeacLOTh9po4AlUT5rtkGyYM5kew0z5w==", + "dev": true, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.9.0.tgz", + "integrity": "sha512-zBCMCkrb2YjpKV3LA0ZJubtKCDxLttxfdGmwZvTqqWevUPN0FZvSI26FalGFFUZU/9YQK/A4xcQF9o/VVaCKAg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.9.0", + "@typescript-eslint/visitor-keys": "7.9.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.9.0.tgz", + "integrity": "sha512-5KVRQCzZajmT4Ep+NEgjXCvjuypVvYHUW7RHlXzNPuak2oWpVoD1jf5xCP0dPAuNIchjC7uQyvbdaSTFaLqSdA==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.9.0", + "@typescript-eslint/types": "7.9.0", + "@typescript-eslint/typescript-estree": "7.9.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.9.0.tgz", + "integrity": "sha512-iESPx2TNLDNGQLyjKhUvIKprlP49XNEK+MvIf9nIO7ZZaZdbnfWKHnXAgufpxqfA0YryH8XToi4+CjBgVnFTSQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.9.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", @@ -16055,11 +16277,22 @@ } }, "node_modules/openapi3-ts": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-2.0.1.tgz", - "integrity": "sha512-v6X3iwddhi276siej96jHGIqTx3wzVfMTmpGJEQDt7GPI7pI6sywItURLzpEci21SBRpPN/aOWSF5mVfFVNmcg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-4.3.1.tgz", + "integrity": "sha512-ha/kTOLhMQL7MvS9Abu/cpCXx5qwHQ++88YkUzn1CGfmM8JvCOG/4ZE6tRsexgXRFaoJrcwLyf81H2Y/CXALtA==", "dependencies": { - "yaml": "^1.10.0" + "yaml": "^2.4.1" + } + }, + "node_modules/openapi3-ts/node_modules/yaml": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.2.tgz", + "integrity": "sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" } }, "node_modules/optimism": { @@ -19137,6 +19370,18 @@ "node": ">=8" } }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, "node_modules/ts-invariant": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.10.3.tgz", @@ -19509,6 +19754,32 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-7.9.0.tgz", + "integrity": "sha512-7iTn9c10teHHCys5Ud/yaJntXZrjt3h2mrx3feJGBOLgQkF3TB1X89Xs3aVQ/GgdXRAXpk2bPTdpRwHP4YkUow==", + "dev": true, + "dependencies": { + "@typescript-eslint/eslint-plugin": "7.9.0", + "@typescript-eslint/parser": "7.9.0", + "@typescript-eslint/utils": "7.9.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/ua-parser-js": { "version": "0.7.37", "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.37.tgz", @@ -20138,12 +20409,12 @@ }, "plugins/typescript": { "name": "@openapi-codegen/typescript", - "version": "8.0.0", + "version": "8.0.2", "license": "MIT", "dependencies": { "case": "^1.6.3", "lodash": "^4.17.21", - "openapi3-ts": "^2.0.1", + "openapi3-ts": "^4.3.1", "pluralize": "^8.0.0", "tslib": "^2.3.1", "tsutils": "^3.21.0", @@ -24211,6 +24482,14 @@ "responselike": "^3.0.0" } }, + "openapi3-ts": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-2.0.2.tgz", + "integrity": "sha512-TxhYBMoqx9frXyOgnRHufjQfPXomTIHYKhSKJ6jHfj13kS8OEIhvmE8CTuQyKtjjWttAjX5DPxM1vmalEpo8Qw==", + "requires": { + "yaml": "^1.10.2" + } + }, "rxjs": { "version": "7.5.5", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.5.tgz", @@ -24239,7 +24518,7 @@ "@types/qs": "^6.9.7", "case": "^1.6.3", "lodash": "^4.17.21", - "openapi3-ts": "^2.0.1", + "openapi3-ts": "^4.3.1", "pluralize": "^8.0.0", "tslib": "^2.3.1", "tsutils": "^3.21.0", @@ -25103,6 +25382,122 @@ "resolved": "https://registry.npmjs.org/@types/yoga-layout/-/yoga-layout-1.9.2.tgz", "integrity": "sha512-S9q47ByT2pPvD65IvrWp7qppVMpk9WGMbVq9wbWZOHg6tnXSD4vyhao6nOSBwwfDdV2p3Kx9evA9vI+XWTfDvw==" }, + "@typescript-eslint/eslint-plugin": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.9.0.tgz", + "integrity": "sha512-6e+X0X3sFe/G/54aC3jt0txuMTURqLyekmEHViqyA2VnxhLMpvA6nqmcjIy+Cr9tLDHPssA74BP5Mx9HQIxBEA==", + "dev": true, + "requires": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.9.0", + "@typescript-eslint/type-utils": "7.9.0", + "@typescript-eslint/utils": "7.9.0", + "@typescript-eslint/visitor-keys": "7.9.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + } + }, + "@typescript-eslint/parser": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.9.0.tgz", + "integrity": "sha512-qHMJfkL5qvgQB2aLvhUSXxbK7OLnDkwPzFalg458pxQgfxKDfT1ZDbHQM/I6mDIf/svlMkj21kzKuQ2ixJlatQ==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "7.9.0", + "@typescript-eslint/types": "7.9.0", + "@typescript-eslint/typescript-estree": "7.9.0", + "@typescript-eslint/visitor-keys": "7.9.0", + "debug": "^4.3.4" + } + }, + "@typescript-eslint/scope-manager": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.9.0.tgz", + "integrity": "sha512-ZwPK4DeCDxr3GJltRz5iZejPFAAr4Wk3+2WIBaj1L5PYK5RgxExu/Y68FFVclN0y6GGwH8q+KgKRCvaTmFBbgQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "7.9.0", + "@typescript-eslint/visitor-keys": "7.9.0" + } + }, + "@typescript-eslint/type-utils": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.9.0.tgz", + "integrity": "sha512-6Qy8dfut0PFrFRAZsGzuLoM4hre4gjzWJB6sUvdunCYZsYemTkzZNwF1rnGea326PHPT3zn5Lmg32M/xfJfByA==", + "dev": true, + "requires": { + "@typescript-eslint/typescript-estree": "7.9.0", + "@typescript-eslint/utils": "7.9.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + } + }, + "@typescript-eslint/types": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.9.0.tgz", + "integrity": "sha512-oZQD9HEWQanl9UfsbGVcZ2cGaR0YT5476xfWE0oE5kQa2sNK2frxOlkeacLOTh9po4AlUT5rtkGyYM5kew0z5w==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.9.0.tgz", + "integrity": "sha512-zBCMCkrb2YjpKV3LA0ZJubtKCDxLttxfdGmwZvTqqWevUPN0FZvSI26FalGFFUZU/9YQK/A4xcQF9o/VVaCKAg==", + "dev": true, + "requires": { + "@typescript-eslint/types": "7.9.0", + "@typescript-eslint/visitor-keys": "7.9.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "@typescript-eslint/utils": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.9.0.tgz", + "integrity": "sha512-5KVRQCzZajmT4Ep+NEgjXCvjuypVvYHUW7RHlXzNPuak2oWpVoD1jf5xCP0dPAuNIchjC7uQyvbdaSTFaLqSdA==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.9.0", + "@typescript-eslint/types": "7.9.0", + "@typescript-eslint/typescript-estree": "7.9.0" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.9.0.tgz", + "integrity": "sha512-iESPx2TNLDNGQLyjKhUvIKprlP49XNEK+MvIf9nIO7ZZaZdbnfWKHnXAgufpxqfA0YryH8XToi4+CjBgVnFTSQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "7.9.0", + "eslint-visitor-keys": "^3.4.3" + } + }, "@ungap/structured-clone": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", @@ -32341,11 +32736,18 @@ } }, "openapi3-ts": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-2.0.1.tgz", - "integrity": "sha512-v6X3iwddhi276siej96jHGIqTx3wzVfMTmpGJEQDt7GPI7pI6sywItURLzpEci21SBRpPN/aOWSF5mVfFVNmcg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-4.3.1.tgz", + "integrity": "sha512-ha/kTOLhMQL7MvS9Abu/cpCXx5qwHQ++88YkUzn1CGfmM8JvCOG/4ZE6tRsexgXRFaoJrcwLyf81H2Y/CXALtA==", "requires": { - "yaml": "^1.10.0" + "yaml": "^2.4.1" + }, + "dependencies": { + "yaml": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.2.tgz", + "integrity": "sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==" + } } }, "optimism": { @@ -34718,6 +35120,13 @@ "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", "dev": true }, + "ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "requires": {} + }, "ts-invariant": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.10.3.tgz", @@ -34995,6 +35404,17 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz", "integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==" }, + "typescript-eslint": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-7.9.0.tgz", + "integrity": "sha512-7iTn9c10teHHCys5Ud/yaJntXZrjt3h2mrx3feJGBOLgQkF3TB1X89Xs3aVQ/GgdXRAXpk2bPTdpRwHP4YkUow==", + "dev": true, + "requires": { + "@typescript-eslint/eslint-plugin": "7.9.0", + "@typescript-eslint/parser": "7.9.0", + "@typescript-eslint/utils": "7.9.0" + } + }, "ua-parser-js": { "version": "0.7.37", "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.37.tgz", diff --git a/package.json b/package.json index f1d8b2f6..1f6f49df 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@swc/jest": "^0.2.36", "@types/jest": "^29.5.12", "@types/node": "^20.11.30", + "@typescript-eslint/eslint-plugin": "^7.9.0", "all-contributors-cli": "^6.26.1", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", @@ -32,7 +33,8 @@ "lerna": "^8.1.2", "prettier": "^3.2.5", "pretty-quick": "^4.0.0", - "typescript": "5.4.3" + "typescript": "5.4.3", + "typescript-eslint": "^7.9.0" }, "jest": { "projects": [ @@ -66,5 +68,8 @@ ] } ] + }, + "dependencies": { + "openapi3-ts": "^4.3.1" } } diff --git a/plugins/typescript/package.json b/plugins/typescript/package.json index 847fb5c1..b7d7f39b 100644 --- a/plugins/typescript/package.json +++ b/plugins/typescript/package.json @@ -1,11 +1,11 @@ { - "name": "@openapi-codegen/typescript", + "name": "@chatloop/openapi-codegen-typescript", "version": "8.0.2", "description": "OpenAPI Codegen typescript generators", "main": "lib/index.js", "types": "lib/index.d.ts", "repository": { - "url": "https://github.com/fabien0102/openapi-codegen", + "url": "https://github.com/chatloop/openapi-codegen", "directory": "plugins/typescript" }, "publishConfig": { @@ -30,7 +30,7 @@ "dependencies": { "case": "^1.6.3", "lodash": "^4.17.21", - "openapi3-ts": "^2.0.1", + "openapi3-ts": "^4.3.1", "pluralize": "^8.0.0", "tslib": "^2.3.1", "tsutils": "^3.21.0", diff --git a/plugins/typescript/src/core/createOperationFetcherFnNodes.ts b/plugins/typescript/src/core/createOperationFetcherFnNodes.ts index 4b843187..d0d84a37 100644 --- a/plugins/typescript/src/core/createOperationFetcherFnNodes.ts +++ b/plugins/typescript/src/core/createOperationFetcherFnNodes.ts @@ -1,4 +1,4 @@ -import { OperationObject } from "openapi3-ts"; +import { OperationObject } from "openapi3-ts/oas31"; import ts, { factory as f } from "typescript"; import { camelizedPathParams } from "./camelizedPathParams"; @@ -59,7 +59,7 @@ export const createOperationFetcherFnNodes = ({ f.createIdentifier("variables"), undefined, variablesType, - undefined + undefined, ), f.createParameterDeclaration( undefined, @@ -67,8 +67,8 @@ export const createOperationFetcherFnNodes = ({ f.createIdentifier("signal"), f.createToken(ts.SyntaxKind.QuestionToken), f.createTypeReferenceNode( - f.createIdentifier("AbortSignal") - ) + f.createIdentifier("AbortSignal"), + ), ), ] : [ @@ -78,8 +78,8 @@ export const createOperationFetcherFnNodes = ({ f.createIdentifier("signal"), f.createToken(ts.SyntaxKind.QuestionToken), f.createTypeReferenceNode( - f.createIdentifier("AbortSignal") - ) + f.createIdentifier("AbortSignal"), + ), ), ], undefined, @@ -99,37 +99,37 @@ export const createOperationFetcherFnNodes = ({ [ f.createPropertyAssignment( f.createIdentifier("url"), - f.createStringLiteral(camelizedPathParams(url)) + f.createStringLiteral(camelizedPathParams(url)), ), f.createPropertyAssignment( f.createIdentifier("method"), - f.createStringLiteral(verb) + f.createStringLiteral(verb), ), ...(variablesType.kind !== ts.SyntaxKind.VoidKeyword ? [ f.createSpreadAssignment( - f.createIdentifier("variables") + f.createIdentifier("variables"), ), f.createShorthandPropertyAssignment( - f.createIdentifier("signal") + f.createIdentifier("signal"), ), ] : [ f.createShorthandPropertyAssignment( - f.createIdentifier("signal") + f.createIdentifier("signal"), ), ]), ], - false + false, ), - ] - ) - ) + ], + ), + ), ), ], - ts.NodeFlags.Const - ) - ) + ts.NodeFlags.Const, + ), + ), ); return nodes; }; diff --git a/plugins/typescript/src/core/createOperationQueryFnNodes.ts b/plugins/typescript/src/core/createOperationQueryFnNodes.ts index b14d388e..3b3256e5 100644 --- a/plugins/typescript/src/core/createOperationQueryFnNodes.ts +++ b/plugins/typescript/src/core/createOperationQueryFnNodes.ts @@ -1,5 +1,5 @@ import { camelCase } from "lodash"; -import { OperationObject } from "openapi3-ts"; +import { OperationObject } from "openapi3-ts/oas31"; import ts, { factory as f } from "typescript"; import { camelizedPathParams } from "./camelizedPathParams"; @@ -63,16 +63,16 @@ export const createOperationQueryFnNodes = ({ f.createIdentifier("variables"), undefined, variablesType, - undefined + undefined, ), ], f.createTupleTypeNode([ f.createTypeReferenceNode( f.createQualifiedName( f.createIdentifier("reactQuery"), - f.createIdentifier("QueryKey") + f.createIdentifier("QueryKey"), ), - undefined + undefined, ), f.createFunctionTypeNode( undefined, @@ -86,7 +86,7 @@ export const createOperationQueryFnNodes = ({ undefined, undefined, f.createIdentifier("signal"), - undefined + undefined, ), ]), undefined, @@ -97,11 +97,11 @@ export const createOperationQueryFnNodes = ({ f.createToken(ts.SyntaxKind.QuestionToken), f.createTypeReferenceNode( f.createIdentifier("AbortSignal"), - undefined - ) + undefined, + ), ), ]), - undefined + undefined, ), ] : [ @@ -113,13 +113,13 @@ export const createOperationQueryFnNodes = ({ undefined, undefined, f.createIdentifier("variables"), - undefined + undefined, ), f.createBindingElement( undefined, undefined, f.createIdentifier("signal"), - undefined + undefined, ), ]), undefined, @@ -128,7 +128,7 @@ export const createOperationQueryFnNodes = ({ undefined, f.createIdentifier("variables"), undefined, - variablesType + variablesType, ), f.createPropertySignature( undefined, @@ -136,16 +136,16 @@ export const createOperationQueryFnNodes = ({ f.createToken(ts.SyntaxKind.QuestionToken), f.createTypeReferenceNode( f.createIdentifier("AbortSignal"), - undefined - ) + undefined, + ), ), ]), - undefined + undefined, ), ], f.createTypeReferenceNode(f.createIdentifier("Promise"), [ dataType, - ]) + ]), ), ]), f.createToken(ts.SyntaxKind.EqualsGreaterThanToken), @@ -159,20 +159,20 @@ export const createOperationQueryFnNodes = ({ [ f.createPropertyAssignment( f.createIdentifier("path"), - f.createStringLiteral(camelizedPathParams(url)) + f.createStringLiteral(camelizedPathParams(url)), ), f.createPropertyAssignment( f.createIdentifier("operationId"), - f.createStringLiteral(operationId) + f.createStringLiteral(operationId), ), f.createShorthandPropertyAssignment( f.createIdentifier("variables"), - undefined + undefined, ), ], - true + true, ), - ] + ], ), f.createArrowFunction( [f.createModifier(ts.SyntaxKind.AsyncKeyword)], @@ -187,7 +187,7 @@ export const createOperationQueryFnNodes = ({ undefined, undefined, f.createIdentifier("signal"), - undefined + undefined, ), ]), undefined, @@ -198,11 +198,11 @@ export const createOperationQueryFnNodes = ({ f.createToken(ts.SyntaxKind.QuestionToken), f.createTypeReferenceNode( f.createIdentifier("AbortSignal"), - undefined - ) + undefined, + ), ), ]), - undefined + undefined, ), ] : [ @@ -214,13 +214,13 @@ export const createOperationQueryFnNodes = ({ undefined, undefined, f.createIdentifier("variables"), - undefined + undefined, ), f.createBindingElement( undefined, undefined, f.createIdentifier("signal"), - undefined + undefined, ), ]), undefined, @@ -229,7 +229,7 @@ export const createOperationQueryFnNodes = ({ undefined, f.createIdentifier("variables"), undefined, - variablesType + variablesType, ), f.createPropertySignature( undefined, @@ -237,11 +237,11 @@ export const createOperationQueryFnNodes = ({ f.createToken(ts.SyntaxKind.QuestionToken), f.createTypeReferenceNode( f.createIdentifier("AbortSignal"), - undefined - ) + undefined, + ), ), ]), - undefined + undefined, ), ], undefined, @@ -253,24 +253,24 @@ export const createOperationQueryFnNodes = ({ f.createObjectLiteralExpression( [ f.createSpreadAssignment( - f.createIdentifier("variables") + f.createIdentifier("variables"), ), ], - false + false, ), f.createIdentifier("signal"), - ] - ) + ], + ), ), ], - true - ) - ) + true, + ), + ), ), ], - ts.NodeFlags.Const - ) - ) + ts.NodeFlags.Const, + ), + ), ); return nodes; }; diff --git a/plugins/typescript/src/core/createWatermark.ts b/plugins/typescript/src/core/createWatermark.ts index 8b53b9b5..5f337eff 100644 --- a/plugins/typescript/src/core/createWatermark.ts +++ b/plugins/typescript/src/core/createWatermark.ts @@ -1,4 +1,4 @@ -import { InfoObject } from "openapi3-ts"; +import { InfoObject } from "openapi3-ts/oas31"; import { factory as f } from "typescript"; /** @@ -12,6 +12,6 @@ export const createWatermark = (info: InfoObject) => f.createJSDocPropertyTag( f.createIdentifier("version"), f.createIdentifier(info.version), - false + false, ), ]); diff --git a/plugins/typescript/src/core/determineComponentForOperations.ts b/plugins/typescript/src/core/determineComponentForOperations.ts new file mode 100644 index 00000000..0a931167 --- /dev/null +++ b/plugins/typescript/src/core/determineComponentForOperations.ts @@ -0,0 +1,56 @@ +import { OpenAPIObject, PathItemObject } from "openapi3-ts/oas31"; +import { isOperationObject } from "./isOperationObject"; +import { isVerb } from "./isVerb"; +import { isJsonApiOperationPaginated } from "./isJsonApiResponsePaginated"; + +export const determineComponentForOperations = ( + openAPIDocument: OpenAPIObject, +) => { + const operationIds: string[] = []; + + openAPIDocument.paths && + Object.entries(openAPIDocument.paths).forEach( + ([route, verbs]: [string, PathItemObject]) => { + Object.entries(verbs).forEach(([verb, operation]) => { + if (!isVerb(verb) || !isOperationObject(operation)) return; + const operationId = operation.operationId; + + if (operationIds.includes(operationId)) { + throw new Error( + `The operationId "${operation.operationId}" is duplicated in your schema definition!`, + ); + } + operationIds.push(operationId); + const isPaginated = isJsonApiOperationPaginated( + operation, + openAPIDocument, + ); + + const component: "useQuery" | "useMutate" | "useInfiniteQuery" = + operation["x-openapi-codegen-component"] || + (verb === "get" + ? isPaginated + ? "useInfiniteQuery" + : "useQuery" + : "useMutate"); + + if ( + !["useQuery", "useMutate", "useInfiniteQuery"].includes(component) + ) { + throw new Error(`[x-openapi-codegen-component] Invalid value for ${operation.operationId} operation + Valid options: "useMutate", "useQuery", "useInfiniteQuery"`); + } + + if (component === "useInfiniteQuery" && !isPaginated) { + throw new Error( + `[x-openapi-codegen-component] Invalid value for ${operation.operationId} operation, the does not appear to be paginated, its missing pagination query parameters`, + ); + } + + operation["x-openapi-codegen-component"] = component; + }); + }, + ); + + return openAPIDocument; +}; diff --git a/plugins/typescript/src/core/findCompatibleMediaType.ts b/plugins/typescript/src/core/findCompatibleMediaType.ts index efa32cae..1bde89a0 100644 --- a/plugins/typescript/src/core/findCompatibleMediaType.ts +++ b/plugins/typescript/src/core/findCompatibleMediaType.ts @@ -2,7 +2,7 @@ import { MediaTypeObject, RequestBodyObject, ResponseObject, -} from "openapi3-ts"; +} from "openapi3-ts/oas31"; /** * Returns the first compatible media type. @@ -11,13 +11,14 @@ import { * @returns */ export const findCompatibleMediaType = ( - requestBodyOrResponseObject: RequestBodyObject | ResponseObject + requestBodyOrResponseObject: RequestBodyObject | ResponseObject, ): MediaTypeObject | undefined => { if (!requestBodyOrResponseObject.content) return; for (let contentType of Object.keys(requestBodyOrResponseObject.content)) { if ( contentType.startsWith("*/*") || contentType.startsWith("application/json") || + contentType.startsWith("application/vnd.api+json") || contentType.startsWith("application/octet-stream") || contentType.startsWith("multipart/form-data") ) { diff --git a/plugins/typescript/src/core/getDataResponseType.test.ts b/plugins/typescript/src/core/getDataResponseType.test.ts index 8063c852..3ac4598c 100644 --- a/plugins/typescript/src/core/getDataResponseType.test.ts +++ b/plugins/typescript/src/core/getDataResponseType.test.ts @@ -1,4 +1,4 @@ -import { ResponsesObject } from "openapi3-ts"; +import { ResponsesObject } from "openapi3-ts/oas31"; import { print } from "../testUtils"; import { getDataResponseType } from "./getDataResponseType"; @@ -112,7 +112,7 @@ describe("getDataResponseType", () => { }); expect(print(responseType)).toMatchInlineSnapshot( - `"Schemas.Pet[] | Schemas.Cat[]"` + `"Schemas.Pet[] | Schemas.Cat[]"`, ); }); diff --git a/plugins/typescript/src/core/getDataResponseType.ts b/plugins/typescript/src/core/getDataResponseType.ts index 021df3cb..f45d6e16 100644 --- a/plugins/typescript/src/core/getDataResponseType.ts +++ b/plugins/typescript/src/core/getDataResponseType.ts @@ -5,7 +5,7 @@ import { ReferenceObject, ResponseObject, ResponsesObject, -} from "openapi3-ts"; +} from "openapi3-ts/oas31"; import { uniqBy } from "lodash"; import { pascal } from "case"; @@ -20,27 +20,30 @@ export const getDataResponseType = ({ components, printNodes, }: { - responses: ResponsesObject; + responses?: ResponsesObject; components?: ComponentsObject; printNodes: (nodes: ts.Node[]) => string; }) => { + if (responses === undefined) { + return f.createKeywordTypeNode(ts.SyntaxKind.UndefinedKeyword); + } const responseTypes = uniqBy( Object.entries(responses).reduce( ( mem, - [statusCode, response]: [string, ResponseObject | ReferenceObject] + [statusCode, response]: [string, ResponseObject | ReferenceObject], ) => { if (!statusCode.startsWith("2")) return mem; if (isReferenceObject(response)) { const [hash, topLevel, namespace, name] = response.$ref.split("/"); if (hash !== "#" || topLevel !== "components") { throw new Error( - "This library only resolve $ref that are include into `#/components/*` for now" + "This library only resolve $ref that are include into `#/components/*` for now", ); } if (namespace !== "responses") { throw new Error( - "$ref for responses must be on `#/components/responses`" + "$ref for responses must be on `#/components/responses`", ); } return [ @@ -48,9 +51,9 @@ export const getDataResponseType = ({ f.createTypeReferenceNode( f.createQualifiedName( f.createIdentifier("Responses"), - f.createIdentifier(pascal(name)) + f.createIdentifier(pascal(name)), ), - undefined + undefined, ), ]; } @@ -66,14 +69,14 @@ export const getDataResponseType = ({ }), ]; }, - [] as ts.TypeNode[] + [] as ts.TypeNode[], ), - (node) => printNodes([node]) + (node) => printNodes([node]), ); return responseTypes.length === 0 ? f.createKeywordTypeNode(ts.SyntaxKind.UndefinedKeyword) : responseTypes.length === 1 - ? responseTypes[0] - : f.createUnionTypeNode(responseTypes); + ? responseTypes[0] + : f.createUnionTypeNode(responseTypes); }; diff --git a/plugins/typescript/src/core/getErrorResponseType.test.ts b/plugins/typescript/src/core/getErrorResponseType.test.ts index f7360d31..0dacd0be 100644 --- a/plugins/typescript/src/core/getErrorResponseType.test.ts +++ b/plugins/typescript/src/core/getErrorResponseType.test.ts @@ -1,4 +1,4 @@ -import { ResponseObject } from "openapi3-ts"; +import { ResponseObject } from "openapi3-ts/oas31"; import { print } from "../testUtils"; import { getErrorResponseType } from "./getErrorResponseType"; @@ -13,8 +13,8 @@ describe("getErrorResponseType", () => { }, printNodes: (nodes) => nodes.map(print).join("\n"), - }) - ) + }), + ), ).toMatchInlineSnapshot(` "Fetcher.ErrorWrapper<{ status: 500; @@ -33,8 +33,8 @@ describe("getErrorResponseType", () => { }, printNodes: (nodes) => nodes.map(print).join("\n"), - }) - ) + }), + ), ).toMatchInlineSnapshot(` "Fetcher.ErrorWrapper<{ status: 404; @@ -52,8 +52,8 @@ describe("getErrorResponseType", () => { getErrorResponseType({ responses: {}, printNodes: (nodes) => nodes.map(print).join("\n"), - }) - ) + }), + ), ).toEqual("Fetcher.ErrorWrapper"); }); @@ -66,8 +66,8 @@ describe("getErrorResponseType", () => { }, printNodes: (nodes) => nodes.map(print).join("\n"), - }) - ) + }), + ), ).toMatchInlineSnapshot(` "Fetcher.ErrorWrapper<{ status: ClientErrorStatus | ServerErrorStatus; @@ -86,8 +86,8 @@ describe("getErrorResponseType", () => { }, printNodes: (nodes) => nodes.map(print).join("\n"), - }) - ) + }), + ), ).toMatchInlineSnapshot(` "Fetcher.ErrorWrapper<{ status: ClientErrorStatus; @@ -109,8 +109,8 @@ describe("getErrorResponseType", () => { }, printNodes: (nodes) => nodes.map(print).join("\n"), - }) - ) + }), + ), ).toMatchInlineSnapshot(` "Fetcher.ErrorWrapper<{ status: ServerErrorStatus; @@ -132,8 +132,8 @@ describe("getErrorResponseType", () => { }, printNodes: (nodes) => nodes.map(print).join("\n"), - }) - ) + }), + ), ).toMatchInlineSnapshot(` "Fetcher.ErrorWrapper<{ status: 422; @@ -156,8 +156,8 @@ describe("getErrorResponseType", () => { }, printNodes: (nodes) => nodes.map(print).join("\n"), - }) - ) + }), + ), ).toMatchInlineSnapshot(` "Fetcher.ErrorWrapper<{ status: 501; @@ -185,8 +185,8 @@ describe("getErrorResponseType", () => { }, printNodes: (nodes) => nodes.map(print).join("\n"), - }) - ) + }), + ), ).toMatchInlineSnapshot(` "Fetcher.ErrorWrapper<{ status: 422; diff --git a/plugins/typescript/src/core/getErrorResponseType.ts b/plugins/typescript/src/core/getErrorResponseType.ts index 4b53a28c..6e0a21d0 100644 --- a/plugins/typescript/src/core/getErrorResponseType.ts +++ b/plugins/typescript/src/core/getErrorResponseType.ts @@ -5,7 +5,7 @@ import { ReferenceObject, ResponseObject, ResponsesObject, -} from "openapi3-ts"; +} from "openapi3-ts/oas31"; import { findCompatibleMediaType } from "./findCompatibleMediaType"; import { getType } from "./schemaToTypeAliasDeclaration"; @@ -22,28 +22,31 @@ export const getErrorResponseType = ({ components, printNodes, }: { - responses: ResponsesObject; + responses?: ResponsesObject; components?: ComponentsObject; printNodes: (nodes: ts.Node[]) => string; }) => { + if (responses === undefined) { + return f.createKeywordTypeNode(ts.SyntaxKind.UndefinedKeyword); + } const status = Object.keys(responses); const responseTypes = Object.entries(responses).reduce( ( mem, - [statusCode, response]: [string, ResponseObject | ReferenceObject] + [statusCode, response]: [string, ResponseObject | ReferenceObject], ) => { if (statusCode.startsWith("2")) return mem; if (isReferenceObject(response)) { const [hash, topLevel, namespace, name] = response.$ref.split("/"); if (hash !== "#" || topLevel !== "components") { throw new Error( - "This library only resolve $ref that are include into `#/components/*` for now" + "This library only resolve $ref that are include into `#/components/*` for now", ); } if (namespace !== "responses") { throw new Error( - "$ref for responses must be on `#/components/responses`" + "$ref for responses must be on `#/components/responses`", ); } return [ @@ -53,11 +56,11 @@ export const getErrorResponseType = ({ f.createTypeReferenceNode( f.createQualifiedName( f.createIdentifier("Responses"), - f.createIdentifier(pascal(name)) + f.createIdentifier(pascal(name)), ), - undefined + undefined, ), - status + status, ), ]; } @@ -73,37 +76,39 @@ export const getErrorResponseType = ({ currentComponent: null, openAPIDocument: { components }, }), - status + status, ), ]; }, - [] as ts.TypeNode[] + [] as ts.TypeNode[], ); return f.createTypeReferenceNode("Fetcher.ErrorWrapper", [ responseTypes.length === 0 ? f.createKeywordTypeNode(ts.SyntaxKind.UndefinedKeyword) : responseTypes.length === 1 - ? responseTypes[0] - : f.createUnionTypeNode(responseTypes), + ? responseTypes[0] + : f.createUnionTypeNode(responseTypes), ]); }; const createStatusDeclaration = ( statusCode: string, type: ts.TypeNode, - status: string[] + status: string[], ): ts.TypeNode => { let statusType: ts.TypeNode = f.createLiteralTypeNode( - f.createNumericLiteral(statusCode) + f.createNumericLiteral(statusCode), ); if ( statusCode === "4xx" || - (statusCode === "default" && status.includes("5xx")) + statusCode === "4XX" || + (statusCode === "default" && + (status.includes("5xx") || status.includes("5XX"))) ) { const usedClientCode = status.filter( - (s) => s.startsWith("4") && s !== "4xx" + (s) => s.startsWith("4") && s !== "4xx" && s !== "4XX", ); if (usedClientCode.length > 0) { statusType = f.createTypeReferenceNode("Exclude", [ @@ -112,8 +117,8 @@ const createStatusDeclaration = ( ? f.createLiteralTypeNode(f.createNumericLiteral(usedClientCode[0])) : f.createUnionTypeNode( usedClientCode.map((code) => - f.createLiteralTypeNode(f.createNumericLiteral(code)) - ) + f.createLiteralTypeNode(f.createNumericLiteral(code)), + ), ), ]); } else { @@ -123,10 +128,12 @@ const createStatusDeclaration = ( if ( statusCode === "5xx" || - (statusCode === "default" && status.includes("4xx")) + statusCode === "5XX" || + (statusCode === "default" && + (status.includes("4xx") || status.includes("4XX"))) ) { const usedServerCode = status.filter( - (s) => s.startsWith("5") && s !== "5xx" + (s) => s.startsWith("5") && s !== "5xx" && s !== "5XX", ); if (usedServerCode.length > 0) { statusType = f.createTypeReferenceNode("Exclude", [ @@ -135,8 +142,8 @@ const createStatusDeclaration = ( ? f.createLiteralTypeNode(f.createNumericLiteral(usedServerCode[0])) : f.createUnionTypeNode( usedServerCode.map((code) => - f.createLiteralTypeNode(f.createNumericLiteral(code)) - ) + f.createLiteralTypeNode(f.createNumericLiteral(code)), + ), ), ]); } else { @@ -147,7 +154,9 @@ const createStatusDeclaration = ( if ( statusCode === "default" && !status.includes("4xx") && - !status.includes("5xx") + !status.includes("4XX") && + !status.includes("5xx") && + !status.includes("5XX") ) { const otherCodes = status.filter((s) => s !== "default"); if (otherCodes.length > 0) { @@ -160,8 +169,8 @@ const createStatusDeclaration = ( ? f.createLiteralTypeNode(f.createNumericLiteral(otherCodes[0])) : f.createUnionTypeNode( otherCodes.map((code) => - f.createLiteralTypeNode(f.createNumericLiteral(code)) - ) + f.createLiteralTypeNode(f.createNumericLiteral(code)), + ), ), ]); } else { @@ -174,8 +183,8 @@ const createStatusDeclaration = ( if ( statusCode === "default" && - status.includes("4xx") && - status.includes("5xx") + (status.includes("4xx") || status.includes("4XX")) && + (status.includes("5xx") || status.includes("5XX")) ) { statusType = f.createKeywordTypeNode(ts.SyntaxKind.NeverKeyword); } diff --git a/plugins/typescript/src/core/getJsonApiResponseResource.ts b/plugins/typescript/src/core/getJsonApiResponseResource.ts new file mode 100644 index 00000000..7613fa00 --- /dev/null +++ b/plugins/typescript/src/core/getJsonApiResponseResource.ts @@ -0,0 +1,97 @@ +import { + isReferenceObject, + OpenAPIObject, + ReferenceObject, + ResponseObject, + ResponsesObject, + SchemaObject, +} from "openapi3-ts/oas31"; +import { getReferenceSchema } from "./getReferenceSchema"; +import { isJsonApiResourceSchema } from "./isJsonApiResourceSchema"; + +export type JsonApiResponseResource = { + resourceType: string; + isArray: boolean; +}; + +/** + * Extract the json api resource type from success responses (2xx) + */ +export const getJsonApiResponseResource = ( + responses: ResponsesObject | undefined, + openApiDocument: OpenAPIObject, +): JsonApiResponseResource | undefined => { + if (responses === undefined) { + return undefined; + } + + let resourceType: JsonApiResponseResource | undefined = undefined; + + for (const [statusCode, response] of Object.entries(responses)) { + if (!statusCode.startsWith("2")) continue; + + let responseSchema: ResponseObject | ReferenceObject = response; + if (isReferenceObject(responseSchema)) { + responseSchema = getReferenceSchema( + response.$ref, + openApiDocument, + ) as ResponseObject; + } + + if (!responseSchema.content) continue; + const jsonApiContent = Object.keys(responseSchema.content).find( + (contentType) => contentType.startsWith("application/vnd.api+json"), + ); + if (!jsonApiContent) continue; + + let responseContentSchema = response.content[jsonApiContent].schema; + if (!responseContentSchema) continue; + + if (isReferenceObject(responseContentSchema)) { + responseContentSchema = getReferenceSchema( + responseContentSchema.$ref, + openApiDocument, + ); + } + + const type = getJsonApiResourceType(responseContentSchema, openApiDocument); + if (type) { + resourceType = type; + break; + } + } + return resourceType; +}; + +export const getJsonApiResourceType = ( + schema: SchemaObject, + openApiDocument: OpenAPIObject, +): JsonApiResponseResource | undefined => { + if ( + schema.properties === undefined || + schema.properties["data"] === undefined + ) + return undefined; + + let isArray = false; + + let dataSchema = isReferenceObject(schema.properties["data"]) + ? getReferenceSchema(schema.properties["data"].$ref, openApiDocument) + : schema.properties["data"]; + + if (dataSchema.type === "array" && dataSchema.items !== undefined) { + dataSchema = isReferenceObject(dataSchema.items) + ? getReferenceSchema(dataSchema.items.$ref, openApiDocument) + : dataSchema.items; + isArray = true; + } + const resourceSchema = isJsonApiResourceSchema(dataSchema, openApiDocument); + if (!resourceSchema) return undefined; + + const resourceType = + (resourceSchema.properties.type.enum && + resourceSchema.properties.type.enum[0]) || + resourceSchema.properties.type.const; + + return resourceType ? { resourceType, isArray } : undefined; +}; diff --git a/plugins/typescript/src/core/getOperationTypes.test.ts b/plugins/typescript/src/core/getOperationTypes.test.ts index 989c22a3..dcc241fc 100644 --- a/plugins/typescript/src/core/getOperationTypes.test.ts +++ b/plugins/typescript/src/core/getOperationTypes.test.ts @@ -1,6 +1,6 @@ import { omit } from "lodash"; import ts, { factory } from "typescript"; -import { OperationObject } from "openapi3-ts"; +import { OperationObject } from "openapi3-ts/oas31"; import { petstore } from "../fixtures/petstore"; import { getOperationTypes } from "./getOperationTypes"; @@ -30,7 +30,7 @@ describe("getOperationTypes", () => { openAPIDocument: petstore, printNodes: () => "", variablesExtraPropsType: factory.createKeywordTypeNode( - ts.SyntaxKind.VoidKeyword + ts.SyntaxKind.VoidKeyword, ), }); @@ -46,7 +46,7 @@ describe("getOperationTypes", () => { operationId: "listPet", operation: omit( petstore.paths["/pets"].get, - "parameters" + "parameters", ) as OperationObject, openAPIDocument: petstore, printNodes: () => "", @@ -54,7 +54,7 @@ describe("getOperationTypes", () => { }); expect(print(output.declarationNodes[2])).toMatchInlineSnapshot( - `"export type ListPetVariables = ExtraProps;"` + `"export type ListPetVariables = ExtraProps;"`, ); }); @@ -63,12 +63,12 @@ describe("getOperationTypes", () => { operationId: "listPet", operation: omit( petstore.paths["/pets"].get, - "parameters" + "parameters", ) as OperationObject, openAPIDocument: petstore, printNodes: () => "", variablesExtraPropsType: factory.createKeywordTypeNode( - ts.SyntaxKind.VoidKeyword + ts.SyntaxKind.VoidKeyword, ), }); diff --git a/plugins/typescript/src/core/getOperationTypes.ts b/plugins/typescript/src/core/getOperationTypes.ts index ee2a3898..610d1200 100644 --- a/plugins/typescript/src/core/getOperationTypes.ts +++ b/plugins/typescript/src/core/getOperationTypes.ts @@ -1,5 +1,9 @@ import { pascal } from "case"; -import { OpenAPIObject, OperationObject, PathItemObject } from "openapi3-ts"; +import { + OpenAPIObject, + OperationObject, + PathItemObject, +} from "openapi3-ts/oas31"; import ts, { factory as f } from "typescript"; import { getParamsGroupByType } from "./getParamsGroupByType"; @@ -69,7 +73,7 @@ export const getOperationTypes = ({ // Generate params types const { pathParams, queryParams, headerParams } = getParamsGroupByType( [...pathParameters, ...(operation.parameters || [])], - openAPIDocument.components + openAPIDocument.components, ); // Check if types can be marked as optional (all properties are optional) @@ -96,8 +100,8 @@ export const getOperationTypes = ({ { currentComponent: null, openAPIDocument, - } - ) + }, + ), ); } @@ -109,8 +113,8 @@ export const getOperationTypes = ({ { currentComponent: null, openAPIDocument, - } - ) + }, + ), ); } @@ -122,8 +126,8 @@ export const getOperationTypes = ({ { currentComponent: null, openAPIDocument, - } - ) + }, + ), ); } @@ -134,8 +138,8 @@ export const getOperationTypes = ({ [f.createModifier(ts.SyntaxKind.ExportKeyword)], f.createIdentifier(errorTypeIdentifier), undefined, - errorType - ) + errorType, + ), ); errorType = f.createTypeReferenceNode(errorTypeIdentifier); @@ -148,8 +152,8 @@ export const getOperationTypes = ({ [f.createModifier(ts.SyntaxKind.ExportKeyword)], f.createIdentifier(dataTypeIdentifier), undefined, - dataType - ) + dataType, + ), ); dataType = f.createTypeReferenceNode(dataTypeIdentifier); @@ -163,8 +167,8 @@ export const getOperationTypes = ({ [f.createModifier(ts.SyntaxKind.ExportKeyword)], f.createIdentifier(requestBodyIdentifier), undefined, - requestBodyType - ) + requestBodyType, + ), ); requestBodyType = f.createTypeReferenceNode(requestBodyIdentifier); @@ -215,8 +219,8 @@ export const getOperationTypes = ({ [f.createModifier(ts.SyntaxKind.ExportKeyword)], f.createIdentifier(variablesIdentifier), undefined, - variablesType - ) + variablesType, + ), ); variablesType = f.createTypeReferenceNode(variablesIdentifier); diff --git a/plugins/typescript/src/core/getParamsGroupByType.ts b/plugins/typescript/src/core/getParamsGroupByType.ts index 4c681382..b3bdbb94 100644 --- a/plugins/typescript/src/core/getParamsGroupByType.ts +++ b/plugins/typescript/src/core/getParamsGroupByType.ts @@ -4,7 +4,7 @@ import { isReferenceObject, OperationObject, ParameterObject, -} from "openapi3-ts"; +} from "openapi3-ts/oas31"; /** * Resolve $ref and group parameters by `type`. @@ -14,7 +14,7 @@ import { */ export const getParamsGroupByType = ( parameters: OperationObject["parameters"] = [], - components: ComponentsObject = {} + components: ComponentsObject = {}, ) => { const { query: queryParams = [] as ParameterObject[], @@ -25,7 +25,7 @@ export const getParamsGroupByType = ( if (isReferenceObject(p)) { const schema = get( components, - p.$ref.replace("#/components/", "").split("/") + p.$ref.replace("#/components/", "").split("/"), ); if (!schema) { throw new Error(`${p.$ref} not found!`); @@ -35,7 +35,7 @@ export const getParamsGroupByType = ( return p; } }), - "in" + "in", ); return { queryParams, pathParams, headerParams }; diff --git a/plugins/typescript/src/core/getReferenceSchema.test.ts b/plugins/typescript/src/core/getReferenceSchema.test.ts index bae88485..a64eab03 100644 --- a/plugins/typescript/src/core/getReferenceSchema.test.ts +++ b/plugins/typescript/src/core/getReferenceSchema.test.ts @@ -1,81 +1,78 @@ import { getReferenceSchema } from "./getReferenceSchema"; -import type { OpenAPIObject, SchemasObject } from "openapi3-ts"; +import type { OpenAPIObject } from "openapi3-ts/oas31"; +import { ReferenceObject, SchemaObject } from "openapi3-ts/src/model/openapi31"; type OpenAPIDocWithComponents = Pick; -const schemas: SchemasObject = { - "Pet": { - "type": "object", - "description": "Pet", - "required": [ - "id", - "name" - ], - "properties": { - "id": { - "type": "integer", - "format": "int64" +const schemas: { [schema: string]: SchemaObject | ReferenceObject } = { + Pet: { + type: "object", + description: "Pet", + required: ["id", "name"], + properties: { + id: { + type: "integer", + format: "int64", }, - "name": { - "type": "string" + name: { + type: "string", }, - "tag": { - "type": "string" - } - } + tag: { + type: "string", + }, + }, }, "Pet.With.Dot": { - "type": "object", - "description": "Pet.With.Dot", - "required": [ - "id", - "name" - ], - "properties": { - "id": { - "type": "integer", - "format": "int64" + type: "object", + description: "Pet.With.Dot", + required: ["id", "name"], + properties: { + id: { + type: "integer", + format: "int64", + }, + name: { + type: "string", }, - "name": { - "type": "string" + tag: { + type: "string", }, - "tag": { - "type": "string" - } - } + }, }, - "PetRef": { - $ref: "#/components/schemas/Pet" + PetRef: { + $ref: "#/components/schemas/Pet", }, }; const document: OpenAPIDocWithComponents = { components: { - schemas - } + schemas, + }, }; const base$Ref = "#/components/schemas"; -describe('getReferenceSchema', () => { - it('should return the SchemaObject from $ref with a nested leaf path', () => { +describe("getReferenceSchema", () => { + it("should return the SchemaObject from $ref with a nested leaf path", () => { const $ref = `${base$Ref}/Pet`; const schema = getReferenceSchema($ref, document); expect(schema).toBeDefined(); expect(schema.description).toBe(schemas.Pet.description); }); - it('should return the SchemaObject from $ref with a dot-separated leaf path', () => { + it("should return the SchemaObject from $ref with a dot-separated leaf path", () => { const $ref = `${base$Ref}/Pet.With.Dot`; const schema = getReferenceSchema($ref, document); expect(schema).toBeDefined(); - expect(schema.description).toBe(schemas['Pet.With.Dot'].description); + expect(schema.description).toBe(schemas["Pet.With.Dot"].description); }); - it('should throw an Error if the $ref cannot be found', () => { + it("should throw an Error if the $ref cannot be found", () => { const $ref = `${base$Ref}/does/not/exist`; - expect(() => getReferenceSchema($ref, document)).toThrow(new RegExp($ref, 'g')); + expect(() => getReferenceSchema($ref, document)).toThrow( + new RegExp($ref, "g"), + ); }); it("should resolve the schema if the $ref has a nested $ref", () => { @@ -88,4 +85,4 @@ describe('getReferenceSchema', () => { it("should throw an error if he resulting schema is not a valid SchemaObject", () => { // TODO: not sure how to produce this scenario }); -}); \ No newline at end of file +}); diff --git a/plugins/typescript/src/core/getReferenceSchema.ts b/plugins/typescript/src/core/getReferenceSchema.ts index 87fb9797..139201f2 100644 --- a/plugins/typescript/src/core/getReferenceSchema.ts +++ b/plugins/typescript/src/core/getReferenceSchema.ts @@ -4,7 +4,7 @@ import { isSchemaObject, OpenAPIObject, SchemaObject, -} from "openapi3-ts"; +} from "openapi3-ts/oas31"; /** * Get the SchemaObject from a $ref. @@ -15,7 +15,7 @@ import { */ export const getReferenceSchema = ( $ref: string, - openAPIDocument: Pick + openAPIDocument: Pick, ): SchemaObject => { const [hash, ...refPath] = $ref.split("/"); if (hash !== "#") { @@ -28,10 +28,16 @@ export const getReferenceSchema = ( // get the last element of the refPath, [0] = 'components', [1] = 'schemas' const directSchemaName = refPath.at(-1); // try a direct access of the name from the schemas object - const defaultDirectSearch = openAPIDocument.components?.schemas && openAPIDocument.components.schemas[directSchemaName!]; + const defaultDirectSearch = + openAPIDocument.components?.schemas && + openAPIDocument.components.schemas[directSchemaName!]; // try to perform the typical ref path search but use the direct search as a fallback - const referenceSchema = get(openAPIDocument, refPath.join("."), defaultDirectSearch); + const referenceSchema = get( + openAPIDocument, + refPath.join("."), + defaultDirectSearch, + ); // if neither ref path nor direct search find the schema then throw that the ref cant be found if (!referenceSchema) { diff --git a/plugins/typescript/src/core/getRequestBodyType.ts b/plugins/typescript/src/core/getRequestBodyType.ts index b42ce9cd..52308f26 100644 --- a/plugins/typescript/src/core/getRequestBodyType.ts +++ b/plugins/typescript/src/core/getRequestBodyType.ts @@ -4,7 +4,7 @@ import { isReferenceObject, ReferenceObject, RequestBodyObject, -} from "openapi3-ts"; +} from "openapi3-ts/oas31"; import { pascal } from "case"; import { findCompatibleMediaType } from "./findCompatibleMediaType"; @@ -28,20 +28,20 @@ export const getRequestBodyType = ({ const [hash, topLevel, namespace, name] = requestBody.$ref.split("/"); if (hash !== "#" || topLevel !== "components") { throw new Error( - "This library only resolve $ref that are include into `#/components/*` for now" + "This library only resolve $ref that are include into `#/components/*` for now", ); } if (namespace !== "requestBodies") { throw new Error( - "$ref for requestBody must be on `#/components/requestBodies`" + "$ref for requestBody must be on `#/components/requestBodies`", ); } return f.createTypeReferenceNode( f.createQualifiedName( f.createIdentifier("RequestBodies"), - f.createIdentifier(pascal(name)) + f.createIdentifier(pascal(name)), ), - undefined + undefined, ); } @@ -54,7 +54,7 @@ export const getRequestBodyType = ({ const [hash, topLevel, namespace, name] = mediaType.$ref.split("/"); if (hash !== "#" || topLevel !== "components") { throw new Error( - "This library only resolve $ref that are include into `#/components/*` for now" + "This library only resolve $ref that are include into `#/components/*` for now", ); } if (namespace !== "schemas") { @@ -64,9 +64,9 @@ export const getRequestBodyType = ({ return f.createTypeReferenceNode( f.createQualifiedName( f.createIdentifier("Schemas"), - f.createIdentifier(pascal(name)) + f.createIdentifier(pascal(name)), ), - undefined + undefined, ); } diff --git a/plugins/typescript/src/core/getUsedImports.ts b/plugins/typescript/src/core/getUsedImports.ts index 91ee0aa0..776aaa4f 100644 --- a/plugins/typescript/src/core/getUsedImports.ts +++ b/plugins/typescript/src/core/getUsedImports.ts @@ -18,14 +18,14 @@ export const getUsedImports = ( schemas: string; parameters: string; responses: string; - utils: string; - } + utils?: string; + }, ): { keys: string[]; nodes: ts.Node[] } => { - const imports: Record< - keyof typeof files, - | { type: "namespace"; used: boolean; namespace: string; from: string } - | { type: "named"; used: boolean; imports: Set; from: string } - > = { + const imports: { + [key in keyof typeof files]: + | { type: "namespace"; used: boolean; namespace: string; from: string } + | { type: "named"; used: boolean; imports: Set; from: string }; + } = { parameters: { type: "namespace", used: false, @@ -50,30 +50,33 @@ export const getUsedImports = ( namespace: "Responses", from: files.responses, }, - utils: { + }; + if (files.utils) { + imports["utils"] = { type: "named", used: false, from: files.utils, imports: new Set(), - }, - }; - + }; + } const visitor: ts.Visitor = (node) => { if (ts.isQualifiedName(node)) { // We can’t use `node.left.getText()` because the node is not compiled (no internal `text` property) const text = camel(get(node.left, "escapedText", "") as string); if (text in imports) { - imports[text as keyof typeof imports].used = true; + imports[text as keyof typeof imports]!.used = true; } } - if (imports.utils.type === "named" && ts.isTypeReferenceNode(node)) { - if (get(node.typeName, "escapedText", "") === clientErrorStatus) { - imports.utils.used = true; - imports.utils.imports.add(clientErrorStatus); - } - if (get(node.typeName, "escapedText", "") === serverErrorStatus) { - imports.utils.used = true; - imports.utils.imports.add(serverErrorStatus); + if (imports.utils) { + if (imports.utils.type === "named" && ts.isTypeReferenceNode(node)) { + if (get(node.typeName, "escapedText", "") === clientErrorStatus) { + imports.utils.used = true; + imports.utils.imports.add(clientErrorStatus); + } + if (get(node.typeName, "escapedText", "") === serverErrorStatus) { + imports.utils.used = true; + imports.utils.imports.add(serverErrorStatus); + } } } return node.forEachChild(visitor); @@ -89,7 +92,11 @@ export const getUsedImports = ( if (i.type === "namespace") { return createNamespaceImport(i.namespace, `./${i.from}`); } else { - return createNamedImport(Array.from(i.imports.values()), `./${i.from}`, true); + return createNamedImport( + Array.from(i.imports.values()), + `./${i.from}`, + true, + ); } }), }; diff --git a/plugins/typescript/src/core/isJsonApiResourceSchema.ts b/plugins/typescript/src/core/isJsonApiResourceSchema.ts new file mode 100644 index 00000000..0a2ee050 --- /dev/null +++ b/plugins/typescript/src/core/isJsonApiResourceSchema.ts @@ -0,0 +1,58 @@ +import { + isReferenceObject, + OpenAPIObject, + SchemaObject, +} from "openapi3-ts/oas31"; +import { getReferenceSchema } from "./getReferenceSchema"; + +export type JsonApiResourceSchema = SchemaObject & { + type: "object"; + properties: { + id: { + type: "string"; + }; + type: { + type: "string"; + const?: string; + enum?: string[]; + }; + attributes: Record; + }; +}; +export const isJsonApiResourceSchema = ( + schema: SchemaObject, + openAPIDocument: OpenAPIObject, +): false | JsonApiResourceSchema => { + if ( + schema.type === undefined || + schema.type !== "object" || + schema.properties === undefined || + schema.properties["id"] === undefined || + schema.properties["type"] === undefined || + schema.properties["attributes"] === undefined + ) { + return false; + } + let id = isReferenceObject(schema.properties["id"]) + ? getReferenceSchema(schema.properties["id"].$ref, openAPIDocument) + : schema.properties["id"]; + if (id.type === undefined || id.type !== "string") { + return false; + } + let type = isReferenceObject(schema.properties["type"]) + ? getReferenceSchema(schema.properties["type"].$ref, openAPIDocument) + : schema.properties["type"]; + if (type.type === undefined || type.type !== "string") { + return false; + } + let attributes = isReferenceObject(schema.properties["attributes"]) + ? getReferenceSchema(schema.properties["attributes"].$ref, openAPIDocument) + : schema.properties["attributes"]; + + return attributes.type !== undefined && attributes.type === "object" + ? ({ + ...schema, + properties: { id, type, attributes }, + } as JsonApiResourceSchema) + : false; +}; diff --git a/plugins/typescript/src/core/isJsonApiResponsePaginated.ts b/plugins/typescript/src/core/isJsonApiResponsePaginated.ts new file mode 100644 index 00000000..738531c3 --- /dev/null +++ b/plugins/typescript/src/core/isJsonApiResponsePaginated.ts @@ -0,0 +1,61 @@ +import { + isReferenceObject, + OpenAPIObject, + OperationObject, + ParameterObject, + ReferenceObject, + ResponseObject, +} from "openapi3-ts/oas31"; +import { getReferenceSchema } from "./getReferenceSchema"; + +/** + * Extract the json api resource type from success responses (2xx) + */ +export const isJsonApiOperationPaginated = ( + operation: OperationObject, + openApiDocument: OpenAPIObject, +) => { + if (operation.responses === undefined || operation.parameters === undefined) { + return false; + } + let hasJsonApiResponse = false; + for (const [statusCode, response] of Object.entries(operation.responses)) { + if (!statusCode.startsWith("2")) continue; + + let responseSchema: ResponseObject | ReferenceObject = response; + if (isReferenceObject(responseSchema)) { + responseSchema = getReferenceSchema( + response.$ref, + openApiDocument, + ) as ResponseObject; + } + + if (!responseSchema.content) continue; + const jsonApiContent = Object.keys(responseSchema.content).find( + (contentType) => contentType.startsWith("application/vnd.api+json"), + ); + if (!jsonApiContent) continue; + + hasJsonApiResponse = true; + break; + } + + if (!hasJsonApiResponse) { + return false; + } + + const parameters = operation.parameters + .map((parameter) => { + return isReferenceObject(parameter) + ? (getReferenceSchema( + parameter.$ref, + openApiDocument, + ) as ParameterObject) + : parameter; + }) + .filter( + (parameter) => + parameter.in === "query" && parameter.name.startsWith("page["), + ); + return parameters.length > 0; +}; diff --git a/plugins/typescript/src/core/isOperationObject.ts b/plugins/typescript/src/core/isOperationObject.ts index 52a58df0..f9839ef4 100644 --- a/plugins/typescript/src/core/isOperationObject.ts +++ b/plugins/typescript/src/core/isOperationObject.ts @@ -1,4 +1,4 @@ -import { OperationObject } from "openapi3-ts"; +import { OperationObject } from "openapi3-ts/oas31"; /** * Type guard for `OperationObject` @@ -6,6 +6,8 @@ import { OperationObject } from "openapi3-ts"; * @param obj */ export const isOperationObject = ( - obj: any -): obj is OperationObject & { operationId: string } => - typeof obj === "object" && typeof (obj as any).operationId === "string"; + obj: any, +): obj is OperationObject & { + operationId: string; + "x-openapi-codegen-component"?: "useQuery" | "useMutate" | "useInfiniteQuery"; +} => typeof obj === "object" && typeof (obj as any).operationId === "string"; diff --git a/plugins/typescript/src/core/isRequestBodyOptional.ts b/plugins/typescript/src/core/isRequestBodyOptional.ts index 4aea3c60..0c6a9014 100644 --- a/plugins/typescript/src/core/isRequestBodyOptional.ts +++ b/plugins/typescript/src/core/isRequestBodyOptional.ts @@ -6,7 +6,7 @@ import { OperationObject, ReferenceObject, RequestBodyObject, -} from "openapi3-ts"; +} from "openapi3-ts/oas31"; import { findCompatibleMediaType } from "./findCompatibleMediaType"; import { getReferenceSchema } from "./getReferenceSchema"; @@ -30,18 +30,18 @@ export const isRequestBodyOptional = ({ const [hash, topLevel, namespace, _name] = requestBody.$ref.split("/"); if (hash !== "#" || topLevel !== "components") { throw new Error( - "This library only resolve $ref that are include into `#/components/*` for now" + "This library only resolve $ref that are include into `#/components/*` for now", ); } if (namespace !== "requestBodies") { throw new Error( - "$ref for requestBody must be on `#/components/requestBodies`" + "$ref for requestBody must be on `#/components/requestBodies`", ); } const schema: RequestBodyObject | ReferenceObject = get( components, - requestBody.$ref.replace("#/components/", "").split("/") + requestBody.$ref.replace("#/components/", "").split("/"), ); if (!schema) { diff --git a/plugins/typescript/src/core/paramsToSchema.test.ts b/plugins/typescript/src/core/paramsToSchema.test.ts index 364a827a..152d4a1b 100644 --- a/plugins/typescript/src/core/paramsToSchema.test.ts +++ b/plugins/typescript/src/core/paramsToSchema.test.ts @@ -1,4 +1,4 @@ -import { ParameterObject } from "openapi3-ts"; +import { ParameterObject } from "openapi3-ts/oas31"; import { paramsToSchema } from "./paramsToSchema"; describe("paramsToSchema", () => { diff --git a/plugins/typescript/src/core/paramsToSchema.ts b/plugins/typescript/src/core/paramsToSchema.ts index d385ea4f..53df46d4 100644 --- a/plugins/typescript/src/core/paramsToSchema.ts +++ b/plugins/typescript/src/core/paramsToSchema.ts @@ -1,5 +1,5 @@ import { camel } from "case"; -import { ParameterObject, SchemaObject } from "openapi3-ts"; +import { ParameterObject, SchemaObject } from "openapi3-ts/oas31"; /** * Convert a list of params in an object schema. @@ -10,7 +10,7 @@ import { ParameterObject, SchemaObject } from "openapi3-ts"; */ export const paramsToSchema = ( params: ParameterObject[], - optionalKeys: string[] = [] + optionalKeys: string[] = [], ): SchemaObject => { const formatKey = params[0].in === "path" ? camel : (key: string) => key; return { diff --git a/plugins/typescript/src/core/schemaToEnumDeclaration.test.ts b/plugins/typescript/src/core/schemaToEnumDeclaration.test.ts index 586f77c6..197f8056 100644 --- a/plugins/typescript/src/core/schemaToEnumDeclaration.test.ts +++ b/plugins/typescript/src/core/schemaToEnumDeclaration.test.ts @@ -1,4 +1,4 @@ -import { OpenAPIObject, SchemaObject } from "openapi3-ts"; +import { OpenAPIObject, SchemaObject } from "openapi3-ts/oas31"; import ts from "typescript"; import { schemaToEnumDeclaration } from "./schemaToEnumDeclaration"; import { OpenAPIComponentType } from "./schemaToTypeAliasDeclaration"; @@ -19,6 +19,21 @@ describe("schemaToTypeAliasDeclaration", () => { `); }); + it("should quote string values starting with a digit", () => { + const schema: SchemaObject = { + type: "string", + enum: ["1", "1.0", "1.1.1"], + }; + + expect(printSchema(schema)).toMatchInlineSnapshot(` + "export enum Test { + "1" = "1", + "1.0" = "1.0", + "1.1.1" = "1.1.1" + }" + `); + }); + it("should generate a int enum", () => { const schema: SchemaObject = { type: "string", diff --git a/plugins/typescript/src/core/schemaToEnumDeclaration.ts b/plugins/typescript/src/core/schemaToEnumDeclaration.ts index f765324e..3cdb0eee 100644 --- a/plugins/typescript/src/core/schemaToEnumDeclaration.ts +++ b/plugins/typescript/src/core/schemaToEnumDeclaration.ts @@ -1,5 +1,5 @@ import { pascal } from "case"; -import { SchemaObject } from "openapi3-ts"; +import { SchemaObject } from "openapi3-ts/oas31"; import ts, { factory as f } from "typescript"; import { convertNumberToWord } from "../utils/getEnumProperties"; import { Context, getJSDocComment } from "./schemaToTypeAliasDeclaration"; @@ -14,14 +14,14 @@ import { Context, getJSDocComment } from "./schemaToTypeAliasDeclaration"; export const schemaToEnumDeclaration = ( name: string, schema: SchemaObject, - context: Context + context: Context, ): ts.Node[] => { const jsDocNode = getJSDocComment(schema, context); const members = getEnumMembers(schema, context); const declarationNode = f.createEnumDeclaration( [f.createModifier(ts.SyntaxKind.ExportKeyword)], pascal(name), - members + members, ); return jsDocNode ? [jsDocNode, declarationNode] : [declarationNode]; @@ -29,32 +29,38 @@ export const schemaToEnumDeclaration = ( function getEnumMembers( schema: SchemaObject, - context: Context + context: Context, ): ts.EnumMember[] { if (!schema.enum || !Array.isArray(schema.enum)) { throw new Error( - "The provided schema does not have an 'enum' property or it is not an array." + "The provided schema does not have an 'enum' property or it is not an array.", ); } - return schema.enum.map((enumValue, index) => { - let enumName: string; - let enumValueNode: ts.Expression | undefined = undefined; + return schema.enum + .map((enumValue, index) => { + let enumName: string; + let enumValueNode: ts.Expression | undefined = undefined; - if (typeof enumValue === "string") { - enumName = enumValue; - enumValueNode = f.createStringLiteral(enumValue); - } else if (typeof enumValue === "number") { - enumName = convertNumberToWord(enumValue) - .toUpperCase() - .replace(/[-\s]/g, "_"); - enumValueNode = f.createNumericLiteral(enumValue); - } else if (typeof enumValue === "boolean") { - enumName = enumValue ? "True" : "False"; - } else { - throw new Error(`Unsupported enum value type: ${typeof enumValue}`); - } + if (enumValue === null) { + return null; + } - return f.createEnumMember(f.createIdentifier(enumName), enumValueNode); - }); + if (typeof enumValue === "string") { + enumName = enumValue.match(/^\d/) ? `"${enumValue}"` : enumValue; + enumValueNode = f.createStringLiteral(enumValue); + } else if (typeof enumValue === "number") { + enumName = convertNumberToWord(enumValue) + .toUpperCase() + .replace(/[-\s]/g, "_"); + enumValueNode = f.createNumericLiteral(enumValue); + } else if (typeof enumValue === "boolean") { + enumName = enumValue ? "True" : "False"; + } else { + throw new Error(`Unsupported enum value type: ${typeof enumValue}`); + } + + return f.createEnumMember(f.createIdentifier(enumName), enumValueNode); + }) + .filter((member): member is ts.EnumMember => member !== null); } diff --git a/plugins/typescript/src/core/schemaToTypeAliasDeclaration.test.ts b/plugins/typescript/src/core/schemaToTypeAliasDeclaration.test.ts index 367dac31..4354f656 100644 --- a/plugins/typescript/src/core/schemaToTypeAliasDeclaration.test.ts +++ b/plugins/typescript/src/core/schemaToTypeAliasDeclaration.test.ts @@ -1,4 +1,8 @@ -import { OpenAPIObject, ReferenceObject, SchemaObject } from "openapi3-ts"; +import { + OpenAPIObject, + ReferenceObject, + SchemaObject, +} from "openapi3-ts/oas31"; import ts from "typescript"; import { OpenAPIComponentType, @@ -40,13 +44,20 @@ describe("schemaToTypeAliasDeclaration", () => { it("should generate a nullable value", () => { const schema: SchemaObject = { - type: "integer", - nullable: true, + type: ["integer", "null"], }; expect(printSchema(schema)).toBe("export type Test = number | null;"); }); + it("should generate a union value when type is an array", () => { + const schema: SchemaObject = { + type: ["integer", "string"], + }; + + expect(printSchema(schema)).toBe("export type Test = number | string;"); + }); + it("should generate an array of numbers", () => { const schema: SchemaObject = { type: "array", @@ -97,9 +108,8 @@ describe("schemaToTypeAliasDeclaration", () => { it("should generate nullable enums (strings)", () => { const schema: SchemaObject = { - type: "string", - enum: ["foo", "bar", "baz"], - nullable: true, + type: ["string", "null"], + enum: ["foo", "bar", "baz", null], }; expect(printSchema(schema)).toBe( @@ -159,8 +169,8 @@ describe("schemaToTypeAliasDeclaration", () => { default: "42", format: "int32", deprecated: true, - exclusiveMaximum: true, - exclusiveMinimum: false, + exclusiveMaximum: 43, + exclusiveMinimum: 42, example: "I’m an example", "x-test": "plop", }; @@ -174,8 +184,8 @@ describe("schemaToTypeAliasDeclaration", () => { * @default 42 * @format int32 * @deprecated true - * @exclusiveMaximum true - * @exclusiveMinimum false + * @exclusiveMaximum 43 + * @exclusiveMinimum 42 * @example I’m an example * @x-test plop */ @@ -268,7 +278,7 @@ describe("schemaToTypeAliasDeclaration", () => { expect(printSchema(schema)).toMatchInlineSnapshot(` "export type Test = { - ["foo.bar"]?: string; + "foo.bar"?: string; };" `); }); @@ -849,7 +859,6 @@ describe("schemaToTypeAliasDeclaration", () => { required: ["files"], }, ], - nullable: true, properties: { description: { description: "Description of the gist", @@ -869,8 +878,10 @@ describe("schemaToTypeAliasDeclaration", () => { maxProperties: 0, type: "object", }, + { + type: "null", + }, ], - nullable: true, properties: { content: { description: "The new content of the file", @@ -878,8 +889,7 @@ describe("schemaToTypeAliasDeclaration", () => { }, filename: { description: "The new filename for the file", - nullable: true, - type: "string", + type: ["string", "null"], }, }, type: "object", @@ -894,11 +904,11 @@ describe("schemaToTypeAliasDeclaration", () => { type: "object", }, }, - type: "object", + type: ["object", "null"], }; expect(printSchema(schema)).toMatchInlineSnapshot(` - "export type Test = { + "export type Test = ({ /** * Description of the gist * @@ -931,7 +941,7 @@ describe("schemaToTypeAliasDeclaration", () => { filename: string | null; } | {} | null; }; - } | { + } | null) | ({ /** * Description of the gist * @@ -964,14 +974,14 @@ describe("schemaToTypeAliasDeclaration", () => { filename: string | null; } | {} | null; }; - } | null;" + } | null);" `); }); }); }); const printSchema = ( - schema: SchemaObject, + schema: SchemaObject | ReferenceObject, currentComponent: OpenAPIComponentType = "schemas", components?: OpenAPIObject["components"], useEnums?: boolean, diff --git a/plugins/typescript/src/core/schemaToTypeAliasDeclaration.ts b/plugins/typescript/src/core/schemaToTypeAliasDeclaration.ts index cef90425..31f1ef02 100644 --- a/plugins/typescript/src/core/schemaToTypeAliasDeclaration.ts +++ b/plugins/typescript/src/core/schemaToTypeAliasDeclaration.ts @@ -8,9 +8,9 @@ import { SchemaObject, isReferenceObject, isSchemaObject, -} from "openapi3-ts"; +} from "openapi3-ts/oas31"; import { singular } from "pluralize"; -import { isValidIdentifier } from "tsutils"; +import { isValidPropertyName } from "tsutils"; import ts, { factory as f } from "typescript"; import { getReferenceSchema } from "./getReferenceSchema"; @@ -18,8 +18,8 @@ type RemoveIndex = { [P in keyof T as string extends P ? never : number extends P - ? never - : P]: T[P]; + ? never + : P]: T[P]; }; export type OpenAPIComponentType = Extract< @@ -48,9 +48,9 @@ let useEnumsConfigBase: boolean | undefined; */ export const schemaToTypeAliasDeclaration = ( name: string, - schema: SchemaObject, + schema: SchemaObject | ReferenceObject, context: Context, - useEnums?: boolean + useEnums?: boolean, ): ts.Node[] => { useEnumsConfigBase = useEnums; const jsDocNode = getJSDocComment(schema, context); @@ -58,7 +58,7 @@ export const schemaToTypeAliasDeclaration = ( [f.createModifier(ts.SyntaxKind.ExportKeyword)], pascal(name), undefined, - getType(schema, context, name) + getType(schema, context, name), ); return jsDocNode ? [jsDocNode, declarationNode] : [declarationNode]; @@ -74,13 +74,13 @@ export const getType = ( schema: SchemaObject | ReferenceObject, context: Context, name?: string, - isNodeEnum?: boolean + isNodeEnum?: boolean, ): ts.TypeNode => { if (isReferenceObject(schema)) { const [hash, topLevel, namespace, name] = schema.$ref.split("/"); if (hash !== "#" || topLevel !== "components") { throw new Error( - "This library only resolve $ref that are include into `#/components/*` for now" + "This library only resolve $ref that are include into `#/components/*` for now", ); } if (namespace === context.currentComponent) { @@ -90,8 +90,8 @@ export const getType = ( return f.createTypeReferenceNode( f.createQualifiedName( f.createIdentifier(pascal(namespace)), - f.createIdentifier(pascal(name)) - ) + f.createIdentifier(pascal(name)), + ), ); } @@ -100,61 +100,56 @@ export const getType = ( } if (schema.oneOf) { - return f.createUnionTypeNode([ - ...schema.oneOf.map((i) => + return f.createUnionTypeNode( + schema.oneOf.map((i) => withDiscriminator( - getType({ ...omit(schema, ["oneOf", "nullable"]), ...i }, context), + getType({ ...omit(schema, ["oneOf"]), ...i }, context), i, schema.discriminator, - context - ) + context, + ), ), - ...(schema.nullable ? [f.createLiteralTypeNode(f.createNull())] : []), - ]); + ); } if (schema.anyOf) { - return f.createUnionTypeNode([ - ...schema.anyOf.map((i) => + return f.createUnionTypeNode( + schema.anyOf.map((i) => withDiscriminator( - getType({ ...omit(schema, ["anyOf", "nullable"]), ...i }, context), + getType({ ...omit(schema, ["anyOf"]), ...i }, context), i, schema.discriminator, - context - ) + context, + ), ), - ...(schema.nullable ? [f.createLiteralTypeNode(f.createNull())] : []), - ]); + ); } if (schema.allOf) { const adHocSchemas: Array = []; if (schema.properties) { adHocSchemas.push({ - type: 'object', + type: "object", properties: schema.properties, - required: schema.required + required: schema.required, }); } if (schema.additionalProperties) { adHocSchemas.push({ - type: 'object', - additionalProperties: schema.additionalProperties + type: "object", + additionalProperties: schema.additionalProperties, }); } - return getAllOf([ - ...schema.allOf, - ...adHocSchemas - ], context); + return getAllOf([...schema.allOf, ...adHocSchemas], context); } if (schema.enum) { if (isNodeEnum) { - return f.createTypeReferenceNode(f.createIdentifier(name || "")); + return f.createTypeReferenceNode(f.createIdentifier(pascal(name || ""))); } - const unionTypes = f.createUnionTypeNode([ - ...schema.enum.map((value) => { + const unionTypes = f.createUnionTypeNode( + schema.enum.map((value) => { if (typeof value === "string") { return f.createLiteralTypeNode(f.createStringLiteral(value)); } @@ -163,13 +158,15 @@ export const getType = ( } if (typeof value === "boolean") { return f.createLiteralTypeNode( - value ? f.createTrue() : f.createFalse() + value ? f.createTrue() : f.createFalse(), ); } + if (value === null) { + return f.createLiteralTypeNode(f.createNull()); + } return f.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword); }), - ...(schema.nullable ? [f.createLiteralTypeNode(f.createNull())] : []), - ]); + ); return unionTypes; } @@ -183,119 +180,97 @@ export const getType = ( if (schema.items && !schema.type) { schema.type = "array"; } + if (schema.const) { + return f.createLiteralTypeNode(f.createStringLiteral(schema.const)); + } - switch (schema.type) { - case "null": - return f.createLiteralTypeNode(f.createNull()); - case "integer": - case "number": - return withNullable( - f.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), - schema.nullable - ); - case "string": - if (schema.format === "binary") { - return f.createTypeReferenceNode("Blob"); - } - return withNullable( - f.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), - schema.nullable - ); - case "boolean": - return withNullable( - f.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword), - schema.nullable - ); - case "object": - if (schema.maxProperties === 0) { - return withNullable(f.createTypeLiteralNode([]), schema.nullable); - } + if (typeof schema.type === "string") { + switch (schema.type) { + case "null": + return f.createLiteralTypeNode(f.createNull()); + case "integer": + case "number": + return f.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword); + case "string": + if (schema.format === "binary") { + return f.createTypeReferenceNode("Blob"); + } + return f.createKeywordTypeNode(ts.SyntaxKind.StringKeyword); + case "boolean": + return f.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword); + case "object": + if (schema.maxProperties === 0) { + return f.createTypeLiteralNode([]); + } - if ( - !schema.properties /* free form object */ && - !schema.additionalProperties - ) { - return withNullable( - f.createTypeReferenceNode(f.createIdentifier("Record"), [ + if ( + !schema.properties /* free form object */ && + !schema.additionalProperties + ) { + return f.createTypeReferenceNode(f.createIdentifier("Record"), [ f.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), f.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword), - ]), - schema.nullable - ); - } + ]); + } - const members: ts.TypeElement[] = Object.entries( - schema.properties || {} - ).map(([key, property]) => { - const isEnum = typeof property === "object" && "enum" in property && useEnumsConfigBase; - - const propertyNode = f.createPropertySignature( - undefined, - isValidIdentifier(key) - ? f.createIdentifier(key) - : f.createComputedPropertyName(f.createStringLiteral(key)), - schema.required?.includes(key) - ? undefined - : f.createToken(ts.SyntaxKind.QuestionToken), - getType( - property, - context, - `${name}${pascal(key)}`.replace(/[^a-zA-Z0-9 ]/g, ""), - isEnum - ) - ); - const jsDocNode = getJSDocComment(property, context); - if (jsDocNode) addJSDocToNode(propertyNode, jsDocNode); + const members: ts.TypeElement[] = Object.entries( + schema.properties || {}, + ).map(([key, property]) => { + const isEnum = + typeof property === "object" && + "enum" in property && + useEnumsConfigBase; + + const propertyNode = f.createPropertySignature( + undefined, + isValidPropertyName(key) ? key : f.createStringLiteral(key), + schema.required?.includes(key) + ? undefined + : f.createToken(ts.SyntaxKind.QuestionToken), + getType( + property, + context, + `${name}${pascal(key)}`.replace(/[^a-zA-Z0-9 ]/g, ""), + isEnum, + ), + ); + const jsDocNode = getJSDocComment(property, context); + if (jsDocNode) addJSDocToNode(propertyNode, jsDocNode); - return propertyNode; - }); + return propertyNode; + }); - const additionalPropertiesNode = getAdditionalProperties(schema, context); + const additionalPropertiesNode = getAdditionalProperties( + schema, + context, + ); - if (additionalPropertiesNode) { - return withNullable( - members.length > 0 + if (additionalPropertiesNode) { + return members.length > 0 ? f.createIntersectionTypeNode([ f.createTypeLiteralNode(members), f.createTypeLiteralNode([additionalPropertiesNode]), ]) - : f.createTypeLiteralNode([additionalPropertiesNode]), - schema.nullable - ); - } + : f.createTypeLiteralNode([additionalPropertiesNode]); + } - return withNullable(f.createTypeLiteralNode(members), schema.nullable); - case "array": - return withNullable( - f.createArrayTypeNode( + return f.createTypeLiteralNode(members); + case "array": + return f.createArrayTypeNode( !schema.items || Object.keys(schema.items).length === 0 ? f.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword) - : getType(schema.items, context) - ), - schema.nullable - ); - default: - return withNullable( - f.createKeywordTypeNode(ts.SyntaxKind.VoidKeyword), - schema.nullable - ); + : getType(schema.items, context), + ); + default: + return f.createKeywordTypeNode(ts.SyntaxKind.VoidKeyword); + } } -}; - -/** - * Add nullable option if needed. - * - * @param node Any node - * @param nullable Add nullable option if true - * @returns Type with or without nullable option - */ -const withNullable = ( - node: ts.TypeNode, - nullable: boolean | undefined -): ts.TypeNode => { - return nullable - ? f.createUnionTypeNode([node, f.createLiteralTypeNode(f.createNull())]) - : node; + if (schema.type) { + return f.createUnionTypeNode( + schema.type.map((type) => getType({ ...schema, type }, context)), + ); + } + return f.createKeywordTypeNode(ts.SyntaxKind.VoidKeyword); }; /** @@ -309,30 +284,32 @@ const withDiscriminator = ( node: ts.TypeNode, schema: SchemaObject | ReferenceObject, discriminator: DiscriminatorObject | undefined, - context: Context + context: Context, ): ts.TypeNode => { if (!discriminator || !discriminator.propertyName || !discriminator.mapping) { return node; } - const discriminatedValue = findKey( - discriminator.mapping, - (i) => i === schema.$ref - ); + const discriminatedValue = isReferenceObject(schema) + ? findKey(discriminator.mapping, (i) => i === schema.$ref) + : undefined; if (discriminatedValue) { const propertyNameAsLiteral = f.createTypeLiteralNode([ f.createPropertySignature( undefined, f.createIdentifier(discriminator.propertyName), undefined, - f.createLiteralTypeNode(f.createStringLiteral(discriminatedValue)) + f.createLiteralTypeNode(f.createStringLiteral(discriminatedValue)), ), ]); - const spec = get( - context.openAPIDocument, - schema.$ref.slice(2).replace(/\//g, ".") - ); + const spec = isReferenceObject(schema) + ? get( + context.openAPIDocument, + // @ts-expect-error overload incorrectly inferred from lodash typings + schema.$ref.slice(2).replace(/\//g, "."), + ) + : undefined; if (spec && isSchemaObject(spec) && spec.properties) { const property = spec.properties[discriminator.propertyName]; if ( @@ -354,9 +331,9 @@ const withDiscriminator = ( [ node, f.createLiteralTypeNode( - f.createStringLiteral(discriminator.propertyName) + f.createStringLiteral(discriminator.propertyName), ), - ] + ], ); return f.createIntersectionTypeNode([ @@ -373,7 +350,7 @@ const withDiscriminator = ( */ const getAllOf = ( members: Required["allOf"], - context: Context + context: Context, ): ts.TypeNode => { const initialValue = { isSchemaObjectOnly: true, @@ -399,9 +376,9 @@ const getAllOf = ( if (isSchemaObject(member)) { const { mergedSchema, isColliding } = mergeSchemas( acc.mergedSchema, - member + member, + context, ); - return { ...acc, mergedSchema, @@ -417,11 +394,12 @@ const getAllOf = ( if (isReferenceObject(member)) { const referenceSchema = getReferenceSchema( member.$ref, - context.openAPIDocument + context.openAPIDocument, ); const { mergedSchema, isColliding } = mergeSchemas( acc.mergedSchema, - referenceSchema + referenceSchema, + context, ); return { @@ -456,11 +434,13 @@ const getAllOf = ( * * @param a * @param b + * @param context * @returns the merged schema and a flag to know if the schema was colliding */ const mergeSchemas = ( a: SchemaObject, - b: SchemaObject + b: SchemaObject, + context: Context, ): { mergedSchema: SchemaObject; isColliding: boolean } => { if (Boolean(a.type) && Boolean(b.type) && a.type !== b.type) { return { @@ -478,7 +458,7 @@ const mergeSchemas = ( let isColliding = false; const properties = Object.entries(a.properties).reduce( (mergedProperties, [key, propertyA]) => { - const propertyB = b.properties?.[key]; + let propertyB = b.properties?.[key]; if (propertyB) { isColliding = true; } @@ -502,9 +482,32 @@ const mergeSchemas = ( }; } - return { ...mergedProperties, [key]: propertyA }; + if (propertyB) { + if (isReferenceObject(propertyA)) { + propertyA = getReferenceSchema( + propertyA.$ref, + context.openAPIDocument, + ); + } + if (isReferenceObject(propertyB)) { + propertyB = getReferenceSchema( + propertyB.$ref, + context.openAPIDocument, + ); + } + if (propertyB) { + return { + ...mergedProperties, + [key]: mergeSchemas(propertyA, propertyB, context).mergedSchema, + }; + } + } + return { + ...mergedProperties, + [key]: propertyA, + }; }, - {} as typeof a.properties + {} as typeof a.properties, ); return { @@ -565,20 +568,23 @@ const keysToExpressAsJsDocProperty: Array> = [ * @returns JSDoc node */ export const getJSDocComment = ( - schema: SchemaObject, - context: Context + schema: SchemaObject | ReferenceObject, + context: Context, ): ts.JSDoc | undefined => { + if (isReferenceObject(schema)) { + return undefined; + } // `allOf` can add some documentation to the schema, let’s merge all items as first step const schemaWithAllOfResolved = schema.allOf ? schema.allOf.reduce((mem, allOfItem) => { if (isReferenceObject(allOfItem)) { const referenceSchema = getReferenceSchema( allOfItem.$ref, - context.openAPIDocument + context.openAPIDocument, ); - return mergeSchemas(mem, referenceSchema).mergedSchema; + return mergeSchemas(mem, referenceSchema, context).mergedSchema; } else { - return mergeSchemas(mem, allOfItem).mergedSchema; + return mergeSchemas(mem, allOfItem, context).mergedSchema; } }, schema) : schema; @@ -611,7 +617,7 @@ export const getJSDocComment = ( .filter( ([key, value]) => keysToExpressAsJsDocProperty.includes(key as any) || - (/^x-/.exec(key) && typeof value !== "object") + (/^x-/.exec(key) && typeof value !== "object"), ) .forEach(([key, value]) => { if (Array.isArray(value)) { @@ -620,17 +626,17 @@ export const getJSDocComment = ( f.createJSDocPropertyTag( f.createIdentifier(singular(key)), getJsDocIdentifier(v), - false - ) - ) + false, + ), + ), ); } else if (typeof value !== "undefined") { propertyTags.push( f.createJSDocPropertyTag( f.createIdentifier(key), getJsDocIdentifier(value), - false - ) + false, + ), ); } }); @@ -641,7 +647,7 @@ export const getJSDocComment = ( ? schemaWithAllOfResolved.description.trim() + (propertyTags.length ? "\n" : "") : undefined, - propertyTags + propertyTags, ); } return undefined; @@ -659,7 +665,7 @@ const addJSDocToNode = (node: ts.Node, jsDocComment: ts.JSDoc) => { const sourceFile = ts.createSourceFile( "index.ts", "", - ts.ScriptTarget.Latest + ts.ScriptTarget.Latest, ); const printer = ts.createPrinter({ @@ -676,7 +682,7 @@ const addJSDocToNode = (node: ts.Node, jsDocComment: ts.JSDoc) => { node, ts.SyntaxKind.MultiLineCommentTrivia, "*" + jsDocString, // https://github.com/microsoft/TypeScript/issues/17146 - true + true, ); }; @@ -689,7 +695,7 @@ const addJSDocToNode = (node: ts.Node, jsDocComment: ts.JSDoc) => { */ const getAdditionalProperties = ( schema: SchemaObject, - context: Context + context: Context, ): ts.IndexSignatureDeclaration | undefined => { if (!schema.additionalProperties) return undefined; @@ -702,12 +708,12 @@ const getAdditionalProperties = ( f.createIdentifier("key"), undefined, f.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), - undefined + undefined, ), ], schema.additionalProperties === true || Object.keys(schema.additionalProperties).length === 0 ? f.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword) - : getType(schema.additionalProperties, context) + : getType(schema.additionalProperties, context), ); }; diff --git a/plugins/typescript/src/fixtures/petstore.ts b/plugins/typescript/src/fixtures/petstore.ts index 6e5e669b..41ead556 100644 --- a/plugins/typescript/src/fixtures/petstore.ts +++ b/plugins/typescript/src/fixtures/petstore.ts @@ -1,7 +1,7 @@ -import { OpenAPIObject } from "openapi3-ts"; +import { OpenAPIObject } from "openapi3-ts/oas31"; export const petstore: OpenAPIObject = { - openapi: "3.0.0", + openapi: "3.1.0", info: { version: "1.0.0", title: "Swagger Petstore", @@ -378,4 +378,4 @@ export const petstore: OpenAPIObject = { }, }, }, -}; +} as const; diff --git a/plugins/typescript/src/generators/generateFetchers.test.ts b/plugins/typescript/src/generators/generateFetchers.test.ts index df7ee263..119cf084 100644 --- a/plugins/typescript/src/generators/generateFetchers.test.ts +++ b/plugins/typescript/src/generators/generateFetchers.test.ts @@ -1,5 +1,5 @@ import { set } from "lodash"; -import { OpenAPIObject } from "openapi3-ts"; +import { OpenAPIObject } from "openapi3-ts/oas31"; import { Config, generateFetchers } from "./generateFetchers"; const config: Config = { diff --git a/plugins/typescript/src/generators/generateFetchers.ts b/plugins/typescript/src/generators/generateFetchers.ts index 7e5ce6f8..ea4a1601 100644 --- a/plugins/typescript/src/generators/generateFetchers.ts +++ b/plugins/typescript/src/generators/generateFetchers.ts @@ -3,7 +3,7 @@ import * as c from "case"; import { get } from "lodash"; import { ConfigBase, Context } from "./types"; -import { PathItemObject } from "openapi3-ts"; +import { PathItemObject } from "openapi3-ts/oas31"; import { getUsedImports } from "../core/getUsedImports"; import { createWatermark } from "../core/createWatermark"; @@ -39,7 +39,7 @@ export const generateFetchers = async (context: Context, config: Config) => { const sourceFile = ts.createSourceFile( "index.ts", "", - ts.ScriptTarget.Latest + ts.ScriptTarget.Latest, ); const printer = ts.createPrinter({ @@ -77,11 +77,11 @@ export const generateFetchers = async (context: Context, config: Config) => { const utilsFilename = formatFilename(filenamePrefix + "-utils"); const fetcherExtraPropsTypeName = `${c.pascal( - filenamePrefix + filenamePrefix, )}FetcherExtraProps`; let variablesExtraPropsType: ts.TypeNode = f.createKeywordTypeNode( - ts.SyntaxKind.VoidKeyword + ts.SyntaxKind.VoidKeyword, ); if (!context.existsFile(`${fetcherFilename}.ts`)) { @@ -90,7 +90,7 @@ export const generateFetchers = async (context: Context, config: Config) => { getFetcher({ prefix: filenamePrefix, baseUrl: get(context.openAPIDocument, "servers.0.url"), - }) + }), ); } else { const fetcherSourceText = await context.readFile(`${fetcherFilename}.ts`); @@ -98,7 +98,7 @@ export const generateFetchers = async (context: Context, config: Config) => { const fetcherSourceFile = ts.createSourceFile( `${fetcherFilename}.ts`, fetcherSourceText, - ts.ScriptTarget.Latest + ts.ScriptTarget.Latest, ); // Lookup for {prefix}FetcherExtraProps declaration @@ -111,7 +111,7 @@ export const generateFetchers = async (context: Context, config: Config) => { ) { // Use the type of defined variablesExtraPropsType = f.createTypeReferenceNode( - fetcherExtraPropsTypeName + fetcherExtraPropsTypeName, ); fetcherImports.push(fetcherExtraPropsTypeName); } @@ -121,45 +121,25 @@ export const generateFetchers = async (context: Context, config: Config) => { const operationIds: string[] = []; const operationByTags: Record = {}; - Object.entries(context.openAPIDocument.paths).forEach( - ([route, verbs]: [string, PathItemObject]) => { - Object.entries(verbs).forEach(([verb, operation]) => { - if (!isVerb(verb) || !isOperationObject(operation)) return; - const operationId = c.camel(operation.operationId); - if (operationIds.includes(operationId)) { - throw new Error( - `The operationId "${operation.operationId}" is duplicated in your schema definition!` - ); - } - - operationIds.push(operationId); - operation.tags?.forEach((tag) => { - if (!operationByTags[tag]) operationByTags[tag] = []; - operationByTags[tag].push(operationId); - }); - - const { - dataType, - errorType, - requestBodyType, - pathParamsType, - variablesType, - queryParamsType, - headersType, - declarationNodes, - } = getOperationTypes({ - openAPIDocument: context.openAPIDocument, - operation, - operationId, - printNodes, - injectedHeaders: config.injectedHeaders, - pathParameters: verbs.parameters, - variablesExtraPropsType, - }); - - nodes.push( - ...declarationNodes, - ...createOperationFetcherFnNodes({ + context.openAPIDocument.paths && + Object.entries(context.openAPIDocument.paths).forEach( + ([route, verbs]: [string, PathItemObject]) => { + Object.entries(verbs).forEach(([verb, operation]) => { + if (!isVerb(verb) || !isOperationObject(operation)) return; + const operationId = c.camel(operation.operationId); + if (operationIds.includes(operationId)) { + throw new Error( + `The operationId "${operation.operationId}" is duplicated in your schema definition!`, + ); + } + + operationIds.push(operationId); + operation.tags?.forEach((tag) => { + if (!operationByTags[tag]) operationByTags[tag] = []; + operationByTags[tag].push(operationId); + }); + + const { dataType, errorType, requestBodyType, @@ -167,16 +147,37 @@ export const generateFetchers = async (context: Context, config: Config) => { variablesType, queryParamsType, headersType, + declarationNodes, + } = getOperationTypes({ + openAPIDocument: context.openAPIDocument, operation, - fetcherFn, - url: route, - verb, - name: operationId, - }) - ); - }); - } - ); + operationId, + printNodes, + injectedHeaders: config.injectedHeaders, + pathParameters: verbs.parameters, + variablesExtraPropsType, + }); + + nodes.push( + ...declarationNodes, + ...createOperationFetcherFnNodes({ + dataType, + errorType, + requestBodyType, + pathParamsType, + variablesType, + queryParamsType, + headersType, + operation, + fetcherFn, + url: route, + verb, + name: operationId, + }), + ); + }); + }, + ); if (operationIds.length === 0) { console.log(`⚠️ You don't have any operation with "operationId" defined!`); @@ -198,17 +199,17 @@ export const generateFetchers = async (context: Context, config: Config) => { f.createStringLiteral(c.camel(tag)), f.createObjectLiteralExpression( operationIds.map((operationId) => - f.createShorthandPropertyAssignment(operationId) - ) - ) + f.createShorthandPropertyAssignment(operationId), + ), + ), ); - }) - ) + }), + ), ), ], - ts.NodeFlags.Const - ) - ) + ts.NodeFlags.Const, + ), + ), ); } @@ -217,7 +218,7 @@ export const generateFetchers = async (context: Context, config: Config) => { { ...config.schemasFiles, utils: utilsFilename, - } + }, ); if (usedImportsKeys.includes("utils")) { @@ -232,6 +233,6 @@ export const generateFetchers = async (context: Context, config: Config) => { createNamedImport(fetcherImports, `./${fetcherFilename}`), ...usedImportsNodes, ...nodes, - ]) + ]), ); }; diff --git a/plugins/typescript/src/generators/generateJsonApiReactQueryComponents.ts b/plugins/typescript/src/generators/generateJsonApiReactQueryComponents.ts new file mode 100644 index 00000000..b68ced78 --- /dev/null +++ b/plugins/typescript/src/generators/generateJsonApiReactQueryComponents.ts @@ -0,0 +1,761 @@ +import ts, { factory as f } from "typescript"; +import * as c from "case"; + +import { ConfigBase, Context } from "./types"; +import { + isReferenceObject, + OperationObject, + PathItemObject, +} from "openapi3-ts/oas31"; + +import { getUsedImports } from "../core/getUsedImports"; +import { createWatermark } from "../core/createWatermark"; +import { isVerb } from "../core/isVerb"; +import { isOperationObject } from "../core/isOperationObject"; + +import { getReferenceSchema } from "../core/getReferenceSchema"; +import { isValidPropertyName } from "tsutils"; +import { isJsonApiResourceSchema } from "../core/isJsonApiResourceSchema"; +import { + getJsonApiResponseResource, + JsonApiResponseResource, +} from "../core/getJsonApiResponseResource"; +import { generateReactQueryComponents } from "./generateReactQueryComponents"; +import { isJsonApiOperationPaginated } from "../core/isJsonApiResponsePaginated"; +import { determineComponentForOperations } from "../core/determineComponentForOperations"; + +export type Config = ConfigBase & { + /** + * Generated files paths from `generateSchemaTypes` + */ + schemasFiles: { + requestBodies: string; + schemas: string; + parameters: string; + responses: string; + }; + /** + * List of headers injected in the custom fetcher + * + * This will mark the header as optional in the component API + */ + injectedHeaders?: string[]; +}; + +export const generateJsonApiReactQueryComponents = async ( + context: Context, + config: Config, +) => { + context.openAPIDocument = determineComponentForOperations( + context.openAPIDocument, + ); + generateReactQueryComponents(context, config); + + const sourceFile = ts.createSourceFile( + "index.ts", + "", + ts.ScriptTarget.Latest, + ); + + const printer = ts.createPrinter({ + newLine: ts.NewLineKind.LineFeed, + removeComments: false, + }); + + const printNodes = (nodes: ts.Node[]) => + nodes + .map((node: ts.Node, i, nodes) => { + return ( + printer.printNode(ts.EmitHint.Unspecified, node, sourceFile) + + (ts.isJSDoc(node) || + (ts.isImportDeclaration(node) && + nodes[i + 1] && + ts.isImportDeclaration(nodes[i + 1])) + ? "" + : "\n") + ); + }) + .join("\n"); + + const filenamePrefix = + c.snake(config.filenamePrefix ?? context.openAPIDocument.info.title) + "-"; + + const formatFilename = config.filenameCase ? c[config.filenameCase] : c.camel; + + const filename = formatFilename(filenamePrefix + "-resources"); + + const nodes: ts.Node[] = []; + + const operationIds: string[] = []; + + const resources: Record< + string, + { name: string; node: ts.TypeReferenceNode } + > = {}; + + context.openAPIDocument.components && + context.openAPIDocument.components.schemas && + Object.entries(context.openAPIDocument.components.schemas).forEach( + ([name, schema]) => { + if (isReferenceObject(schema)) { + schema = getReferenceSchema(schema.$ref, context.openAPIDocument); + } + const resourceSchema = isJsonApiResourceSchema( + schema, + context.openAPIDocument, + ); + + if (!resourceSchema) { + return; + } + + const resourceType = + (resourceSchema.properties.type.enum && + resourceSchema.properties.type.enum[0]) || + resourceSchema.properties.type.const; + + if (resourceType === undefined) { + return; + } + resources[resourceType] = { + name: c.pascal(name), + node: f.createTypeReferenceNode( + f.createQualifiedName( + f.createIdentifier("Schemas"), + f.createIdentifier(c.pascal(name)), + ), + ), + }; + }, + ); + if (Object.keys(resources).length === 0) { + console.error(`⚠️ You don't have any json api resources defined!`); + return; + } + nodes.push( + f.createInterfaceDeclaration( + [f.createModifier(ts.SyntaxKind.ExportKeyword)], + "ResourceMap", + undefined, + [ + f.createHeritageClause(ts.SyntaxKind.ExtendsKeyword, [ + f.createExpressionWithTypeArguments( + f.createIdentifier("Utils.IResourceMap"), + [], + ), + ]), + ], + Object.entries(resources).map(([name, { node }]) => + f.createPropertySignature( + undefined, + isValidPropertyName(name) ? name : f.createStringLiteral(name), + undefined, + node, + ), + ), + ), + ); + nodes.push( + f.createTypeAliasDeclaration( + [f.createModifier(ts.SyntaxKind.ExportKeyword)], + "Resource", + undefined, + f.createUnionTypeNode(Object.values(resources).map(({ node }) => node)), + ), + ); + context.openAPIDocument.paths && + Object.entries(context.openAPIDocument.paths).forEach( + ([route, verbs]: [string, PathItemObject]) => { + Object.entries(verbs).forEach(([verb, operation]) => { + if (!isVerb(verb) || !isOperationObject(operation)) return; + const operationId = operation.operationId; + + const resourceType = getJsonApiResponseResource( + operation.responses, + context.openAPIDocument, + ); + if (resourceType === undefined) { + return; + } + if (operationIds.includes(operationId)) { + throw new Error( + `The operationId "${operation.operationId}" is duplicated in your schema definition!`, + ); + } + operationIds.push(operationId); + const isPaginated = isJsonApiOperationPaginated( + operation, + context.openAPIDocument, + ); + + const component: "useQuery" | "useMutate" | "useInfiniteQuery" = + operation["x-openapi-codegen-component"] || + (verb === "get" + ? isPaginated + ? "useInfiniteQuery" + : "useQuery" + : "useMutate"); + + if ( + !["useQuery", "useMutate", "useInfiniteQuery"].includes(component) + ) { + throw new Error(`[x-openapi-codegen-component] Invalid value for ${operation.operationId} operation + Valid options: "useMutate", "useQuery", "useInfiniteQuery"`); + } + + if (component === "useInfiniteQuery" && !isPaginated) { + throw new Error( + `[x-openapi-codegen-component] Invalid value for ${operation.operationId} operation, the does not appear to be paginated, its missing pagination query parameters`, + ); + } + + let hook: ts.Node[] = []; + + // noinspection JSUnreachableSwitchBranches <-- phpstorm is confused :S + switch (component) { + case "useInfiniteQuery": + hook = createInfiniteQueryHook({ + operation, + dataType: c.pascal(`${operationId}Response`), + errorType: c.pascal(`${operationId}Error`), + variablesType: c.pascal(`${operationId}Variables`), + name: `use${c.pascal(operationId)}`, + resourceType: resourceType.resourceType, + }); + break; + case "useQuery": + hook = createQueryHook({ + operation, + dataType: c.pascal(`${operationId}Response`), + errorType: c.pascal(`${operationId}Error`), + variablesType: c.pascal(`${operationId}Variables`), + name: `use${c.pascal(operationId)}`, + resourceType, + }); + break; + case "useMutate": + // hook = createMutationHook({ + // operation, + // dataType: c.pascal(`${operationId}Response`), + // errorType: c.pascal(`${operationId}Error`), + // variablesType: c.pascal(`${operationId}Variables`), + // name: `use${c.pascal(operationId)}`, + // }); + break; + } + + nodes.push(...hook); + }); + }, + ); + + if (operationIds.length === 0) { + console.log(`⚠️ You don't have any operation with "operationId" defined!`); + } + + const { nodes: usedImportsNodes } = getUsedImports(nodes, { + ...config.schemasFiles, + }); + + await context.writeFile( + filename + ".ts", + printNodes([ + createWatermark(context.openAPIDocument.info), + createReactQueryImport(), + f.createImportDeclaration( + undefined, + f.createImportClause( + false, + undefined, + f.createNamespaceImport(f.createIdentifier("Components")), + ), + f.createStringLiteral( + `./${formatFilename(filenamePrefix + "-components")}`, + ), + undefined, + ), + f.createImportDeclaration( + undefined, + f.createImportClause( + false, + undefined, + f.createNamespaceImport(f.createIdentifier("Utils")), + ), + f.createStringLiteral(`./${formatFilename(filenamePrefix + "-utils")}`), + undefined, + ), + ...usedImportsNodes, + ...nodes, + ]), + ); +}; + +// const createMutationHook = ({ +// dataType, +// errorType, +// variablesType, +// name, +// operation, +// }: { +// name: string; +// dataType: string; +// errorType: string; +// variablesType: string; +// operation: OperationObject; +// }) => { +// const nodes: ts.Node[] = []; +// if (operation.description) { +// nodes.push(f.createJSDocComment(operation.description.trim(), [])); +// } +// +// nodes.push( +// f.createVariableStatement( +// [f.createModifier(ts.SyntaxKind.ExportKeyword)], +// f.createVariableDeclarationList( +// [ +// f.createVariableDeclaration( +// f.createIdentifier(name), +// undefined, +// undefined, +// f.createArrowFunction( +// undefined, +// undefined, +// [ +// f.createParameterDeclaration( +// undefined, +// undefined, +// f.createIdentifier("options"), +// f.createToken(ts.SyntaxKind.QuestionToken), +// f.createTypeReferenceNode(f.createIdentifier("Omit"), [ +// f.createTypeReferenceNode( +// f.createQualifiedName( +// f.createIdentifier("reactQuery"), +// f.createIdentifier("UseMutationOptions"), +// ), +// [dataType, errorType, variablesType], +// ), +// f.createLiteralTypeNode( +// f.createStringLiteral("mutationFn"), +// ), +// ]), +// undefined, +// ), +// ], +// undefined, +// f.createToken(ts.SyntaxKind.EqualsGreaterThanToken), +// f.createBlock( +// [ +// f.createVariableStatement( +// undefined, +// f.createVariableDeclarationList( +// [ +// f.createVariableDeclaration( +// f.createObjectBindingPattern([ +// f.createBindingElement( +// undefined, +// undefined, +// f.createIdentifier("fetcherOptions"), +// undefined, +// ), +// ]), +// undefined, +// undefined, +// f.createCallExpression( +// f.createIdentifier(contextHookName), +// undefined, +// [], +// ), +// ), +// ], +// ts.NodeFlags.Const, +// ), +// ), +// f.createReturnStatement( +// f.createCallExpression( +// f.createPropertyAccessExpression( +// f.createIdentifier("reactQuery"), +// f.createIdentifier("useMutation"), +// ), +// [dataType, errorType, variablesType], +// [ +// f.createObjectLiteralExpression( +// [ +// f.createPropertyAssignment( +// "mutationFn", +// f.createArrowFunction( +// undefined, +// undefined, +// [ +// f.createParameterDeclaration( +// undefined, +// undefined, +// f.createIdentifier("variables"), +// undefined, +// variablesType, +// undefined, +// ), +// ], +// undefined, +// f.createToken( +// ts.SyntaxKind.EqualsGreaterThanToken, +// ), +// f.createCallExpression( +// f.createIdentifier(operationFetcherFnName), +// undefined, +// [ +// f.createObjectLiteralExpression( +// [ +// f.createSpreadAssignment( +// f.createIdentifier("fetcherOptions"), +// ), +// f.createSpreadAssignment( +// f.createIdentifier("variables"), +// ), +// ], +// false, +// ), +// ], +// ), +// ), +// ), +// f.createSpreadAssignment( +// f.createIdentifier("options"), +// ), +// ], +// true, +// ), +// ], +// ), +// ), +// ], +// true, +// ), +// ), +// ), +// ], +// ts.NodeFlags.Const, +// ), +// ), +// ); +// +// return nodes; +// }; +// +const createQueryHook = ({ + dataType, + errorType, + variablesType, + name, + operation, + resourceType, +}: { + name: string; + dataType: string; + errorType: string; + variablesType: string; + operation: OperationObject; + resourceType: JsonApiResponseResource; +}) => { + const deserializerName = resourceType.isArray + ? "deserializeResourceCollection" + : "deserializeResource"; + + const nodes: ts.Node[] = []; + if (operation.description) { + nodes.push(f.createJSDocComment(operation.description.trim(), [])); + } + nodes.push( + f.createVariableStatement( + [f.createModifier(ts.SyntaxKind.ExportKeyword)], + f.createVariableDeclarationList( + [ + f.createVariableDeclaration( + f.createIdentifier(name), + undefined, + undefined, + f.createArrowFunction( + undefined, + [ + f.createTypeParameterDeclaration( + undefined, + "TData", + undefined, + f.createTypeReferenceNode(`Components.${dataType}`), + ), + f.createTypeParameterDeclaration( + undefined, + "Includes", + f.createArrayTypeNode( + f.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), + ), + f.createArrayTypeNode( + f.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), + ), + ), + ], + [ + f.createParameterDeclaration( + undefined, + undefined, + f.createIdentifier("variables"), + undefined, + f.createIntersectionTypeNode([ + f.createTypeReferenceNode(`Components.${variablesType}`), + f.createTypeLiteralNode([ + f.createPropertySignature( + undefined, + "queryParams", + f.createToken(ts.SyntaxKind.QuestionToken), + f.createTypeLiteralNode([ + f.createPropertySignature( + undefined, + "include", + f.createToken(ts.SyntaxKind.QuestionToken), + f.createTypeReferenceNode("Includes"), + ), + ]), + ), + ]), + ]), + ), + f.createParameterDeclaration( + undefined, + undefined, + f.createIdentifier("options"), + f.createToken(ts.SyntaxKind.QuestionToken), + f.createTypeReferenceNode(f.createIdentifier("Omit"), [ + f.createTypeReferenceNode("Components.UseQueryOptions", [ + f.createTypeReferenceNode(`Components.${dataType}`), + f.createTypeReferenceNode(`Components.${errorType}`), + f.createTypeReferenceNode("TData"), + ]), + f.createLiteralTypeNode(f.createStringLiteral("select")), + ]), + ), + ], + undefined, + f.createToken(ts.SyntaxKind.EqualsGreaterThanToken), + f.createCallExpression( + f.createIdentifier(`Components.${name}`), + undefined, + [ + f.createIdentifier("variables"), + f.createObjectLiteralExpression([ + f.createSpreadAssignment(f.createIdentifier("options")), + f.createPropertyAssignment( + "select", + f.createArrowFunction( + undefined, + undefined, + [ + f.createParameterDeclaration( + undefined, + undefined, + f.createIdentifier("data"), + undefined, + ), + ], + undefined, + f.createToken(ts.SyntaxKind.EqualsGreaterThanToken), + f.createCallExpression( + f.createIdentifier(`Utils.${deserializerName}`), + [ + f.createLiteralTypeNode( + f.createStringLiteral(resourceType.resourceType), + ), + f.createIndexedAccessTypeNode( + f.createTypeReferenceNode( + f.createIdentifier("Includes"), + ), + f.createLiteralTypeNode( + f.createNumericLiteral("number"), + ), + ), + f.createTypeReferenceNode( + f.createIdentifier("ResourceMap"), + ), + ], + [f.createIdentifier("data")], + ), + ), + ), + ]), + ], + ), + ), + ), + ], + ts.NodeFlags.Const, + ), + ), + ); + + return nodes; +}; + +const createInfiniteQueryHook = ({ + dataType, + errorType, + variablesType, + name, + operation, + resourceType, +}: { + name: string; + dataType: string; + errorType: string; + variablesType: string; + operation: OperationObject; + resourceType: string; +}) => { + const nodes: ts.Node[] = []; + if (operation.description) { + nodes.push(f.createJSDocComment(operation.description.trim(), [])); + } + nodes.push( + f.createVariableStatement( + [f.createModifier(ts.SyntaxKind.ExportKeyword)], + f.createVariableDeclarationList( + [ + f.createVariableDeclaration( + f.createIdentifier(name), + undefined, + undefined, + f.createArrowFunction( + undefined, + [ + f.createTypeParameterDeclaration( + undefined, + "TData", + undefined, + f.createTypeReferenceNode("reactQuery.InfiniteData", [ + f.createTypeReferenceNode(`Components.${dataType}`), + ]), + ), + f.createTypeParameterDeclaration( + undefined, + "Includes", + f.createArrayTypeNode( + f.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), + ), + f.createArrayTypeNode( + f.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), + ), + ), + ], + [ + f.createParameterDeclaration( + undefined, + undefined, + f.createIdentifier("variables"), + undefined, + f.createIntersectionTypeNode([ + f.createTypeReferenceNode(`Components.${variablesType}`), + f.createTypeLiteralNode([ + f.createPropertySignature( + undefined, + "queryParams", + f.createToken(ts.SyntaxKind.QuestionToken), + f.createTypeLiteralNode([ + f.createPropertySignature( + undefined, + "include", + f.createToken(ts.SyntaxKind.QuestionToken), + f.createTypeReferenceNode("Includes"), + ), + ]), + ), + ]), + ]), + ), + f.createParameterDeclaration( + undefined, + undefined, + f.createIdentifier("options"), + f.createToken(ts.SyntaxKind.QuestionToken), + f.createTypeReferenceNode(f.createIdentifier("Omit"), [ + f.createTypeReferenceNode( + "Components.UseInfiniteQueryOptions", + [ + f.createTypeReferenceNode(`Components.${dataType}`), + f.createTypeReferenceNode(`Components.${errorType}`), + f.createTypeReferenceNode("TData"), + ], + ), + f.createLiteralTypeNode(f.createStringLiteral("select")), + ]), + ), + ], + undefined, + f.createToken(ts.SyntaxKind.EqualsGreaterThanToken), + f.createCallExpression( + f.createIdentifier(`Components.${name}`), + undefined, + [ + f.createIdentifier("variables"), + f.createObjectLiteralExpression([ + f.createSpreadAssignment(f.createIdentifier("options")), + f.createPropertyAssignment( + "select", + f.createArrowFunction( + undefined, + undefined, + [ + f.createParameterDeclaration( + undefined, + undefined, + f.createIdentifier("data"), + undefined, + ), + ], + undefined, + f.createToken(ts.SyntaxKind.EqualsGreaterThanToken), + f.createCallExpression( + f.createIdentifier( + "Utils.deserializeInfiniteResourceCollection", + ), + [ + f.createLiteralTypeNode( + f.createStringLiteral(resourceType), + ), + f.createIndexedAccessTypeNode( + f.createTypeReferenceNode( + f.createIdentifier("Includes"), + ), + f.createLiteralTypeNode( + f.createNumericLiteral("number"), + ), + ), + f.createTypeReferenceNode( + f.createIdentifier("ResourceMap"), + ), + ], + [f.createIdentifier("data")], + ), + ), + ), + ]), + ], + ), + ), + ), + ], + ts.NodeFlags.Const, + ), + ), + ); + + return nodes; +}; + +const createReactQueryImport = () => + f.createImportDeclaration( + undefined, + f.createImportClause( + false, + undefined, + f.createNamespaceImport(f.createIdentifier("reactQuery")), + ), + f.createStringLiteral("@tanstack/react-query"), + undefined, + ); diff --git a/plugins/typescript/src/generators/generateReactQueryComponents.test.ts b/plugins/typescript/src/generators/generateReactQueryComponents.test.ts index f168ec38..8bdd227b 100644 --- a/plugins/typescript/src/generators/generateReactQueryComponents.test.ts +++ b/plugins/typescript/src/generators/generateReactQueryComponents.test.ts @@ -1,4 +1,4 @@ -import { OpenAPIObject } from "openapi3-ts"; +import { OpenAPIObject } from "openapi3-ts/oas31"; import { Config, generateReactQueryComponents, @@ -90,7 +90,7 @@ describe("generateReactQueryComponents", () => { * @version 1.0.0 */ import * as reactQuery from "@tanstack/react-query"; - import { usePetstoreContext, PetstoreContext } from "./petstoreContext"; + import { usePetstoreQueryContext, PetstoreContext } from "./petstoreContext"; import type * as Fetcher from "./petstoreFetcher"; import { petstoreFetch } from "./petstoreFetcher"; import type * as Schemas from "./petstoreSchemas"; @@ -109,13 +109,17 @@ describe("generateReactQueryComponents", () => { /** * Get all the pets */ - export const useListPets = (variables: ListPetsVariables, options?: Omit, "queryKey" | "queryFn" | "initialData">) => { const { fetcherOptions, queryOptions, queryKeyFn } = usePetstoreContext(options); return reactQuery.useQuery({ - queryKey: queryKeyFn({ path: "/pets", operationId: "listPets", variables }), + export const useListPets = (variables: ListPetsVariables, options?: UseQueryOptions) => { const { fetcherOptions, queryOptions, queryKeyFn } = usePetstoreQueryContext(options); return reactQuery.useQuery({ + queryKey: queryKeyFn({ path: "/pets", operationId: "listPets", variables }, fetcherOptions), queryFn: ({ signal }) => fetchListPets({ ...fetcherOptions, ...variables }, signal), ...options, ...queryOptions }); }; + export type UseQueryOptions = Omit, "queryKey" | "queryFn">; + + export type UseInfiniteQueryOptions = Omit, "queryKey" | "queryFn" | "getNextPageParam" | "initialPageParam"> & Partial, "getNextPageParam" | "initialPageParam">>; + export type QueryOperation = { path: "/pets"; operationId: "listPets"; @@ -201,7 +205,7 @@ describe("generateReactQueryComponents", () => { * @version 1.0.0 */ import * as reactQuery from "@tanstack/react-query"; - import { usePetstoreContext, PetstoreContext } from "./petstoreContext"; + import { usePetstoreQueryContext, PetstoreContext } from "./petstoreContext"; import type * as Fetcher from "./petstoreFetcher"; import { petstoreFetch } from "./petstoreFetcher"; import type * as Schemas from "./petstoreSchemas"; @@ -233,12 +237,16 @@ describe("generateReactQueryComponents", () => { /** * Get all the pets */ - export const useListPets = (variables: ListPetsVariables, options?: Omit, "queryKey" | "queryFn" | "initialData">) => { const { fetcherOptions, queryOptions, queryKeyFn } = usePetstoreContext(options); return reactQuery.useQuery({ - queryKey: queryKeyFn({ path: "/pets", operationId: "listPets", variables }), + export const useListPets = (variables: ListPetsVariables, options?: UseQueryOptions) => { const { fetcherOptions, queryOptions, queryKeyFn } = usePetstoreQueryContext(options); return reactQuery.useQuery({ + queryKey: queryKeyFn({ path: "/pets", operationId: "listPets", variables }, fetcherOptions), queryFn: ({ signal }) => fetchListPets({ ...fetcherOptions, ...variables }, signal), ...options, ...queryOptions }); }; + + export type UseQueryOptions = Omit, "queryKey" | "queryFn">; + + export type UseInfiniteQueryOptions = Omit, "queryKey" | "queryFn" | "getNextPageParam" | "initialPageParam"> & Partial, "getNextPageParam" | "initialPageParam">>; export type QueryOperation = { path: "/pets"; @@ -311,7 +319,7 @@ describe("generateReactQueryComponents", () => { * @version 1.0.0 */ import * as reactQuery from "@tanstack/react-query"; - import { usePetstoreContext, PetstoreContext } from "./petstoreContext"; + import { usePetstoreQueryContext, PetstoreContext } from "./petstoreContext"; import type * as Fetcher from "./petstoreFetcher"; import { petstoreFetch } from "./petstoreFetcher"; import type * as Schemas from "./petstoreSchemas"; @@ -339,12 +347,16 @@ describe("generateReactQueryComponents", () => { /** * Info for a specific pet */ - export const useShowPetById = (variables: ShowPetByIdVariables, options?: Omit, "queryKey" | "queryFn" | "initialData">) => { const { fetcherOptions, queryOptions, queryKeyFn } = usePetstoreContext(options); return reactQuery.useQuery({ - queryKey: queryKeyFn({ path: "/pets/{petId}", operationId: "showPetById", variables }), + export const useShowPetById = (variables: ShowPetByIdVariables, options?: UseQueryOptions) => { const { fetcherOptions, queryOptions, queryKeyFn } = usePetstoreQueryContext(options); return reactQuery.useQuery({ + queryKey: queryKeyFn({ path: "/pets/{petId}", operationId: "showPetById", variables }, fetcherOptions), queryFn: ({ signal }) => fetchShowPetById({ ...fetcherOptions, ...variables }, signal), ...options, ...queryOptions }); }; + + export type UseQueryOptions = Omit, "queryKey" | "queryFn">; + + export type UseInfiniteQueryOptions = Omit, "queryKey" | "queryFn" | "getNextPageParam" | "initialPageParam"> & Partial, "getNextPageParam" | "initialPageParam">>; export type QueryOperation = { path: "/pets/{petId}"; @@ -431,7 +443,7 @@ describe("generateReactQueryComponents", () => { * @version 1.0.0 */ import * as reactQuery from "@tanstack/react-query"; - import { usePetstoreContext, PetstoreContext } from "./petstoreContext"; + import { usePetstoreQueryContext, PetstoreContext } from "./petstoreContext"; import type * as Fetcher from "./petstoreFetcher"; import { petstoreFetch } from "./petstoreFetcher"; import type * as Schemas from "./petstoreSchemas"; @@ -467,12 +479,16 @@ describe("generateReactQueryComponents", () => { /** * Get all the pets */ - export const useListPets = (variables: ListPetsVariables, options?: Omit, "queryKey" | "queryFn" | "initialData">) => { const { fetcherOptions, queryOptions, queryKeyFn } = usePetstoreContext(options); return reactQuery.useQuery({ - queryKey: queryKeyFn({ path: "/pets", operationId: "listPets", variables }), + export const useListPets = (variables: ListPetsVariables, options?: UseQueryOptions) => { const { fetcherOptions, queryOptions, queryKeyFn } = usePetstoreQueryContext(options); return reactQuery.useQuery({ + queryKey: queryKeyFn({ path: "/pets", operationId: "listPets", variables }, fetcherOptions), queryFn: ({ signal }) => fetchListPets({ ...fetcherOptions, ...variables }, signal), ...options, ...queryOptions }); }; + + export type UseQueryOptions = Omit, "queryKey" | "queryFn">; + + export type UseInfiniteQueryOptions = Omit, "queryKey" | "queryFn" | "getNextPageParam" | "initialPageParam"> & Partial, "getNextPageParam" | "initialPageParam">>; export type QueryOperation = { path: "/pets"; @@ -547,7 +563,7 @@ describe("generateReactQueryComponents", () => { * @version 1.0.0 */ import * as reactQuery from "@tanstack/react-query"; - import { usePetstoreContext, PetstoreContext } from "./petstoreContext"; + import { usePetstoreQueryContext, PetstoreContext } from "./petstoreContext"; import type * as Fetcher from "./petstoreFetcher"; import { petstoreFetch } from "./petstoreFetcher"; import type * as Schemas from "./petstoreSchemas"; @@ -566,12 +582,16 @@ describe("generateReactQueryComponents", () => { /** * Get all the pets */ - export const useListPets = (variables: ListPetsVariables, options?: Omit, "queryKey" | "queryFn" | "initialData">) => { const { fetcherOptions, queryOptions, queryKeyFn } = usePetstoreContext(options); return reactQuery.useQuery({ - queryKey: queryKeyFn({ path: "/pets", operationId: "listPets", variables }), + export const useListPets = (variables: ListPetsVariables, options?: UseQueryOptions) => { const { fetcherOptions, queryOptions, queryKeyFn } = usePetstoreQueryContext(options); return reactQuery.useQuery({ + queryKey: queryKeyFn({ path: "/pets", operationId: "listPets", variables }, fetcherOptions), queryFn: ({ signal }) => fetchListPets({ ...fetcherOptions, ...variables }, signal), ...options, ...queryOptions }); }; + + export type UseQueryOptions = Omit, "queryKey" | "queryFn">; + + export type UseInfiniteQueryOptions = Omit, "queryKey" | "queryFn" | "getNextPageParam" | "initialPageParam"> & Partial, "getNextPageParam" | "initialPageParam">>; export type QueryOperation = { path: "/pets"; @@ -673,7 +693,6 @@ describe("generateReactQueryComponents", () => { * @version 1.0.0 */ import * as reactQuery from "@tanstack/react-query"; - import { usePetstoreContext, PetstoreContext } from "./petstoreContext"; import type * as Fetcher from "./petstoreFetcher"; import { petstoreFetch } from "./petstoreFetcher"; @@ -699,12 +718,16 @@ describe("generateReactQueryComponents", () => { export const fetchAddPet = (variables: AddPetVariables, signal?: AbortSignal) => petstoreFetch({ url: "/pet", method: "post", ...variables, signal }); export const useAddPet = (options?: Omit, "mutationFn">) => { - const { fetcherOptions } = usePetstoreContext(); + const { fetcherOptions } = usePetstoreQueryContext(); return reactQuery.useMutation({ mutationFn: (variables: AddPetVariables) => fetchAddPet({ ...fetcherOptions, ...variables }), ...options }); }; + + export type UseQueryOptions = Omit, "queryKey" | "queryFn">; + + export type UseInfiniteQueryOptions = Omit, "queryKey" | "queryFn" | "getNextPageParam" | "initialPageParam"> & Partial, "getNextPageParam" | "initialPageParam">>; export type QueryOperation = { path: string; @@ -807,7 +830,6 @@ describe("generateReactQueryComponents", () => { * @version 1.0.0 */ import * as reactQuery from "@tanstack/react-query"; - import { usePetstoreContext, PetstoreContext } from "./petstoreContext"; import type * as Fetcher from "./petstoreFetcher"; import { petstoreFetch } from "./petstoreFetcher"; @@ -833,12 +855,16 @@ describe("generateReactQueryComponents", () => { export const fetchAddPet = (variables: AddPetVariables, signal?: AbortSignal) => petstoreFetch({ url: "/pet", method: "get", ...variables, signal }); export const useAddPet = (options?: Omit, "mutationFn">) => { - const { fetcherOptions } = usePetstoreContext(); + const { fetcherOptions } = usePetstoreQueryContext(); return reactQuery.useMutation({ mutationFn: (variables: AddPetVariables) => fetchAddPet({ ...fetcherOptions, ...variables }), ...options }); }; + + export type UseQueryOptions = Omit, "queryKey" | "queryFn">; + + export type UseInfiniteQueryOptions = Omit, "queryKey" | "queryFn" | "getNextPageParam" | "initialPageParam"> & Partial, "getNextPageParam" | "initialPageParam">>; export type QueryOperation = { path: string; @@ -947,7 +973,6 @@ describe("generateReactQueryComponents", () => { * @version 1.0.0 */ import * as reactQuery from "@tanstack/react-query"; - import { usePetstoreContext, PetstoreContext } from "./petstoreContext"; import type * as Fetcher from "./petstoreFetcher"; import { petstoreFetch } from "./petstoreFetcher"; import type * as RequestBodies from "./petstoreRequestBodies"; @@ -967,12 +992,16 @@ describe("generateReactQueryComponents", () => { export const fetchAddPet = (variables: AddPetVariables, signal?: AbortSignal) => petstoreFetch({ url: "/pet", method: "post", ...variables, signal }); export const useAddPet = (options?: Omit, "mutationFn">) => { - const { fetcherOptions } = usePetstoreContext(); + const { fetcherOptions } = usePetstoreQueryContext(); return reactQuery.useMutation({ mutationFn: (variables: AddPetVariables) => fetchAddPet({ ...fetcherOptions, ...variables }), ...options }); }; + + export type UseQueryOptions = Omit, "queryKey" | "queryFn">; + + export type UseInfiniteQueryOptions = Omit, "queryKey" | "queryFn" | "getNextPageParam" | "initialPageParam"> & Partial, "getNextPageParam" | "initialPageParam">>; export type QueryOperation = { path: string; @@ -1060,7 +1089,6 @@ describe("generateReactQueryComponents", () => { * @version 1.0.0 */ import * as reactQuery from "@tanstack/react-query"; - import { usePetstoreContext, PetstoreContext } from "./petstoreContext"; import type * as Fetcher from "./petstoreFetcher"; import { petstoreFetch } from "./petstoreFetcher"; import type * as RequestBodies from "./petstoreRequestBodies"; @@ -1079,12 +1107,16 @@ describe("generateReactQueryComponents", () => { export const fetchUpdatePet = (variables: UpdatePetVariables, signal?: AbortSignal) => petstoreFetch({ url: "/pet/{petId}", method: "put", ...variables, signal }); export const useUpdatePet = (options?: Omit, "mutationFn">) => { - const { fetcherOptions } = usePetstoreContext(); + const { fetcherOptions } = usePetstoreQueryContext(); return reactQuery.useMutation({ mutationFn: (variables: UpdatePetVariables) => fetchUpdatePet({ ...fetcherOptions, ...variables }), ...options }); }; + + export type UseQueryOptions = Omit, "queryKey" | "queryFn">; + + export type UseInfiniteQueryOptions = Omit, "queryKey" | "queryFn" | "getNextPageParam" | "initialPageParam"> & Partial, "getNextPageParam" | "initialPageParam">>; export type QueryOperation = { path: string; @@ -1172,7 +1204,6 @@ describe("generateReactQueryComponents", () => { * @version 1.0.0 */ import * as reactQuery from "@tanstack/react-query"; - import { usePetstoreContext, PetstoreContext } from "./petstoreContext"; import type * as Fetcher from "./petstoreFetcher"; import { petstoreFetch } from "./petstoreFetcher"; import type * as RequestBodies from "./petstoreRequestBodies"; @@ -1191,12 +1222,16 @@ describe("generateReactQueryComponents", () => { export const fetchUpdatePet = (variables: UpdatePetVariables, signal?: AbortSignal) => petstoreFetch({ url: "/pet/{petId}", method: "put", ...variables, signal }); export const useUpdatePet = (options?: Omit, "mutationFn">) => { - const { fetcherOptions } = usePetstoreContext(); + const { fetcherOptions } = usePetstoreQueryContext(); return reactQuery.useMutation({ mutationFn: (variables: UpdatePetVariables) => fetchUpdatePet({ ...fetcherOptions, ...variables }), ...options }); }; + + export type UseQueryOptions = Omit, "queryKey" | "queryFn">; + + export type UseInfiniteQueryOptions = Omit, "queryKey" | "queryFn" | "getNextPageParam" | "initialPageParam"> & Partial, "getNextPageParam" | "initialPageParam">>; export type QueryOperation = { path: string; @@ -1284,7 +1319,6 @@ describe("generateReactQueryComponents", () => { * @version 1.0.0 */ import * as reactQuery from "@tanstack/react-query"; - import { usePetstoreContext, PetstoreContext } from "./petstoreContext"; import type * as Fetcher from "./petstoreFetcher"; import { petstoreFetch } from "./petstoreFetcher"; import type * as RequestBodies from "./petstoreRequestBodies"; @@ -1303,12 +1337,16 @@ describe("generateReactQueryComponents", () => { export const fetchUpdatePet = (variables: UpdatePetVariables, signal?: AbortSignal) => petstoreFetch({ url: "/pet/{petId}", method: "put", ...variables, signal }); export const useUpdatePet = (options?: Omit, "mutationFn">) => { - const { fetcherOptions } = usePetstoreContext(); + const { fetcherOptions } = usePetstoreQueryContext(); return reactQuery.useMutation({ mutationFn: (variables: UpdatePetVariables) => fetchUpdatePet({ ...fetcherOptions, ...variables }), ...options }); }; + + export type UseQueryOptions = Omit, "queryKey" | "queryFn">; + + export type UseInfiniteQueryOptions = Omit, "queryKey" | "queryFn" | "getNextPageParam" | "initialPageParam"> & Partial, "getNextPageParam" | "initialPageParam">>; export type QueryOperation = { path: string; @@ -1396,7 +1434,6 @@ describe("generateReactQueryComponents", () => { * @version 1.0.0 */ import * as reactQuery from "@tanstack/react-query"; - import { useContext, Context } from "./context"; import type * as Fetcher from "./fetcher"; import { fetch } from "./fetcher"; import type * as RequestBodies from "./petstoreRequestBodies"; @@ -1415,12 +1452,16 @@ describe("generateReactQueryComponents", () => { export const fetchUpdatePet = (variables: UpdatePetVariables, signal?: AbortSignal) => fetch({ url: "/pet/{petId}", method: "put", ...variables, signal }); export const useUpdatePet = (options?: Omit, "mutationFn">) => { - const { fetcherOptions } = useContext(); + const { fetcherOptions } = useQueryContext(); return reactQuery.useMutation({ mutationFn: (variables: UpdatePetVariables) => fetchUpdatePet({ ...fetcherOptions, ...variables }), ...options }); }; + + export type UseQueryOptions = Omit, "queryKey" | "queryFn">; + + export type UseInfiniteQueryOptions = Omit, "queryKey" | "queryFn" | "getNextPageParam" | "initialPageParam"> & Partial, "getNextPageParam" | "initialPageParam">>; export type QueryOperation = { path: string; @@ -1492,7 +1533,7 @@ describe("generateReactQueryComponents", () => { * @version 1.0.0 */ import * as reactQuery from "@tanstack/react-query"; - import { usePetstoreContext, PetstoreContext } from "./petstoreContext"; + import { usePetstoreQueryContext, PetstoreContext } from "./petstoreContext"; import type * as Fetcher from "./petstoreFetcher"; import { petstoreFetch } from "./petstoreFetcher"; import type * as Schemas from "./petstoreSchemas"; @@ -1515,12 +1556,16 @@ describe("generateReactQueryComponents", () => { /** * Get all the pets */ - export const useListPets = (variables: ListPetsVariables, options?: Omit, "queryKey" | "queryFn" | "initialData">) => { const { fetcherOptions, queryOptions, queryKeyFn } = usePetstoreContext(options); return reactQuery.useQuery({ - queryKey: queryKeyFn({ path: "/pets", operationId: "listPets", variables }), + export const useListPets = (variables: ListPetsVariables, options?: UseQueryOptions) => { const { fetcherOptions, queryOptions, queryKeyFn } = usePetstoreQueryContext(options); return reactQuery.useQuery({ + queryKey: queryKeyFn({ path: "/pets", operationId: "listPets", variables }, fetcherOptions), queryFn: ({ signal }) => fetchListPets({ ...fetcherOptions, ...variables }, signal), ...options, ...queryOptions }); }; + + export type UseQueryOptions = Omit, "queryKey" | "queryFn">; + + export type UseInfiniteQueryOptions = Omit, "queryKey" | "queryFn" | "getNextPageParam" | "initialPageParam"> & Partial, "getNextPageParam" | "initialPageParam">>; export type QueryOperation = { path: "/pets"; @@ -1531,3 +1576,94 @@ describe("generateReactQueryComponents", () => { `); }); }); +it("should generate a useInfiniteQuery wrapper (no parameters)", async () => { + const writeFile = jest.fn(); + const openAPIDocument: OpenAPIObject = { + openapi: "3.0.0", + info: { + title: "petshop", + version: "1.0.0", + }, + paths: { + "/pets": { + get: { + operationId: "listPets", + description: "Get all the pets", + "x-openapi-codegen-component": "useInfiniteQuery", + responses: { + "200": { + description: "pet response", + content: { + "application/json": { + schema: { + type: "array", + items: { + $ref: "#/components/schemas/Pet", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + await generateReactQueryComponents( + { + openAPIDocument, + writeFile, + existsFile: () => true, + readFile: async () => "", + }, + config, + ); + + expect(writeFile.mock.calls[0][0]).toBe("petstoreComponents.ts"); + expect(writeFile.mock.calls[0][1]).toMatchInlineSnapshot(` + "/** + * Generated by @openapi-codegen + * + * @version 1.0.0 + */ + import * as reactQuery from "@tanstack/react-query"; + import { usePetstoreInfiniteQueryContext, PetstoreInfiniteContext } from "./petstoreContext"; + import type * as Fetcher from "./petstoreFetcher"; + import { petstoreFetch } from "./petstoreFetcher"; + import type * as Schemas from "./petstoreSchemas"; + + export type ListPetsError = Fetcher.ErrorWrapper; + + export type ListPetsResponse = Schemas.Pet[]; + + export type ListPetsVariables = PetstoreInfiniteContext["fetcherOptions"]; + + /** + * Get all the pets + */ + export const fetchListPets = (variables: ListPetsVariables, signal?: AbortSignal) => petstoreFetch({ url: "/pets", method: "get", ...variables, signal }); + + /** + * Get all the pets + */ + export const useListPets = >(variables: ListPetsVariables, options?: UseInfiniteQueryOptions) => { const operation: QueryOperation = { path: "/pets", operationId: "listPets", variables }; const { fetcherOptions, queryOptions, queryKeyFn, paginateVariables, ...paginationOptions } = usePetstoreInfiniteQueryContext(operation, options); return reactQuery.useInfiniteQuery({ + queryKey: queryKeyFn(operation, fetcherOptions), + queryFn: ({ signal, pageParam }) => fetchListPets(paginateVariables(pageParam as typeof paginationOptions.initialPageParam, { ...fetcherOptions, ...variables }), signal), + ...paginationOptions, + ...options, + ...queryOptions + }); }; + + export type UseQueryOptions = Omit, "queryKey" | "queryFn">; + + export type UseInfiniteQueryOptions = Omit, "queryKey" | "queryFn" | "getNextPageParam" | "initialPageParam"> & Partial, "getNextPageParam" | "initialPageParam">>; + + export type QueryOperation = { + path: "/pets"; + operationId: "listPets"; + variables: ListPetsVariables; + }; + " + `); +}); diff --git a/plugins/typescript/src/generators/generateReactQueryComponents.ts b/plugins/typescript/src/generators/generateReactQueryComponents.ts index 99887fd3..855ea639 100644 --- a/plugins/typescript/src/generators/generateReactQueryComponents.ts +++ b/plugins/typescript/src/generators/generateReactQueryComponents.ts @@ -3,7 +3,7 @@ import * as c from "case"; import { get } from "lodash"; import { ConfigBase, Context } from "./types"; -import { OperationObject, PathItemObject } from "openapi3-ts"; +import { OperationObject, PathItemObject } from "openapi3-ts/oas31"; import { getUsedImports } from "../core/getUsedImports"; import { createWatermark } from "../core/createWatermark"; @@ -39,12 +39,12 @@ export type Config = ConfigBase & { export const generateReactQueryComponents = async ( context: Context, - config: Config + config: Config, ) => { const sourceFile = ts.createSourceFile( "index.ts", "", - ts.ScriptTarget.Latest + ts.ScriptTarget.Latest, ); const printer = ts.createPrinter({ @@ -75,8 +75,12 @@ export const generateReactQueryComponents = async ( const filename = formatFilename(filenamePrefix + "-components"); const fetcherFn = c.camel(`${filenamePrefix}-fetch`); - const contextTypeName = `${c.pascal(filenamePrefix)}Context`; - const contextHookName = `use${c.pascal(filenamePrefix)}Context`; + const queryContextTypeName = `${c.pascal(filenamePrefix)}Context`; + const infiniteQueryContextTypeName = `${c.pascal(filenamePrefix)}InfiniteContext`; + + const queryContextHookName = `use${c.pascal(filenamePrefix)}QueryContext`; + const infiniteQueryContextHookName = `use${c.pascal(filenamePrefix)}InfiniteQueryContext`; + const nodes: ts.Node[] = []; const keyManagerItems: ts.TypeLiteralNode[] = []; @@ -91,98 +95,55 @@ export const generateReactQueryComponents = async ( prefix: filenamePrefix, contextPath: contextFilename, baseUrl: get(context.openAPIDocument, "servers.0.url"), - }) + }), ); } if (!context.existsFile(`${contextFilename}.ts`)) { context.writeFile( `${contextFilename}.ts`, - getContext(filenamePrefix, filename) + getContext(filenamePrefix, filename), ); } - // Generate `useQuery` & `useMutation` const operationIds: string[] = []; + const componentsUsed: { + useQuery: boolean; + useInfiniteQuery: boolean; + useMutate: boolean; + } = { + useQuery: false, + useInfiniteQuery: false, + useMutate: false, + }; - Object.entries(context.openAPIDocument.paths).forEach( - ([route, verbs]: [string, PathItemObject]) => { - Object.entries(verbs).forEach(([verb, operation]) => { - if (!isVerb(verb) || !isOperationObject(operation)) return; - const operationId = c.camel(operation.operationId); - if (operationIds.includes(operationId)) { - throw new Error( - `The operationId "${operation.operationId}" is duplicated in your schema definition!` - ); - } - operationIds.push(operationId); - - const { - dataType, - errorType, - requestBodyType, - pathParamsType, - variablesType, - queryParamsType, - headersType, - declarationNodes, - } = getOperationTypes({ - openAPIDocument: context.openAPIDocument, - operation, - operationId, - printNodes, - injectedHeaders: config.injectedHeaders, - pathParameters: verbs.parameters, - variablesExtraPropsType: f.createIndexedAccessTypeNode( - f.createTypeReferenceNode( - f.createIdentifier(contextTypeName), - undefined - ), - f.createLiteralTypeNode(f.createStringLiteral("fetcherOptions")) - ), - }); - - nodes.push(...declarationNodes); - - const operationFetcherFnName = `fetch${c.pascal(operationId)}`; - const component: "useQuery" | "useMutate" = - operation["x-openapi-codegen-component"] || - (verb === "get" ? "useQuery" : "useMutate"); + context.openAPIDocument.paths && + Object.entries(context.openAPIDocument.paths).forEach( + ([route, verbs]: [string, PathItemObject]) => { + Object.entries(verbs).forEach(([verb, operation]) => { + if (!isVerb(verb) || !isOperationObject(operation)) return; + const operationId = operation.operationId; + if (operationIds.includes(operationId)) { + throw new Error( + `The operationId "${operation.operationId}" is duplicated in your schema definition!`, + ); + } + operationIds.push(operationId); - if (!["useQuery", "useMutate"].includes(component)) { - throw new Error(`[x-openapi-codegen-component] Invalid value for ${operation.operationId} operation - Valid options: "useMutate", "useQuery"`); - } + const operationFetcherFnName = `fetch${c.pascal(operationId)}`; + const component: "useQuery" | "useMutate" | "useInfiniteQuery" = + operation["x-openapi-codegen-component"] || + (verb === "get" ? "useQuery" : "useMutate"); - if (component === "useQuery") { - keyManagerItems.push( - f.createTypeLiteralNode([ - f.createPropertySignature( - undefined, - f.createIdentifier("path"), - undefined, - f.createLiteralTypeNode( - f.createStringLiteral(camelizedPathParams(route)) - ) - ), - f.createPropertySignature( - undefined, - f.createIdentifier("operationId"), - undefined, - f.createLiteralTypeNode(f.createStringLiteral(operationId)) - ), - f.createPropertySignature( - undefined, - f.createIdentifier("variables"), - undefined, - variablesType - ), - ]) - ); - } + if ( + !["useQuery", "useMutate", "useInfiniteQuery"].includes(component) + ) { + throw new Error(`[x-openapi-codegen-component] Invalid value for ${operation.operationId} operation + Valid options: "useMutate", "useQuery", "useInfiniteQuery"`); + } + componentsUsed[component] = true; - nodes.push( - ...createOperationFetcherFnNodes({ + const { dataType, errorType, requestBodyType, @@ -190,37 +151,117 @@ export const generateReactQueryComponents = async ( variablesType, queryParamsType, headersType, + declarationNodes, + } = getOperationTypes({ + openAPIDocument: context.openAPIDocument, operation, - fetcherFn, - url: route, - verb, - name: operationFetcherFnName, - }), - ...(component === "useQuery" - ? createQueryHook({ + operationId, + printNodes, + injectedHeaders: config.injectedHeaders, + pathParameters: verbs.parameters, + variablesExtraPropsType: f.createIndexedAccessTypeNode( + f.createTypeReferenceNode( + f.createIdentifier( + component === "useInfiniteQuery" + ? infiniteQueryContextTypeName + : queryContextTypeName, + ), + undefined, + ), + f.createLiteralTypeNode(f.createStringLiteral("fetcherOptions")), + ), + }); + + nodes.push(...declarationNodes); + + if (component === "useQuery" || component === "useInfiniteQuery") { + keyManagerItems.push( + f.createTypeLiteralNode([ + f.createPropertySignature( + undefined, + f.createIdentifier("path"), + undefined, + f.createLiteralTypeNode( + f.createStringLiteral(camelizedPathParams(route)), + ), + ), + f.createPropertySignature( + undefined, + f.createIdentifier("operationId"), + undefined, + f.createLiteralTypeNode(f.createStringLiteral(operationId)), + ), + f.createPropertySignature( + undefined, + f.createIdentifier("variables"), + undefined, + variablesType, + ), + ]), + ); + } + let hook: ts.Node[]; + + // noinspection JSUnreachableSwitchBranches <-- phpstorm is confused :S + switch (component) { + case "useInfiniteQuery": + hook = createInfiniteQueryHook({ operationFetcherFnName, operation, dataType, errorType, variablesType, - contextHookName, + contextHookName: infiniteQueryContextHookName, name: `use${c.pascal(operationId)}`, operationId, url: route, - }) - : createMutationHook({ + }); + break; + case "useQuery": + hook = createQueryHook({ operationFetcherFnName, operation, dataType, errorType, variablesType, - contextHookName, + contextHookName: queryContextHookName, name: `use${c.pascal(operationId)}`, - })) - ); - }); - } - ); + operationId, + url: route, + }); + break; + case "useMutate": + hook = createMutationHook({ + operationFetcherFnName, + operation, + dataType, + errorType, + variablesType, + contextHookName: queryContextHookName, + name: `use${c.pascal(operationId)}`, + }); + break; + } + nodes.push( + ...createOperationFetcherFnNodes({ + dataType, + errorType, + requestBodyType, + pathParamsType, + variablesType, + queryParamsType, + headersType, + operation, + fetcherFn, + url: route, + verb, + name: operationFetcherFnName, + }), + ...hook, + ); + }); + }, + ); if (operationIds.length === 0) { console.log(`⚠️ You don't have any operation with "operationId" defined!`); @@ -236,21 +277,21 @@ export const generateReactQueryComponents = async ( undefined, f.createIdentifier("path"), undefined, - f.createKeywordTypeNode(ts.SyntaxKind.StringKeyword) + f.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), ), f.createPropertySignature( undefined, f.createIdentifier("operationId"), undefined, - f.createKeywordTypeNode(ts.SyntaxKind.NeverKeyword) + f.createKeywordTypeNode(ts.SyntaxKind.NeverKeyword), ), f.createPropertySignature( undefined, f.createIdentifier("variables"), undefined, - f.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword) + f.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword), ), - ]) + ]), ); const { nodes: usedImportsNodes, keys: usedImportsKeys } = getUsedImports( @@ -258,28 +299,145 @@ export const generateReactQueryComponents = async ( { ...config.schemasFiles, utils: utilsFilename, - } + }, ); if (usedImportsKeys.includes("utils")) { await context.writeFile(`${utilsFilename}.ts`, getUtils()); } + nodes.push( + f.createTypeAliasDeclaration( + [f.createModifier(ts.SyntaxKind.ExportKeyword)], + "UseQueryOptions", + [ + f.createTypeParameterDeclaration( + undefined, + f.createIdentifier("TQueryFnData"), + ), + f.createTypeParameterDeclaration( + undefined, + f.createIdentifier("TError"), + ), + f.createTypeParameterDeclaration( + undefined, + f.createIdentifier("TData"), + ), + ], + f.createTypeReferenceNode(f.createIdentifier("Omit"), [ + f.createTypeReferenceNode( + f.createQualifiedName( + f.createIdentifier("reactQuery"), + f.createIdentifier("UseQueryOptions"), + ), + [ + f.createTypeReferenceNode(f.createIdentifier("TQueryFnData"), []), + f.createTypeReferenceNode(f.createIdentifier("TError"), []), + f.createTypeReferenceNode(f.createIdentifier("TData"), []), + ], + ), + f.createUnionTypeNode([ + f.createLiteralTypeNode(f.createStringLiteral("queryKey")), + f.createLiteralTypeNode(f.createStringLiteral("queryFn")), + ]), + ]), + ), + ); + + nodes.push( + f.createTypeAliasDeclaration( + [f.createModifier(ts.SyntaxKind.ExportKeyword)], + "UseInfiniteQueryOptions", + [ + f.createTypeParameterDeclaration( + undefined, + f.createIdentifier("TQueryFnData"), + ), + f.createTypeParameterDeclaration( + undefined, + f.createIdentifier("TError"), + ), + f.createTypeParameterDeclaration( + undefined, + f.createIdentifier("TData"), + ), + ], + f.createIntersectionTypeNode([ + f.createTypeReferenceNode(f.createIdentifier("Omit"), [ + f.createTypeReferenceNode( + f.createQualifiedName( + f.createIdentifier("reactQuery"), + f.createIdentifier("UseInfiniteQueryOptions"), + ), + [ + f.createTypeReferenceNode(f.createIdentifier("TQueryFnData"), []), + f.createTypeReferenceNode(f.createIdentifier("TError"), []), + f.createTypeReferenceNode(f.createIdentifier("TData"), []), + ], + ), + f.createUnionTypeNode([ + f.createLiteralTypeNode(f.createStringLiteral("queryKey")), + f.createLiteralTypeNode(f.createStringLiteral("queryFn")), + f.createLiteralTypeNode(f.createStringLiteral("getNextPageParam")), + f.createLiteralTypeNode(f.createStringLiteral("initialPageParam")), + ]), + ]), + f.createTypeReferenceNode(f.createIdentifier("Partial"), [ + f.createTypeReferenceNode(f.createIdentifier("Pick"), [ + f.createTypeReferenceNode( + f.createQualifiedName( + f.createIdentifier("reactQuery"), + f.createIdentifier("UseInfiniteQueryOptions"), + ), + [ + f.createTypeReferenceNode( + f.createIdentifier("TQueryFnData"), + [], + ), + f.createTypeReferenceNode(f.createIdentifier("TError"), []), + f.createTypeReferenceNode(f.createIdentifier("TData"), []), + ], + ), + f.createUnionTypeNode([ + f.createLiteralTypeNode( + f.createStringLiteral("getNextPageParam"), + ), + f.createLiteralTypeNode( + f.createStringLiteral("initialPageParam"), + ), + ]), + ]), + ]), + ]), + ), + ); + + const componentContextImports = [ + ...(componentsUsed["useQuery"] + ? [queryContextHookName, queryContextTypeName] + : []), + ...(componentsUsed["useInfiniteQuery"] + ? [infiniteQueryContextHookName, infiniteQueryContextTypeName] + : []), + ]; + + const componentContextImportsNode: ts.Node[] = + componentContextImports.length > 0 + ? [createNamedImport(componentContextImports, `./${contextFilename}`)] + : []; + await context.writeFile( filename + ".ts", printNodes([ createWatermark(context.openAPIDocument.info), createReactQueryImport(), - createNamedImport( - [contextHookName, contextTypeName], - `./${contextFilename}` - ), + ...componentContextImportsNode, createNamespaceImport("Fetcher", `./${fetcherFilename}`), createNamedImport(fetcherFn, `./${fetcherFilename}`), ...usedImportsNodes, ...nodes, queryKeyManager, - ]) + ]), ); }; @@ -327,15 +485,15 @@ const createMutationHook = ({ f.createTypeReferenceNode( f.createQualifiedName( f.createIdentifier("reactQuery"), - f.createIdentifier("UseMutationOptions") + f.createIdentifier("UseMutationOptions"), ), - [dataType, errorType, variablesType] + [dataType, errorType, variablesType], ), f.createLiteralTypeNode( - f.createStringLiteral("mutationFn") + f.createStringLiteral("mutationFn"), ), ]), - undefined + undefined, ), ], undefined, @@ -352,7 +510,7 @@ const createMutationHook = ({ undefined, undefined, f.createIdentifier("fetcherOptions"), - undefined + undefined, ), ]), undefined, @@ -360,18 +518,18 @@ const createMutationHook = ({ f.createCallExpression( f.createIdentifier(contextHookName), undefined, - [] - ) + [], + ), ), ], - ts.NodeFlags.Const - ) + ts.NodeFlags.Const, + ), ), f.createReturnStatement( f.createCallExpression( f.createPropertyAccessExpression( f.createIdentifier("reactQuery"), - f.createIdentifier("useMutation") + f.createIdentifier("useMutation"), ), [dataType, errorType, variablesType], [ @@ -389,12 +547,12 @@ const createMutationHook = ({ f.createIdentifier("variables"), undefined, variablesType, - undefined + undefined, ), ], undefined, f.createToken( - ts.SyntaxKind.EqualsGreaterThanToken + ts.SyntaxKind.EqualsGreaterThanToken, ), f.createCallExpression( f.createIdentifier(operationFetcherFnName), @@ -403,36 +561,36 @@ const createMutationHook = ({ f.createObjectLiteralExpression( [ f.createSpreadAssignment( - f.createIdentifier("fetcherOptions") + f.createIdentifier("fetcherOptions"), ), f.createSpreadAssignment( - f.createIdentifier("variables") + f.createIdentifier("variables"), ), ], - false + false, ), - ] - ) - ) + ], + ), + ), ), f.createSpreadAssignment( - f.createIdentifier("options") + f.createIdentifier("options"), ), ], - true + true, ), - ] - ) + ], + ), ), ], - true - ) - ) + true, + ), + ), ), ], - ts.NodeFlags.Const - ) - ) + ts.NodeFlags.Const, + ), + ), ); return nodes; @@ -479,7 +637,7 @@ const createQueryHook = ({ undefined, "TData", undefined, - dataType + dataType, ), ], [ @@ -488,14 +646,18 @@ const createQueryHook = ({ undefined, f.createIdentifier("variables"), undefined, - variablesType + variablesType, ), f.createParameterDeclaration( undefined, undefined, f.createIdentifier("options"), f.createToken(ts.SyntaxKind.QuestionToken), - createUseQueryOptionsType(dataType, errorType) + f.createTypeReferenceNode("UseQueryOptions", [ + dataType, + errorType, + f.createTypeReferenceNode("TData"), + ]), ), ], undefined, @@ -511,19 +673,19 @@ const createQueryHook = ({ undefined, undefined, f.createIdentifier("fetcherOptions"), - undefined + undefined, ), f.createBindingElement( undefined, undefined, f.createIdentifier("queryOptions"), - undefined + undefined, ), f.createBindingElement( undefined, undefined, f.createIdentifier("queryKeyFn"), - undefined + undefined, ), ]), undefined, @@ -531,25 +693,25 @@ const createQueryHook = ({ f.createCallExpression( f.createIdentifier(contextHookName), undefined, - [f.createIdentifier("options")] - ) + [f.createIdentifier("options")], + ), ), ], - ts.NodeFlags.Const - ) + ts.NodeFlags.Const, + ), ), f.createReturnStatement( f.createCallExpression( f.createPropertyAccessExpression( f.createIdentifier("reactQuery"), - f.createIdentifier("useQuery") + f.createIdentifier("useQuery"), ), [ dataType, errorType, f.createTypeReferenceNode( f.createIdentifier("TData"), - [] + [], ), ], [ @@ -565,19 +727,20 @@ const createQueryHook = ({ f.createPropertyAssignment( "path", f.createStringLiteral( - camelizedPathParams(url) - ) + camelizedPathParams(url), + ), ), f.createPropertyAssignment( "operationId", - f.createStringLiteral(operationId) + f.createStringLiteral(operationId), ), f.createShorthandPropertyAssignment( - f.createIdentifier("variables") + f.createIdentifier("variables"), ), ]), - ] - ) + f.createIdentifier("fetcherOptions"), + ], + ), ), f.createPropertyAssignment( "queryFn", @@ -592,14 +755,14 @@ const createQueryHook = ({ f.createBindingElement( undefined, undefined, - "signal" + "signal", ), - ]) + ]), ), ], undefined, f.createToken( - ts.SyntaxKind.EqualsGreaterThanToken + ts.SyntaxKind.EqualsGreaterThanToken, ), f.createCallExpression( f.createIdentifier(operationFetcherFnName), @@ -608,65 +771,307 @@ const createQueryHook = ({ f.createObjectLiteralExpression( [ f.createSpreadAssignment( - f.createIdentifier("fetcherOptions") + f.createIdentifier("fetcherOptions"), ), f.createSpreadAssignment( - f.createIdentifier("variables") + f.createIdentifier("variables"), ), ], - false + false, ), f.createIdentifier("signal"), - ] - ) - ) + ], + ), + ), ), f.createSpreadAssignment( - f.createIdentifier("options") + f.createIdentifier("options"), ), f.createSpreadAssignment( - f.createIdentifier("queryOptions") + f.createIdentifier("queryOptions"), ), ], - true + true, ), - ] - ) + ], + ), ), - ]) - ) + ]), + ), ), ], - ts.NodeFlags.Const - ) - ) + ts.NodeFlags.Const, + ), + ), ); return nodes; }; -const createUseQueryOptionsType = ( - dataType: ts.TypeNode, - errorType: ts.TypeNode -) => - f.createTypeReferenceNode(f.createIdentifier("Omit"), [ - f.createTypeReferenceNode( - f.createQualifiedName( - f.createIdentifier("reactQuery"), - f.createIdentifier("UseQueryOptions") +const createInfiniteQueryHook = ({ + operationFetcherFnName, + contextHookName, + dataType, + errorType, + variablesType, + name, + operationId, + operation, + url, +}: { + operationFetcherFnName: string; + contextHookName: string; + name: string; + operationId: string; + url: string; + dataType: ts.TypeNode; + errorType: ts.TypeNode; + variablesType: ts.TypeNode; + operation: OperationObject; +}) => { + const nodes: ts.Node[] = []; + if (operation.description) { + nodes.push(f.createJSDocComment(operation.description.trim(), [])); + } + nodes.push( + f.createVariableStatement( + [f.createModifier(ts.SyntaxKind.ExportKeyword)], + f.createVariableDeclarationList( + [ + f.createVariableDeclaration( + f.createIdentifier(name), + undefined, + undefined, + f.createArrowFunction( + undefined, + [ + f.createTypeParameterDeclaration( + undefined, + "TData", + undefined, + f.createTypeReferenceNode("reactQuery.InfiniteData", [ + dataType, + ]), + ), + ], + [ + f.createParameterDeclaration( + undefined, + undefined, + f.createIdentifier("variables"), + undefined, + variablesType, + ), + f.createParameterDeclaration( + undefined, + undefined, + f.createIdentifier("options"), + f.createToken(ts.SyntaxKind.QuestionToken), + f.createTypeReferenceNode("UseInfiniteQueryOptions", [ + dataType, + errorType, + f.createTypeReferenceNode("TData"), + ]), + ), + ], + undefined, + f.createToken(ts.SyntaxKind.EqualsGreaterThanToken), + f.createBlock([ + f.createVariableStatement( + undefined, + f.createVariableDeclarationList( + [ + f.createVariableDeclaration( + "operation", + undefined, + f.createTypeReferenceNode("QueryOperation"), + f.createObjectLiteralExpression([ + f.createPropertyAssignment( + "path", + f.createStringLiteral(camelizedPathParams(url)), + ), + f.createPropertyAssignment( + "operationId", + f.createStringLiteral(operationId), + ), + f.createShorthandPropertyAssignment( + f.createIdentifier("variables"), + ), + ]), + ), + ], + ts.NodeFlags.Const, + ), + ), + f.createVariableStatement( + undefined, + f.createVariableDeclarationList( + [ + f.createVariableDeclaration( + f.createObjectBindingPattern([ + f.createBindingElement( + undefined, + undefined, + f.createIdentifier("fetcherOptions"), + undefined, + ), + f.createBindingElement( + undefined, + undefined, + f.createIdentifier("queryOptions"), + undefined, + ), + f.createBindingElement( + undefined, + undefined, + f.createIdentifier("queryKeyFn"), + undefined, + ), + f.createBindingElement( + undefined, + undefined, + f.createIdentifier("paginateVariables"), + undefined, + ), + f.createBindingElement( + f.createToken(ts.SyntaxKind.DotDotDotToken), + undefined, + f.createIdentifier("paginationOptions"), + undefined, + ), + ]), + undefined, + undefined, + f.createCallExpression( + f.createIdentifier(contextHookName), + undefined, + [ + f.createIdentifier("operation"), + f.createIdentifier("options"), + ], + ), + ), + ], + ts.NodeFlags.Const, + ), + ), + f.createReturnStatement( + f.createCallExpression( + f.createPropertyAccessExpression( + f.createIdentifier("reactQuery"), + f.createIdentifier("useInfiniteQuery"), + ), + [ + dataType, + errorType, + f.createTypeReferenceNode( + f.createIdentifier("TData"), + [], + ), + ], + [ + f.createObjectLiteralExpression( + [ + f.createPropertyAssignment( + "queryKey", + f.createCallExpression( + f.createIdentifier("queryKeyFn"), + undefined, + [ + f.createIdentifier("operation"), + f.createIdentifier("fetcherOptions"), + ], + ), + ), + f.createPropertyAssignment( + "queryFn", + f.createArrowFunction( + undefined, + undefined, + [ + f.createParameterDeclaration( + undefined, + undefined, + f.createObjectBindingPattern([ + f.createBindingElement( + undefined, + undefined, + "signal", + ), + f.createBindingElement( + undefined, + undefined, + "pageParam", + ), + ]), + ), + ], + undefined, + f.createToken( + ts.SyntaxKind.EqualsGreaterThanToken, + ), + f.createCallExpression( + f.createIdentifier(operationFetcherFnName), + undefined, + [ + f.createCallExpression( + f.createIdentifier("paginateVariables"), + undefined, + [ + f.createAsExpression( + f.createIdentifier("pageParam"), + f.createTypeQueryNode( + f.createIdentifier( + "paginationOptions.initialPageParam", + ), + ), + ), + f.createObjectLiteralExpression( + [ + f.createSpreadAssignment( + f.createIdentifier( + "fetcherOptions", + ), + ), + f.createSpreadAssignment( + f.createIdentifier("variables"), + ), + ], + false, + ), + ], + ), + f.createIdentifier("signal"), + ], + ), + ), + ), + f.createSpreadAssignment( + f.createIdentifier("paginationOptions"), + ), + f.createSpreadAssignment( + f.createIdentifier("options"), + ), + f.createSpreadAssignment( + f.createIdentifier("queryOptions"), + ), + ], + true, + ), + ], + ), + ), + ]), + ), + ), + ], + ts.NodeFlags.Const, ), - [ - dataType, - errorType, - f.createTypeReferenceNode(f.createIdentifier("TData"), []), - ] ), - f.createUnionTypeNode([ - f.createLiteralTypeNode(f.createStringLiteral("queryKey")), - f.createLiteralTypeNode(f.createStringLiteral("queryFn")), - f.createLiteralTypeNode(f.createStringLiteral("initialData")), - ]), - ]); + ); + + return nodes; +}; const createReactQueryImport = () => f.createImportDeclaration( @@ -674,8 +1079,8 @@ const createReactQueryImport = () => f.createImportClause( false, undefined, - f.createNamespaceImport(f.createIdentifier("reactQuery")) + f.createNamespaceImport(f.createIdentifier("reactQuery")), ), f.createStringLiteral("@tanstack/react-query"), - undefined + undefined, ); diff --git a/plugins/typescript/src/generators/generateReactQueryFunctions.test.ts b/plugins/typescript/src/generators/generateReactQueryFunctions.test.ts index 0aefb47d..b4d2e66f 100644 --- a/plugins/typescript/src/generators/generateReactQueryFunctions.test.ts +++ b/plugins/typescript/src/generators/generateReactQueryFunctions.test.ts @@ -1,4 +1,4 @@ -import { OpenAPIObject } from "openapi3-ts"; +import { OpenAPIObject } from "openapi3-ts/oas31"; import { Config, generateReactQueryFunctions, diff --git a/plugins/typescript/src/generators/generateReactQueryFunctions.ts b/plugins/typescript/src/generators/generateReactQueryFunctions.ts index 1051690c..718a01c7 100644 --- a/plugins/typescript/src/generators/generateReactQueryFunctions.ts +++ b/plugins/typescript/src/generators/generateReactQueryFunctions.ts @@ -3,7 +3,7 @@ import * as c from "case"; import { get } from "lodash"; import { ConfigBase, Context } from "./types"; -import { PathItemObject } from "openapi3-ts"; +import { PathItemObject } from "openapi3-ts/oas31"; import { getUsedImports } from "../core/getUsedImports"; import { createWatermark } from "../core/createWatermark"; @@ -41,12 +41,12 @@ export type Config = ConfigBase & { export const generateReactQueryFunctions = async ( context: Context, - config: Config + config: Config, ) => { const sourceFile = ts.createSourceFile( "index.ts", "", - ts.ScriptTarget.Latest + ts.ScriptTarget.Latest, ); const printer = ts.createPrinter({ @@ -92,132 +92,135 @@ export const generateReactQueryFunctions = async ( prefix: filenamePrefix, contextPath: contextFilename, baseUrl: get(context.openAPIDocument, "servers.0.url"), - }) + }), ); } if (!context.existsFile(`${contextFilename}.ts`)) { context.writeFile( `${contextFilename}.ts`, - getContext(filenamePrefix, filename) + getContext(filenamePrefix, filename), ); } // Generate `useQuery` & `useMutation` const operationIds: string[] = []; - Object.entries(context.openAPIDocument.paths).forEach( - ([route, verbs]: [string, PathItemObject]) => { - Object.entries(verbs).forEach(([verb, operation]) => { - if (!isVerb(verb) || !isOperationObject(operation)) return; - const operationId = c.camel(operation.operationId); - if (operationIds.includes(operationId)) { - throw new Error( - `The operationId "${operation.operationId}" is duplicated in your schema definition!` - ); - } - operationIds.push(operationId); + context.openAPIDocument.paths && + Object.entries(context.openAPIDocument.paths).forEach( + ([route, verbs]: [string, PathItemObject]) => { + Object.entries(verbs).forEach(([verb, operation]) => { + if (!isVerb(verb) || !isOperationObject(operation)) return; + const operationId = c.camel(operation.operationId); + if (operationIds.includes(operationId)) { + throw new Error( + `The operationId "${operation.operationId}" is duplicated in your schema definition!`, + ); + } + operationIds.push(operationId); - const { - dataType, - errorType, - requestBodyType, - pathParamsType, - variablesType, - queryParamsType, - headersType, - declarationNodes, - } = getOperationTypes({ - openAPIDocument: context.openAPIDocument, - operation, - operationId, - printNodes, - injectedHeaders: config.injectedHeaders, - pathParameters: verbs.parameters, - variablesExtraPropsType: f.createIndexedAccessTypeNode( - f.createTypeReferenceNode( - f.createIdentifier(contextTypeName), - undefined + const { + dataType, + errorType, + requestBodyType, + pathParamsType, + variablesType, + queryParamsType, + headersType, + declarationNodes, + } = getOperationTypes({ + openAPIDocument: context.openAPIDocument, + operation, + operationId, + printNodes, + injectedHeaders: config.injectedHeaders, + pathParameters: verbs.parameters, + variablesExtraPropsType: f.createIndexedAccessTypeNode( + f.createTypeReferenceNode( + f.createIdentifier(contextTypeName), + undefined, + ), + f.createLiteralTypeNode(f.createStringLiteral("fetcherOptions")), ), - f.createLiteralTypeNode(f.createStringLiteral("fetcherOptions")) - ), - }); + }); - const operationFetcherFnName = `fetch${c.pascal(operationId)}`; - const operationQueryFnName = `${c.pascal(operationId)}Query`; - const component: "useQuery" | "useMutate" = - operation["x-openapi-codegen-component"] || - (verb === "get" ? "useQuery" : "useMutate"); + const operationFetcherFnName = `fetch${c.pascal(operationId)}`; + const operationQueryFnName = `${c.pascal(operationId)}Query`; + const component: "useQuery" | "useMutate" | "useInfiniteQuery" = + operation["x-openapi-codegen-component"] || + (verb === "get" ? "useQuery" : "useMutate"); - if (!["useQuery", "useMutate"].includes(component)) { - throw new Error(`[x-openapi-codegen-component] Invalid value for ${operation.operationId} operation - Valid options: "useMutate", "useQuery"`); - } + if ( + !["useQuery", "useMutate", "useInfiniteQuery"].includes(component) + ) { + throw new Error(`[x-openapi-codegen-component] Invalid value for ${operation.operationId} operation + Valid options: "useMutate", "useQuery", "useInfiniteQuery"`); + } - if (component === "useQuery") { - nodes.push(...declarationNodes); + if (component === "useQuery") { + nodes.push(...declarationNodes); - keyManagerItems.push( - f.createTypeLiteralNode([ - f.createPropertySignature( - undefined, - f.createIdentifier("path"), - undefined, - f.createLiteralTypeNode( - f.createStringLiteral(camelizedPathParams(route)) - ) - ), - f.createPropertySignature( - undefined, - f.createIdentifier("operationId"), - undefined, - f.createLiteralTypeNode(f.createStringLiteral(operationId)) - ), - f.createPropertySignature( - undefined, - f.createIdentifier("variables"), - undefined, - variablesType - ), - ]) - ); + keyManagerItems.push( + f.createTypeLiteralNode([ + f.createPropertySignature( + undefined, + f.createIdentifier("path"), + undefined, + f.createLiteralTypeNode( + f.createStringLiteral(camelizedPathParams(route)), + ), + ), + f.createPropertySignature( + undefined, + f.createIdentifier("operationId"), + undefined, + f.createLiteralTypeNode(f.createStringLiteral(operationId)), + ), + f.createPropertySignature( + undefined, + f.createIdentifier("variables"), + undefined, + variablesType, + ), + ]), + ); - nodes.push( - ...createOperationFetcherFnNodes({ - dataType, - errorType, - requestBodyType, - pathParamsType, - variablesType, - queryParamsType, - headersType, - operation, - fetcherFn, - url: route, - verb, - name: operationFetcherFnName, - }), - ...createOperationQueryFnNodes({ - operationFetcherFnName, - dataType, - errorType, - requestBodyType, - pathParamsType, - variablesType, - queryParamsType, - headersType, - operation, - operationId, - fetcherFn, - url: route, - verb, - name: operationQueryFnName, - }) - ); - } - }); - } - ); + nodes.push( + ...createOperationFetcherFnNodes({ + dataType, + errorType, + requestBodyType, + pathParamsType, + variablesType, + queryParamsType, + headersType, + operation, + fetcherFn, + url: route, + verb, + name: operationFetcherFnName, + }), + ...createOperationQueryFnNodes({ + operationFetcherFnName, + dataType, + errorType, + requestBodyType, + pathParamsType, + variablesType, + queryParamsType, + headersType, + operation, + operationId, + fetcherFn, + url: route, + verb, + name: operationQueryFnName, + }), + ); + } + }); + }, + ); if (operationIds.length === 0) { console.log(`⚠️ You don't have any operation with "operationId" defined!`); @@ -233,21 +236,21 @@ export const generateReactQueryFunctions = async ( undefined, f.createIdentifier("path"), undefined, - f.createKeywordTypeNode(ts.SyntaxKind.StringKeyword) + f.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), ), f.createPropertySignature( undefined, f.createIdentifier("operationId"), undefined, - f.createKeywordTypeNode(ts.SyntaxKind.NeverKeyword) + f.createKeywordTypeNode(ts.SyntaxKind.NeverKeyword), ), f.createPropertySignature( undefined, f.createIdentifier("variables"), undefined, - f.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword) + f.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword), ), - ]) + ]), ); const { nodes: usedImportsNodes, keys: usedImportsKeys } = getUsedImports( @@ -255,7 +258,7 @@ export const generateReactQueryFunctions = async ( { ...config.schemasFiles, utils: utilsFilename, - } + }, ); if (usedImportsKeys.includes("utils")) { @@ -269,14 +272,14 @@ export const generateReactQueryFunctions = async ( createReactQueryImport(), createNamedImport( [contextTypeName, "queryKeyFn"], - `./${contextFilename}` + `./${contextFilename}`, ), createNamespaceImport("Fetcher", `./${fetcherFilename}`), createNamedImport(fetcherFn, `./${fetcherFilename}`), ...usedImportsNodes, ...nodes, queryKeyManager, - ]) + ]), ); }; @@ -286,8 +289,8 @@ const createReactQueryImport = () => f.createImportClause( false, undefined, - f.createNamespaceImport(f.createIdentifier("reactQuery")) + f.createNamespaceImport(f.createIdentifier("reactQuery")), ), f.createStringLiteral("@tanstack/react-query"), - undefined + undefined, ); diff --git a/plugins/typescript/src/generators/generateSchemaTypes.ts b/plugins/typescript/src/generators/generateSchemaTypes.ts index 14a32de9..ba783b81 100644 --- a/plugins/typescript/src/generators/generateSchemaTypes.ts +++ b/plugins/typescript/src/generators/generateSchemaTypes.ts @@ -1,14 +1,18 @@ import * as c from "case"; import ts from "typescript"; -import { ReferenceObject, SchemaObject } from "openapi3-ts"; +import { + isSchemaObject, + ReferenceObject, + SchemaObject, +} from "openapi3-ts/oas31"; import { createWatermark } from "../core/createWatermark"; import { getUsedImports } from "../core/getUsedImports"; import { schemaToTypeAliasDeclaration } from "../core/schemaToTypeAliasDeclaration"; import { getEnumProperties } from "../utils/getEnumProperties"; import { ConfigBase, Context } from "./types"; -import { isReferenceObject } from "openapi3-ts"; +import { isReferenceObject } from "openapi3-ts/oas31"; import { findCompatibleMediaType } from "../core/findCompatibleMediaType"; import { schemaToEnumDeclaration } from "../core/schemaToEnumDeclaration"; @@ -22,7 +26,7 @@ type Config = ConfigBase; */ export const generateSchemaTypes = async ( context: Context, - config: Config = {} + config: Config = {}, ) => { const { components } = context.openAPIDocument; if (!components) { @@ -32,7 +36,7 @@ export const generateSchemaTypes = async ( const sourceFile = ts.createSourceFile( "index.ts", "", - ts.ScriptTarget.Latest + ts.ScriptTarget.Latest, ); const printer = ts.createPrinter({ @@ -51,7 +55,7 @@ export const generateSchemaTypes = async ( .join("\n"); const handleTypeAlias = ( - componentSchema: [string, SchemaObject | ReferenceObject][] + componentSchema: [string, SchemaObject | ReferenceObject][], ) => componentSchema.reduce( (mem, [name, schema]) => [ @@ -63,10 +67,10 @@ export const generateSchemaTypes = async ( openAPIDocument: context.openAPIDocument, currentComponent: "schemas", }, - config.useEnums + config.useEnums, ), ], - [] + [], ); const filenamePrefix = @@ -84,7 +88,9 @@ export const generateSchemaTypes = async ( // Generate `components/schemas` types if (components.schemas) { const schemas: ts.Node[] = []; - const componentSchemaEntries = Object.entries(components.schemas); + const componentSchemaEntries = Object.entries(components.schemas).filter( + (entry): entry is [string, SchemaObject] => isSchemaObject(entry[1]), + ); if (config.useEnums) { const enumSchemaEntries = getEnumProperties(componentSchemaEntries); @@ -96,13 +102,14 @@ export const generateSchemaTypes = async ( currentComponent: "schemas", }), ], - [] + [], ); const componentsSchemas = handleTypeAlias( componentSchemaEntries.filter( - ([name]) => !enumSchemaEntries.some(([enumName]) => name === enumName) - ) + ([name]) => + !enumSchemaEntries.some(([enumName]) => name === enumName), + ), ); schemas.push(...enumSchemas, ...componentsSchemas); @@ -117,7 +124,7 @@ export const generateSchemaTypes = async ( createWatermark(context.openAPIDocument.info), ...getUsedImports(schemas, files).nodes, ...schemas, - ]) + ]), ); } @@ -145,7 +152,7 @@ export const generateSchemaTypes = async ( createWatermark(context.openAPIDocument.info), ...getUsedImports(componentsResponses, files).nodes, ...componentsResponses, - ]) + ]), ); } } @@ -153,7 +160,7 @@ export const generateSchemaTypes = async ( // Generate `components/requestBodies` types if (components.requestBodies) { const componentsRequestBodies = Object.entries( - components.requestBodies + components.requestBodies, ).reduce((mem, [name, requestBodyObject]) => { if (isReferenceObject(requestBodyObject)) return mem; const mediaType = findCompatibleMediaType(requestBodyObject); @@ -175,7 +182,7 @@ export const generateSchemaTypes = async ( createWatermark(context.openAPIDocument.info), ...getUsedImports(componentsRequestBodies, files).nodes, ...componentsRequestBodies, - ]) + ]), ); } } @@ -203,7 +210,7 @@ export const generateSchemaTypes = async ( createWatermark(context.openAPIDocument.info), ...getUsedImports(componentsParameters, files).nodes, ...componentsParameters, - ]) + ]), ); } diff --git a/plugins/typescript/src/generators/types.ts b/plugins/typescript/src/generators/types.ts index 18708fca..61e0bf15 100644 --- a/plugins/typescript/src/generators/types.ts +++ b/plugins/typescript/src/generators/types.ts @@ -1,4 +1,4 @@ -import { OpenAPIObject } from "openapi3-ts"; +import { OpenAPIObject } from "openapi3-ts/oas31"; import * as c from "case"; /** diff --git a/plugins/typescript/src/index.ts b/plugins/typescript/src/index.ts index 8af98265..a1a7e2f4 100644 --- a/plugins/typescript/src/index.ts +++ b/plugins/typescript/src/index.ts @@ -1,6 +1,7 @@ // Generators export { generateSchemaTypes } from "./generators/generateSchemaTypes"; export { generateReactQueryComponents } from "./generators/generateReactQueryComponents"; +export { generateJsonApiReactQueryComponents } from "./generators/generateJsonApiReactQueryComponents"; export { generateReactQueryFunctions } from "./generators/generateReactQueryFunctions"; export { generateFetchers } from "./generators/generateFetchers"; diff --git a/plugins/typescript/src/templates/context.ts b/plugins/typescript/src/templates/context.ts index 30b75fd0..33d45089 100644 --- a/plugins/typescript/src/templates/context.ts +++ b/plugins/typescript/src/templates/context.ts @@ -2,7 +2,7 @@ import { pascal } from "case"; export const getContext = (prefix: string, componentsFile: string) => `import type { QueryKey, UseQueryOptions } from "@tanstack/react-query"; - import { QueryOperation } from './${componentsFile}'; + import { QueryOperation, UseInfiniteQueryOptions } from './${componentsFile}'; export type ${pascal(prefix)}Context = { fetcherOptions: { @@ -25,30 +25,98 @@ export const getContext = (prefix: string, componentsFile: string) => /** * Query key manager. */ - queryKeyFn: (operation: QueryOperation) => QueryKey; + queryKeyFn: ( + operation: QueryOperation, + fetcherOptions: Context['fetcherOptions'] + ) => QueryKey }; + export type ${pascal(prefix)}InfiniteContext = Context & { + initialPageParam: PageParam | undefined + getNextPageParam: ( + lastPage: TQueryFnData | undefined, + allPages: TQueryFnData[] + ) => PageParam | undefined + getPreviousPageParam: ( + firstPage: TQueryFnData | undefined, + allPages: TQueryFnData[] + ) => PageParam | undefined + maxPages: number| undefined + paginateVariables: < + TVariables extends { + headers?: {} + queryParams?: {} + } + >( + pageParam: PageParam | undefined, + variables: TVariables + ) => TVariables + } + /** * Context injected into every react-query hook wrappers * - * @param queryOptions options from the useQuery wrapper + * @param _queryOptions options from the useQuery wrapper */ - export function use${pascal(prefix)}Context< - TQueryFnData = unknown, - TError = unknown, - TData = TQueryFnData, - TQueryKey extends QueryKey = QueryKey - >( - _queryOptions?: Omit, 'queryKey' | 'queryFn'> - ): ${pascal(prefix)}Context { + export function use${pascal(prefix)}QueryContext< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey + >( + _queryOptions?: Omit, 'queryKey' | 'queryFn'> + ): ${pascal(prefix)}Context { + return { + fetcherOptions: {}, + queryOptions: {}, + queryKeyFn + } + } + + export function useInfiniteQueryContext< + TQueryFnData, + TError, + TData + >( + operation: QueryOperation & { + variables: { + queryParams?: { + 'page'?: number + } + } + }, + _queryOptions?: UseInfiniteQueryOptions + ): ${pascal(prefix)}InfiniteContext { + const initialPageParam = operation.variables.queryParams && + operation.variables.queryParams['page'] ? operation.variables.queryParams['page'] : 1 return { fetcherOptions: {}, queryOptions: {}, - queryKeyFn + queryKeyFn, + initialPageParam, + getNextPageParam: (lastPage, allPages) => { + return lastPage ? initialPageParam + allPages.length : undefined + }, + getPreviousPageParam: (firstPage, allPages) => { + return undefined + }, + maxPages: undefined, + paginateVariables: (pageParam, variables) => { + if (pageParam === undefined) { + return variables + } + return { + ...variables, + queryParams: { + ...variables.queryParams, + page: pageParam + } + } + } + } } -}; - export const queryKeyFn = (operation: QueryOperation) => { + export const queryKeyFn = (operation: QueryOperation, fetcherOptions: Context['fetcherOptions']) => { const queryKey: unknown[] = hasPathParams(operation) ? operation.path .split("/") @@ -64,6 +132,8 @@ export const getContext = (prefix: string, componentsFile: string) => queryKey.push(operation.variables.body); } + queryKey.push(fetcherOptions) + return queryKey; } // Helpers diff --git a/plugins/typescript/src/templates/fetcher.ts b/plugins/typescript/src/templates/fetcher.ts index d9170135..3a526324 100644 --- a/plugins/typescript/src/templates/fetcher.ts +++ b/plugins/typescript/src/templates/fetcher.ts @@ -23,8 +23,8 @@ export const getFetcher = ({ * * Note: You need to re-gen after adding the first property to * have the \`${pascal(prefix)}FetcherExtraProps\` injected in \`${pascal( - prefix - )}Components.ts\` + prefix, + )}Components.ts\` **/ }` } @@ -36,7 +36,7 @@ export type ErrorWrapper = | { status: "unknown"; payload: string }; export type ${pascal( - prefix + prefix, )}FetcherOptions = { url: string; method: string; @@ -51,7 +51,7 @@ export type ${pascal( : `${pascal(prefix)}FetcherExtraProps` } -export async function ${camel(prefix)}Fetch< +export async function ${camel(`${prefix}Fetch`)}< TData, TError, TBody extends {} | FormData | undefined | null, diff --git a/plugins/typescript/src/templates/utils.ts b/plugins/typescript/src/templates/utils.ts index 3108040d..cfc0e15b 100644 --- a/plugins/typescript/src/templates/utils.ts +++ b/plugins/typescript/src/templates/utils.ts @@ -4,7 +4,11 @@ import { } from "../core/getErrorResponseType"; export const getUtils = () => - `type ComputeRange< + `import { TJsonApiData } from 'jsona/lib/JsonaTypes' +import { InfiniteData } from '@tanstack/react-query' +import { Jsona } from 'jsona' + +type ComputeRange< N extends number, Result extends Array = [] > = Result["length"] extends N @@ -12,4 +16,246 @@ export const getUtils = () => : ComputeRange; export type ${clientErrorStatus} = Exclude[number], ComputeRange<400>[number]>; -export type ${serverErrorStatus} = Exclude[number], ComputeRange<500>[number]>;`; +export type ${serverErrorStatus} = Exclude[number], ComputeRange<500>[number]>; + +/** + Extract the element of an array that also works for array union. + + Returns \`never\` if T is not an array. + + It creates a type-safe way to access the element type of \`unknown\` type. + */ +export type ArrayElement = T extends readonly unknown[] ? T[0] : never + +/** Find first match of multiple keys */ +export type FilterKeys = Obj[keyof Obj & Matchers] + +/** Get the type of a value of an input object with a given key. If the key is not found, return a default type. Works with unions of objects too. */ +export type GetValueWithDefault = Obj extends any + ? FilterKeys extends never + ? Default + : FilterKeys + : never + +/** remove a \`dot\` prefix from a string or string union */ +export type StripPrefix< + TPrefix extends string, + T extends string +> = T extends \`\${TPrefix}.\${infer R}\` ? R : never + +/** Find a \`dot\` prefix in a string or string union */ +export type FindPrefix< + prefix extends string, + T extends string +> = T extends \`\${prefix}.\${string}\` ? prefix : never + + +export interface IResourceMap { + [key: string]: TJsonApiData +} + +/** + * Get the \`data\` property (Resource Identifier(s)) of the \`Relationship\` in the \`Parent\` resource + */ +type RelationshipData< + Relationship extends keyof Parent['relationships'], + Parent extends TJsonApiData +> = GetValueWithDefault + +/** + * Get the \`type\` property of the \`Relationship\`'s data (Resource Identifier(s)) in the \`Parent\` resource + */ +type RelationshipType< + Relationship extends keyof Parent['relationships'], + Parent extends TJsonApiData +> = + GetValueWithDefault< + RelationshipData, + 'type', + never + > extends string + ? GetValueWithDefault, 'type', never> + : GetValueWithDefault< + ArrayElement>, + 'type', + never + > + +/** + * Get the Resource Schema of the related resource(s) for the \`Relationship\` in the \`Parent\` resource + */ +type RelatedResource< + Relationship extends keyof Parent['relationships'], + Parent extends TJsonApiData, + TResourceMap extends IResourceMap +> = + RelationshipType extends keyof TResourceMap + ? TResourceMap[RelationshipType] + : unknown + +/** + * Take a string union of \`Includes\` and reduce to only the keys of the \`Resource\`'s relationships + * Will strip dot suffixes from the \`Includes\` in order to correctly infer the base relationships + * e.g. 'onwReaction' | 'reactions.author' -> 'onwReaction' | 'reactions' + */ +type IncludesForResource< + Included extends string, + Resource extends TJsonApiData +> = Extract< + | keyof { + [Relationship in keyof Resource['relationships'] as FindPrefix< + Relationship & string, + Included + >]: Relationship + } + | Included, + keyof Resource['relationships'] +> + +/** + * Take a string union of \`Includes\` and reduce to only the keys prefixed by the \`Relationship\` + * e.g. for relationship "reactions": + * 'onwReaction' | 'reactions.author' -> 'author' + */ +type IncludesForRelationship< + Included extends string, + Relationship extends string +> = StripPrefix< + Extract, + Extract}.\${string}\`> +> + +/** + * The deserialized Resource for a \`Relationship\` in the \`Parent\` resource with appropriate \`Included\` relationships nested + */ +type DeserializedJsonApiRelationship< + Resource extends TJsonApiData, + Included extends string, + Relationship extends keyof Resource['relationships'] & string, + TResourceMap extends IResourceMap +> = + RelatedResource extends TJsonApiData + ? DeserializedJsonApiResource< + RelatedResource, + IncludesForRelationship, + TResourceMap + > + : never + +/** + * The deserialized Resource for a \`Resource\` with the appropriate \`Included\` relationships nested + */ +export type DeserializedJsonApiResource< + Resource extends TJsonApiData, + Included extends string, + TResourceMap extends IResourceMap +> = { + id: Resource['id'] + type: Resource['type'] +} & Required & { + [Relationship in IncludesForResource]: RelatedResource< + Relationship, + Resource, + TResourceMap + > extends TJsonApiData + ? GetValueWithDefault< + GetValueWithDefault< + Resource['relationships'][Relationship], + 'data', + never + >, + 'type', + never + > extends string + ? DeserializedJsonApiRelationship< + Resource, + Included, + Relationship, + TResourceMap + > | null + : DeserializedJsonApiRelationship< + Resource, + Included, + Relationship, + TResourceMap + >[] + : unknown + } + +const jsona = new Jsona() + +/** + * Deserialize a JSON:API single resource response to a typed resource object with the included relationships nested + */ +export const deserializeResource = < + Resource extends keyof TResourceMap, + Included extends string, + TResourceMap extends IResourceMap +>(data: { + data: TResourceMap[Resource] + included?: TJsonApiData[] +}): DeserializedJsonApiResource< + TResourceMap[Resource], + Included, + TResourceMap +> => { + return jsona.deserialize( + data + ) as unknown as DeserializedJsonApiResource< + TResourceMap[Resource], + Included, + TResourceMap + > +} + +/** + * Deserialize a JSON:API resource collection response to an array of typed resource objects with the included relationships nested + * @param data + */ +export const deserializeResourceCollection = < + Resource extends keyof TResourceMap, + Included extends string, + TResourceMap extends IResourceMap +>(data: { + data: TResourceMap[Resource][] + included?: TJsonApiData[] +}): DeserializedJsonApiResource< + TResourceMap[Resource], + Included, + TResourceMap +>[] => { + return jsona.deserialize( + data + ) as unknown as DeserializedJsonApiResource< + TResourceMap[Resource], + Included, + TResourceMap + >[] +} + +/** + * Deserialize the pages from an useInfiniteQuery result to a single array of typed resource objects with the included relationships nested + */ +export const deserializeInfiniteResourceCollection = < + Resource extends keyof TResourceMap, + Included extends string, + TResourceMap extends IResourceMap +>( + data: InfiniteData<{ + data: TResourceMap[Resource][] + included?: TJsonApiData[] + }> +): DeserializedJsonApiResource< + TResourceMap[Resource], + Included, + TResourceMap +>[] => { + return data.pages.flatMap(page => { + return jsona.deserialize(page) + }) as unknown as DeserializedJsonApiResource< + TResourceMap[Resource], + Included, + TResourceMap + >[] +} +`; diff --git a/plugins/typescript/src/utils/addPathParam.test.ts b/plugins/typescript/src/utils/addPathParam.test.ts index e84a762d..fbacfa7d 100644 --- a/plugins/typescript/src/utils/addPathParam.test.ts +++ b/plugins/typescript/src/utils/addPathParam.test.ts @@ -1,4 +1,4 @@ -import { OpenAPIObject } from "openapi3-ts"; +import { OpenAPIObject } from "openapi3-ts/oas31"; import { addPathParam } from "./addPathParam"; describe("addPathParam", () => { diff --git a/plugins/typescript/src/utils/addPathParam.ts b/plugins/typescript/src/utils/addPathParam.ts index 1490802c..1553514c 100644 --- a/plugins/typescript/src/utils/addPathParam.ts +++ b/plugins/typescript/src/utils/addPathParam.ts @@ -1,5 +1,5 @@ import { mapValues } from "lodash"; -import { OpenAPIObject, PathItemObject } from "openapi3-ts"; +import { OpenAPIObject, PathItemObject } from "openapi3-ts/oas31"; /** * Util to add a path param to an openAPI operation @@ -45,7 +45,7 @@ export const addPathParam = ({ }, ], } - : value + : value, ), }; }; diff --git a/plugins/typescript/src/utils/forceReactQueryComponent.test.ts b/plugins/typescript/src/utils/forceReactQueryComponent.test.ts index 989f8b68..79609962 100644 --- a/plugins/typescript/src/utils/forceReactQueryComponent.test.ts +++ b/plugins/typescript/src/utils/forceReactQueryComponent.test.ts @@ -6,11 +6,11 @@ describe("forceReactQueryComponent", () => { const updatedOpenAPIDocument = forceReactQueryComponent({ openAPIDocument: petstore, component: "useMutate", - operationId: "findPets", + operationIdMatcher: "findPets", }); expect( - updatedOpenAPIDocument.paths["/pets"].get["x-openapi-codegen-component"] + updatedOpenAPIDocument.paths["/pets"].get["x-openapi-codegen-component"], ).toBe("useMutate"); }); it("should throw if the operationId is not found", () => { @@ -18,10 +18,10 @@ describe("forceReactQueryComponent", () => { forceReactQueryComponent({ openAPIDocument: petstore, component: "useMutate", - operationId: "notFound", - }) + operationIdMatcher: "notFound", + }), ).toThrow( - `[forceReactQueryComponent] Operation with the operationId "notFound" not found` + `[forceReactQueryComponent] Operation with the operationId "notFound" not found`, ); }); @@ -30,11 +30,11 @@ describe("forceReactQueryComponent", () => { forceReactQueryComponent({ openAPIDocument: originalDocument, component: "useMutate", - operationId: "findPets", + operationIdMatcher: "findPets", }); expect( - originalDocument.paths["/pets"].get["x-openapi-codegen-component"] + originalDocument.paths["/pets"].get["x-openapi-codegen-component"], ).toBeUndefined(); }); }); diff --git a/plugins/typescript/src/utils/forceReactQueryComponent.ts b/plugins/typescript/src/utils/forceReactQueryComponent.ts index ddff3d69..a8cb6b87 100644 --- a/plugins/typescript/src/utils/forceReactQueryComponent.ts +++ b/plugins/typescript/src/utils/forceReactQueryComponent.ts @@ -1,12 +1,14 @@ import { cloneDeep, set } from "lodash"; -import { OpenAPIObject, PathItemObject } from "openapi3-ts"; +import { OpenAPIObject, PathItemObject } from "openapi3-ts/oas31"; import { isOperationObject } from "../core/isOperationObject"; import { isVerb } from "../core/isVerb"; -export const forceReactQueryComponent = ({ +export const forceReactQueryComponent = < + OperationIdMatcher extends string | RegExp, +>({ openAPIDocument, - operationId, + operationIdMatcher, component, }: { /** @@ -16,31 +18,43 @@ export const forceReactQueryComponent = ({ /** * OperationId to force */ - operationId: OperationId; + operationIdMatcher: OperationIdMatcher; /** * Component to use */ - component: "useMutate" | "useQuery"; + component: "useMutate" | "useQuery" | "useInfiniteQuery"; }) => { - let extensionPath: string | undefined; + let extensionPaths: string[] = []; // Find the component - Object.entries(openAPIDocument.paths).forEach( - ([route, verbs]: [string, PathItemObject]) => { - Object.entries(verbs).forEach(([verb, operation]) => { - if (!isVerb(verb) || !isOperationObject(operation)) return; - if (operation.operationId === operationId) { - extensionPath = `paths.${route}.${verb}.x-openapi-codegen-component`; - } - }); - } - ); - - if (!extensionPath) { - throw new Error( - `[forceReactQueryComponent] Operation with the operationId "${operationId}" not found` + openAPIDocument.paths && + Object.entries(openAPIDocument.paths).forEach( + ([route, verbs]: [string, PathItemObject]) => { + Object.entries(verbs).forEach(([verb, operation]) => { + if (!isVerb(verb) || !isOperationObject(operation)) return; + if ( + operationIdMatcher instanceof RegExp + ? operationIdMatcher.test(operation.operationId) + : operation.operationId === operationIdMatcher + ) { + extensionPaths.push( + `paths.${route}.${verb}.x-openapi-codegen-component`, + ); + } + }); + }, ); + + if (extensionPaths.length === 0) { + if (typeof operationIdMatcher === "string") { + throw new Error( + `[forceReactQueryComponent] Operation with the operationId "${operationIdMatcher}" not found`, + ); + } + return openAPIDocument; } - return set(cloneDeep(openAPIDocument), extensionPath, component); + const newDoc = cloneDeep(openAPIDocument); + extensionPaths.map((extensionPath) => set(newDoc, extensionPath, component)); + return newDoc; }; diff --git a/plugins/typescript/src/utils/getEnumProperties.test.ts b/plugins/typescript/src/utils/getEnumProperties.test.ts index d899489a..75895b05 100644 --- a/plugins/typescript/src/utils/getEnumProperties.test.ts +++ b/plugins/typescript/src/utils/getEnumProperties.test.ts @@ -1,4 +1,4 @@ -import { SchemaObject } from "openapi3-ts"; +import { SchemaObject } from "openapi3-ts/oas31"; import { convertNumberToWord, getEnumProperties } from "./getEnumProperties"; describe("getEnumProperties", () => { diff --git a/plugins/typescript/src/utils/getEnumProperties.ts b/plugins/typescript/src/utils/getEnumProperties.ts index 4cfcb789..3bd5555d 100644 --- a/plugins/typescript/src/utils/getEnumProperties.ts +++ b/plugins/typescript/src/utils/getEnumProperties.ts @@ -1,5 +1,5 @@ import { pascal } from "case"; -import { SchemaObject } from "openapi3-ts"; +import { SchemaObject } from "openapi3-ts/oas31"; /** * Extracts all the properties with enum values from an array of schema objects. @@ -7,7 +7,7 @@ import { SchemaObject } from "openapi3-ts"; * @returns A tuple array containing the property names with enum values and their corresponding schema objects */ export const getEnumProperties = ( - schemaArray: SchemaObject[] + schemaArray: [string, SchemaObject][], ): [string, SchemaObject][] => { const enumProperties: [string, SchemaObject][] = []; @@ -23,9 +23,9 @@ export const getEnumProperties = ( processProperty( enumProperties, `${name}${pascal(propertyName)}`, - propertySchema + propertySchema, ); - } + }, ); } }); @@ -36,7 +36,7 @@ export const getEnumProperties = ( const processProperty = ( enumProperties: [string, SchemaObject][], propertyName: string, - propertySchema: any + propertySchema: any, ) => { if (propertySchema.enum) { enumProperties.push([`${pascal(propertyName)}`, propertySchema]); @@ -46,9 +46,9 @@ const processProperty = ( processProperty( enumProperties, `${propertyName}${pascal(nestedPropertyName)}`, - nestedPropertySchema + nestedPropertySchema, ); - } + }, ); } }; diff --git a/plugins/typescript/src/utils/renameComponent.test.ts b/plugins/typescript/src/utils/renameComponent.test.ts index abbb15a1..a9fe0657 100644 --- a/plugins/typescript/src/utils/renameComponent.test.ts +++ b/plugins/typescript/src/utils/renameComponent.test.ts @@ -1,4 +1,4 @@ -import { OpenAPIObject } from "openapi3-ts"; +import { OpenAPIObject } from "openapi3-ts/oas31"; import { petstore } from "../fixtures/petstore"; import { renameComponent } from "./renameComponent"; diff --git a/plugins/typescript/src/utils/renameComponent.ts b/plugins/typescript/src/utils/renameComponent.ts index 16659b6f..867b9e15 100644 --- a/plugins/typescript/src/utils/renameComponent.ts +++ b/plugins/typescript/src/utils/renameComponent.ts @@ -1,5 +1,5 @@ import { get, set, unset } from "lodash"; -import { OpenAPIObject } from "openapi3-ts"; +import { OpenAPIObject } from "openapi3-ts/oas31"; /** * Util to rename an openAPI component name @@ -25,8 +25,8 @@ export const renameComponent = ({ const renamedOpenAPIDocument: OpenAPIObject = JSON.parse( JSON.stringify(openAPIDocument).replace( new RegExp(`"${from}"`, "g"), - `"${to}"` - ) + `"${to}"`, + ), ); const fromPath = from.slice("#/".length).replace(/\//g, ".");