feat: инициализация API CodeGen
CLI утилита для генерации TypeScript API клиента из OpenAPI спецификации. - Поддержка локальных файлов и URL для спецификаций - Кастомизация имени выходного файла через флаг --name - Генерация типизированного клиента с SWR хуками - Минимальный вывод логов для лучшего UX
This commit is contained in:
34
.gitignore
vendored
Normal file
34
.gitignore
vendored
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# dependencies (bun install)
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# output
|
||||||
|
out
|
||||||
|
dist
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# code coverage
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# logs
|
||||||
|
logs
|
||||||
|
_.log
|
||||||
|
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# caches
|
||||||
|
.eslintcache
|
||||||
|
.cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# IntelliJ based IDEs
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Finder (MacOS) folder config
|
||||||
|
.DS_Store
|
||||||
61
README.md
Normal file
61
README.md
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
# API CodeGen
|
||||||
|
|
||||||
|
CLI утилита для генерации TypeScript API клиента из OpenAPI спецификации.
|
||||||
|
|
||||||
|
## Установка
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Использование
|
||||||
|
|
||||||
|
```bash
|
||||||
|
api-codegen -u <URL> -i <INPUT> -o <OUTPUT> [-n <NAME>]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Аргументы:**
|
||||||
|
- `-u, --url <url>` - Базовый URL API
|
||||||
|
- `-i, --input <path>` - Путь к OpenAPI файлу (локальный или URL)
|
||||||
|
- `-o, --output <path>` - Директория для сохранения файлов
|
||||||
|
- `-n, --name <name>` - Имя сгенерированного файла (опционально)
|
||||||
|
|
||||||
|
**Примеры:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Локальный файл
|
||||||
|
api-codegen -u https://api.example.com -i ./openapi.json -o ./src/api
|
||||||
|
|
||||||
|
# URL на спецификацию
|
||||||
|
api-codegen -u https://api.example.com -i https://petstore.swagger.io/v2/swagger.json -o ./src/api
|
||||||
|
|
||||||
|
# С кастомным именем файла
|
||||||
|
api-codegen -u https://api.example.com -i ./openapi.json -o ./src/api -n MyApiClient
|
||||||
|
```
|
||||||
|
|
||||||
|
## Пример использования
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Api, HttpClient } from './src/api/Api';
|
||||||
|
|
||||||
|
const httpClient = new HttpClient();
|
||||||
|
httpClient.setSecurityData({ token: 'jwt-token' });
|
||||||
|
|
||||||
|
const api = new Api(httpClient);
|
||||||
|
|
||||||
|
// GET запрос
|
||||||
|
const user = await api.auth.getProfile();
|
||||||
|
|
||||||
|
// POST запрос
|
||||||
|
const result = await api.auth.login({ email, password });
|
||||||
|
|
||||||
|
// React + SWR
|
||||||
|
function Profile() {
|
||||||
|
const { data } = useSWR('/auth/me', () => api.auth.getProfile());
|
||||||
|
return <div>{data?.email}</div>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Лицензия
|
||||||
|
|
||||||
|
MIT
|
||||||
227
bun.lock
Normal file
227
bun.lock
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"name": "api-codegen",
|
||||||
|
"dependencies": {
|
||||||
|
"@biomejs/wasm-bundler": "^2.3.0",
|
||||||
|
"@biomejs/wasm-web": "^2.3.0",
|
||||||
|
"chalk": "^5.3.0",
|
||||||
|
"commander": "^12.1.0",
|
||||||
|
"ejs": "^3.1.10",
|
||||||
|
"js-yaml": "^4.1.0",
|
||||||
|
"swagger-typescript-api": "^13.0.22",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "latest",
|
||||||
|
"@types/ejs": "^3.1.5",
|
||||||
|
"@types/js-yaml": "^4.0.9",
|
||||||
|
"@types/node": "^22.10.2",
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@biomejs/js-api": ["@biomejs/js-api@3.0.0", "", { "peerDependencies": { "@biomejs/wasm-bundler": "^2.2.0", "@biomejs/wasm-nodejs": "^2.2.0", "@biomejs/wasm-web": "^2.2.0" }, "optionalPeers": ["@biomejs/wasm-bundler", "@biomejs/wasm-nodejs", "@biomejs/wasm-web"] }, "sha512-5QcGJFj9IO+yXl76ICjvkdE38uxRcTDsBzcCZHEZ+ma+Te/nbvJg4A3KtAds9HCrEF0JKLWiyjMhAbqazuJvYA=="],
|
||||||
|
|
||||||
|
"@biomejs/wasm-bundler": ["@biomejs/wasm-bundler@2.3.0", "", {}, "sha512-rk4EkBd37o8UzIKMo39n/WXnnTA9bSlhOSZ93IAfTHuLqjG9p1BNtosYheH/7VyKn/PncgmiMQ7xboqzWK8XqA=="],
|
||||||
|
|
||||||
|
"@biomejs/wasm-nodejs": ["@biomejs/wasm-nodejs@2.2.6", "", {}, "sha512-lUEcvW+2eyMTgCofknBT04AvY7KkQSqKe3Nv40+ZxWVlStsPB0v2RWLu7xks69Yxcb3TfNGsfq21OWkdrmO2NQ=="],
|
||||||
|
|
||||||
|
"@biomejs/wasm-web": ["@biomejs/wasm-web@2.3.0", "", {}, "sha512-P48lp1O4jO89VSCrjf5VpRFJNNl+vaB/bLtST5E3mvalbOjUB6pE0byfTxL1ZRt8TLmFUKfLZbnnyC8ON5KNAA=="],
|
||||||
|
|
||||||
|
"@exodus/schemasafe": ["@exodus/schemasafe@1.3.0", "", {}, "sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw=="],
|
||||||
|
|
||||||
|
"@types/bun": ["@types/bun@1.3.1", "", { "dependencies": { "bun-types": "1.3.1" } }, "sha512-4jNMk2/K9YJtfqwoAa28c8wK+T7nvJFOjxI4h/7sORWcypRNxBpr+TPNaCfVWq70tLCJsqoFwcf0oI0JU/fvMQ=="],
|
||||||
|
|
||||||
|
"@types/ejs": ["@types/ejs@3.1.5", "", {}, "sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg=="],
|
||||||
|
|
||||||
|
"@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="],
|
||||||
|
|
||||||
|
"@types/lodash": ["@types/lodash@4.17.20", "", {}, "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA=="],
|
||||||
|
|
||||||
|
"@types/node": ["@types/node@22.18.12", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-BICHQ67iqxQGFSzfCFTT7MRQ5XcBjG5aeKh5Ok38UBbPe5fxTyE+aHFxwVrGyr8GNlqFMLKD1D3P2K/1ks8tog=="],
|
||||||
|
|
||||||
|
"@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],
|
||||||
|
|
||||||
|
"@types/swagger-schema-official": ["@types/swagger-schema-official@2.0.25", "", {}, "sha512-T92Xav+Gf/Ik1uPW581nA+JftmjWPgskw/WBf4TJzxRG/SJ+DfNnNE+WuZ4mrXuzflQMqMkm1LSYjzYW7MB1Cg=="],
|
||||||
|
|
||||||
|
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||||
|
|
||||||
|
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||||
|
|
||||||
|
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||||
|
|
||||||
|
"async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
|
||||||
|
|
||||||
|
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||||
|
|
||||||
|
"brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||||
|
|
||||||
|
"bun-types": ["bun-types@1.3.1", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw=="],
|
||||||
|
|
||||||
|
"c12": ["c12@3.3.1", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^17.2.3", "exsolve": "^1.0.7", "giget": "^2.0.0", "jiti": "^2.6.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.0.0", "pkg-types": "^2.3.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, "sha512-LcWQ01LT9tkoUINHgpIOv3mMs+Abv7oVCrtpMRi1PaapVEpWoMga5WuT7/DqFTu7URP9ftbOmimNw1KNIGh9DQ=="],
|
||||||
|
|
||||||
|
"call-me-maybe": ["call-me-maybe@1.0.2", "", {}, "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ=="],
|
||||||
|
|
||||||
|
"chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
||||||
|
|
||||||
|
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
||||||
|
|
||||||
|
"citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="],
|
||||||
|
|
||||||
|
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
|
||||||
|
|
||||||
|
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||||
|
|
||||||
|
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||||
|
|
||||||
|
"commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="],
|
||||||
|
|
||||||
|
"confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="],
|
||||||
|
|
||||||
|
"consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
|
||||||
|
|
||||||
|
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||||
|
|
||||||
|
"defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
|
||||||
|
|
||||||
|
"destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="],
|
||||||
|
|
||||||
|
"dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="],
|
||||||
|
|
||||||
|
"ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
|
||||||
|
|
||||||
|
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||||
|
|
||||||
|
"es6-promise": ["es6-promise@3.3.1", "", {}, "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg=="],
|
||||||
|
|
||||||
|
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||||
|
|
||||||
|
"eta": ["eta@3.5.0", "", {}, "sha512-e3x3FBvGzeCIHhF+zhK8FZA2vC5uFn6b4HJjegUbIWrDb4mJ7JjTGMJY9VGIbRVpmSwHopNiaJibhjIr+HfLug=="],
|
||||||
|
|
||||||
|
"exsolve": ["exsolve@1.0.7", "", {}, "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw=="],
|
||||||
|
|
||||||
|
"fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="],
|
||||||
|
|
||||||
|
"filelist": ["filelist@1.0.4", "", { "dependencies": { "minimatch": "^5.0.1" } }, "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q=="],
|
||||||
|
|
||||||
|
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
|
||||||
|
|
||||||
|
"giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="],
|
||||||
|
|
||||||
|
"http2-client": ["http2-client@1.3.5", "", {}, "sha512-EC2utToWl4RKfs5zd36Mxq7nzHHBuomZboI0yYL6Y0RmBgT7Sgkq4rQ0ezFTYoIsSs7Tm9SJe+o2FcAg6GBhGA=="],
|
||||||
|
|
||||||
|
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||||
|
|
||||||
|
"jake": ["jake@10.9.4", "", { "dependencies": { "async": "^3.2.6", "filelist": "^1.0.4", "picocolors": "^1.1.1" }, "bin": { "jake": "bin/cli.js" } }, "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA=="],
|
||||||
|
|
||||||
|
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
||||||
|
|
||||||
|
"js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
|
||||||
|
|
||||||
|
"lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
|
||||||
|
|
||||||
|
"minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="],
|
||||||
|
|
||||||
|
"nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="],
|
||||||
|
|
||||||
|
"node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
|
||||||
|
|
||||||
|
"node-fetch-h2": ["node-fetch-h2@2.3.0", "", { "dependencies": { "http2-client": "^1.2.5" } }, "sha512-ofRW94Ab0T4AOh5Fk8t0h8OBWrmjb0SSB20xh1H8YnPV9EJ+f5AMoYSUQ2zgJ4Iq2HAK0I2l5/Nequ8YzFS3Hg=="],
|
||||||
|
|
||||||
|
"node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
|
||||||
|
|
||||||
|
"node-readfiles": ["node-readfiles@0.2.0", "", { "dependencies": { "es6-promise": "^3.2.1" } }, "sha512-SU00ZarexNlE4Rjdm83vglt5Y9yiQ+XI1XpflWlb7q7UTN1JUItm69xMeiQCTxtTfnzt+83T8Cx+vI2ED++VDA=="],
|
||||||
|
|
||||||
|
"nypm": ["nypm@0.6.2", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.2", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "tinyexec": "^1.0.1" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g=="],
|
||||||
|
|
||||||
|
"oas-kit-common": ["oas-kit-common@1.0.8", "", { "dependencies": { "fast-safe-stringify": "^2.0.7" } }, "sha512-pJTS2+T0oGIwgjGpw7sIRU8RQMcUoKCDWFLdBqKB2BNmGpbBMH2sdqAaOXUg8OzonZHU0L7vfJu1mJFEiYDWOQ=="],
|
||||||
|
|
||||||
|
"oas-linter": ["oas-linter@3.2.2", "", { "dependencies": { "@exodus/schemasafe": "^1.0.0-rc.2", "should": "^13.2.1", "yaml": "^1.10.0" } }, "sha512-KEGjPDVoU5K6swgo9hJVA/qYGlwfbFx+Kg2QB/kd7rzV5N8N5Mg6PlsoCMohVnQmo+pzJap/F610qTodKzecGQ=="],
|
||||||
|
|
||||||
|
"oas-resolver": ["oas-resolver@2.5.6", "", { "dependencies": { "node-fetch-h2": "^2.3.0", "oas-kit-common": "^1.0.8", "reftools": "^1.1.9", "yaml": "^1.10.0", "yargs": "^17.0.1" }, "bin": { "resolve": "resolve.js" } }, "sha512-Yx5PWQNZomfEhPPOphFbZKi9W93CocQj18NlD2Pa4GWZzdZpSJvYwoiuurRI7m3SpcChrnO08hkuQDL3FGsVFQ=="],
|
||||||
|
|
||||||
|
"oas-schema-walker": ["oas-schema-walker@1.1.5", "", {}, "sha512-2yucenq1a9YPmeNExoUa9Qwrt9RFkjqaMAA1X+U7sbb0AqBeTIdMHky9SQQ6iN94bO5NW0W4TRYXerG+BdAvAQ=="],
|
||||||
|
|
||||||
|
"oas-validator": ["oas-validator@5.0.8", "", { "dependencies": { "call-me-maybe": "^1.0.1", "oas-kit-common": "^1.0.8", "oas-linter": "^3.2.2", "oas-resolver": "^2.5.6", "oas-schema-walker": "^1.1.5", "reftools": "^1.1.9", "should": "^13.2.1", "yaml": "^1.10.0" } }, "sha512-cu20/HE5N5HKqVygs3dt94eYJfBi0TsZvPVXDhbXQHiEityDN+RROTleefoKRKKJ9dFAF2JBkDHgvWj0sjKGmw=="],
|
||||||
|
|
||||||
|
"ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="],
|
||||||
|
|
||||||
|
"openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
|
||||||
|
|
||||||
|
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||||
|
|
||||||
|
"perfect-debounce": ["perfect-debounce@2.0.0", "", {}, "sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow=="],
|
||||||
|
|
||||||
|
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||||
|
|
||||||
|
"pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="],
|
||||||
|
|
||||||
|
"rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="],
|
||||||
|
|
||||||
|
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||||
|
|
||||||
|
"reftools": ["reftools@1.1.9", "", {}, "sha512-OVede/NQE13xBQ+ob5CKd5KyeJYU2YInb1bmV4nRoOfquZPkAkxuOXicSe1PvqIuZZ4kD13sPKBbR7UFDmli6w=="],
|
||||||
|
|
||||||
|
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
|
||||||
|
|
||||||
|
"should": ["should@13.2.3", "", { "dependencies": { "should-equal": "^2.0.0", "should-format": "^3.0.3", "should-type": "^1.4.0", "should-type-adaptors": "^1.0.1", "should-util": "^1.0.0" } }, "sha512-ggLesLtu2xp+ZxI+ysJTmNjh2U0TsC+rQ/pfED9bUZZ4DKefP27D+7YJVVTvKsmjLpIi9jAa7itwDGkDDmt1GQ=="],
|
||||||
|
|
||||||
|
"should-equal": ["should-equal@2.0.0", "", { "dependencies": { "should-type": "^1.4.0" } }, "sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA=="],
|
||||||
|
|
||||||
|
"should-format": ["should-format@3.0.3", "", { "dependencies": { "should-type": "^1.3.0", "should-type-adaptors": "^1.0.1" } }, "sha512-hZ58adtulAk0gKtua7QxevgUaXTTXxIi8t41L3zo9AHvjXO1/7sdLECuHeIN2SRtYXpNkmhoUP2pdeWgricQ+Q=="],
|
||||||
|
|
||||||
|
"should-type": ["should-type@1.4.0", "", {}, "sha512-MdAsTu3n25yDbIe1NeN69G4n6mUnJGtSJHygX3+oN0ZbO3DTiATnf7XnYJdGT42JCXurTb1JI0qOBR65shvhPQ=="],
|
||||||
|
|
||||||
|
"should-type-adaptors": ["should-type-adaptors@1.1.0", "", { "dependencies": { "should-type": "^1.3.0", "should-util": "^1.0.0" } }, "sha512-JA4hdoLnN+kebEp2Vs8eBe9g7uy0zbRo+RMcU0EsNy+R+k049Ki+N5tT5Jagst2g7EAja+euFuoXFCa8vIklfA=="],
|
||||||
|
|
||||||
|
"should-util": ["should-util@1.0.1", "", {}, "sha512-oXF8tfxx5cDk8r2kYqlkUJzZpDBqVY/II2WhvU0n9Y3XYvAYRmeaf1PvvIvTgPnv4KJ+ES5M0PyDq5Jp+Ygy2g=="],
|
||||||
|
|
||||||
|
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||||
|
|
||||||
|
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
|
|
||||||
|
"swagger-schema-official": ["swagger-schema-official@2.0.0-bab6bed", "", {}, "sha512-rCC0NWGKr/IJhtRuPq/t37qvZHI/mH4I4sxflVM+qgVe5Z2uOCivzWaVbuioJaB61kvm5UvB7b49E+oBY0M8jA=="],
|
||||||
|
|
||||||
|
"swagger-typescript-api": ["swagger-typescript-api@13.2.16", "", { "dependencies": { "@biomejs/js-api": "3.0.0", "@biomejs/wasm-nodejs": "2.2.6", "@types/lodash": "^4.17.20", "@types/swagger-schema-official": "^2.0.25", "c12": "^3.3.0", "citty": "^0.1.6", "consola": "^3.4.2", "eta": "^3.5.0", "lodash": "^4.17.21", "nanoid": "^5.1.6", "openapi-types": "^12.1.3", "swagger-schema-official": "2.0.0-bab6bed", "swagger2openapi": "^7.0.8", "typescript": "~5.9.3", "yaml": "^2.8.1" }, "bin": { "sta": "./dist/cli.js", "swagger-typescript-api": "./dist/cli.js" } }, "sha512-PbjfCbNMx1mxqLamUpMA96fl2HJQh9Q5qRWyVRXmZslRJFHBkMfAj7OnEv6IOtSnmD+TXp073yBhKWiUxqeLhQ=="],
|
||||||
|
|
||||||
|
"swagger2openapi": ["swagger2openapi@7.0.8", "", { "dependencies": { "call-me-maybe": "^1.0.1", "node-fetch": "^2.6.1", "node-fetch-h2": "^2.3.0", "node-readfiles": "^0.2.0", "oas-kit-common": "^1.0.8", "oas-resolver": "^2.5.6", "oas-schema-walker": "^1.1.5", "oas-validator": "^5.0.8", "reftools": "^1.1.9", "yaml": "^1.10.0", "yargs": "^17.0.1" }, "bin": { "swagger2openapi": "swagger2openapi.js", "oas-validate": "oas-validate.js", "boast": "boast.js" } }, "sha512-upi/0ZGkYgEcLeGieoz8gT74oWHA0E7JivX7aN9mAf+Tc7BQoRBvnIGHoPDw+f9TXTW4s6kGYCZJtauP6OYp7g=="],
|
||||||
|
|
||||||
|
"tinyexec": ["tinyexec@1.0.1", "", {}, "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw=="],
|
||||||
|
|
||||||
|
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
|
||||||
|
|
||||||
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
|
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||||
|
|
||||||
|
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
|
||||||
|
|
||||||
|
"whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
|
||||||
|
|
||||||
|
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||||
|
|
||||||
|
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
|
||||||
|
|
||||||
|
"yaml": ["yaml@2.8.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw=="],
|
||||||
|
|
||||||
|
"yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
|
||||||
|
|
||||||
|
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
|
||||||
|
|
||||||
|
"bun-types/@types/node": ["@types/node@24.9.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg=="],
|
||||||
|
|
||||||
|
"oas-linter/yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="],
|
||||||
|
|
||||||
|
"oas-resolver/yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="],
|
||||||
|
|
||||||
|
"oas-validator/yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="],
|
||||||
|
|
||||||
|
"swagger2openapi/yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="],
|
||||||
|
|
||||||
|
"bun-types/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
1
cdn-api.json
Normal file
1
cdn-api.json
Normal file
File diff suppressed because one or more lines are too long
282
example-hooks.tsx
Normal file
282
example-hooks.tsx
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
/**
|
||||||
|
* Примеры использования сгенерированных use* функций
|
||||||
|
* с SWR и React Query
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Api, HttpClient } from './output/Api';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// НАСТРОЙКА API КЛИЕНТА
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
const httpClient = new HttpClient({
|
||||||
|
baseUrl: 'https://cdn.example.com',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Устанавливаем токен (например, из localStorage)
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const token = localStorage.getItem('auth_token');
|
||||||
|
if (token) {
|
||||||
|
httpClient.setSecurityData({ token });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const api = new Api(httpClient);
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// ПРИМЕР 1: ИСПОЛЬЗОВАНИЕ С SWR
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
// Простой GET запрос без параметров
|
||||||
|
function UserProfile() {
|
||||||
|
const profileConfig = api.auth.useGetProfile();
|
||||||
|
|
||||||
|
const { data, error, isLoading } = useSWR(
|
||||||
|
profileConfig.path, // Ключ для кеша
|
||||||
|
() => api.auth.getProfile() // Функция для загрузки данных
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isLoading) return <div>Загрузка...</div>;
|
||||||
|
if (error) return <div>Ошибка: {error.message}</div>;
|
||||||
|
if (!data) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Профиль пользователя</h1>
|
||||||
|
<p>Email: {data.email}</p>
|
||||||
|
<p>Имя: {data.firstName} {data.lastName}</p>
|
||||||
|
<p>Email подтверждён: {data.isEmailVerified ? 'Да' : 'Нет'}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET запрос с параметрами
|
||||||
|
function ProjectDetails({ projectId }: { projectId: string }) {
|
||||||
|
const projectConfig = api.projects.useFindOne({ id: projectId });
|
||||||
|
|
||||||
|
const { data: project, error, isLoading } = useSWR(
|
||||||
|
[projectConfig.path, projectId], // Составной ключ
|
||||||
|
() => api.projects.findOne({ id: projectId })
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isLoading) return <div>Загрузка проекта...</div>;
|
||||||
|
if (error) return <div>Ошибка: {error.message}</div>;
|
||||||
|
if (!project) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2>{project.name}</h2>
|
||||||
|
<p>{project.description}</p>
|
||||||
|
<p>Bucket: {project.s3Bucket}</p>
|
||||||
|
<p>Region: {project.s3Region}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Список с автоматической ревалидацией
|
||||||
|
function ProjectsList() {
|
||||||
|
const projectsConfig = api.projects.useFindAll();
|
||||||
|
|
||||||
|
const { data: projects, error, isLoading, mutate } = useSWR(
|
||||||
|
projectsConfig.path,
|
||||||
|
() => api.projects.findAll(),
|
||||||
|
{
|
||||||
|
refreshInterval: 5000, // Обновлять каждые 5 секунд
|
||||||
|
revalidateOnFocus: true, // Обновлять при фокусе на окно
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleCreateProject = async () => {
|
||||||
|
await api.projects.create({
|
||||||
|
name: 'Новый проект',
|
||||||
|
description: 'Описание',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обновляем список
|
||||||
|
mutate();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) return <div>Загрузка списка...</div>;
|
||||||
|
if (error) return <div>Ошибка: {error.message}</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2>Мои проекты ({projects?.length || 0})</h2>
|
||||||
|
<button onClick={handleCreateProject}>Создать проект</button>
|
||||||
|
<ul>
|
||||||
|
{projects?.map((project) => (
|
||||||
|
<li key={project.id}>
|
||||||
|
{project.name} - {project.slug}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// ПРИМЕР 2: ИСПОЛЬЗОВАНИЕ С REACT QUERY
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
function UserProfileWithReactQuery() {
|
||||||
|
const profileConfig = api.auth.useGetProfile();
|
||||||
|
|
||||||
|
const { data, error, isLoading } = useQuery({
|
||||||
|
queryKey: [profileConfig.path],
|
||||||
|
queryFn: () => api.auth.getProfile(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isLoading) return <div>Загрузка...</div>;
|
||||||
|
if (error) return <div>Ошибка</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>{data?.firstName} {data?.lastName}</h1>
|
||||||
|
<p>{data?.email}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProjectsListWithReactQuery() {
|
||||||
|
const projectsConfig = api.projects.useFindAll();
|
||||||
|
|
||||||
|
const { data: projects, isLoading } = useQuery({
|
||||||
|
queryKey: [projectsConfig.path],
|
||||||
|
queryFn: () => api.projects.findAll(),
|
||||||
|
staleTime: 5 * 60 * 1000, // 5 минут
|
||||||
|
refetchInterval: 30000, // Обновлять каждые 30 секунд
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isLoading) return <div>Загрузка...</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2>Проекты</h2>
|
||||||
|
{projects?.map((p) => (
|
||||||
|
<div key={p.id}>{p.name}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// ПРИМЕР 3: УСЛОВНАЯ ЗАГРУЗКА
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
function ConditionalProfile({ userId }: { userId?: string }) {
|
||||||
|
const profileConfig = api.auth.useGetProfile();
|
||||||
|
|
||||||
|
const { data } = useSWR(
|
||||||
|
// Загружаем только если есть userId
|
||||||
|
userId ? profileConfig.path : null,
|
||||||
|
() => api.auth.getProfile()
|
||||||
|
);
|
||||||
|
|
||||||
|
return data ? <div>{data.email}</div> : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// ПРИМЕР 4: ЗАВИСИМЫЕ ЗАПРОСЫ
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
function DependentQueries() {
|
||||||
|
// Сначала получаем список проектов
|
||||||
|
const projectsConfig = api.projects.useFindAll();
|
||||||
|
const { data: projects } = useSWR(
|
||||||
|
projectsConfig.path,
|
||||||
|
() => api.projects.findAll()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Затем получаем первый проект (только когда список загружен)
|
||||||
|
const firstProjectId = projects?.[0]?.id;
|
||||||
|
const projectConfig = firstProjectId
|
||||||
|
? api.projects.useFindOne({ id: firstProjectId })
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const { data: firstProject } = useSWR(
|
||||||
|
projectConfig ? [projectConfig.path, firstProjectId] : null,
|
||||||
|
() => firstProjectId ? api.projects.findOne({ id: firstProjectId }) : null
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h3>Всего проектов: {projects?.length || 0}</h3>
|
||||||
|
{firstProject && (
|
||||||
|
<div>
|
||||||
|
<h4>Первый проект:</h4>
|
||||||
|
<p>{firstProject.name}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// ПРИМЕР 5: СОЗДАНИЕ ХУКА-ОБЁРТКИ
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
// Универсальный хук для всех GET запросов
|
||||||
|
function useApiQuery<T>(
|
||||||
|
useConfigFn: () => { path: string; method: 'GET'; secure?: boolean },
|
||||||
|
apiFn: () => Promise<T>,
|
||||||
|
options?: Parameters<typeof useSWR>[2]
|
||||||
|
) {
|
||||||
|
const config = useConfigFn();
|
||||||
|
return useSWR<T>(config.path, apiFn, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Использование
|
||||||
|
function MyComponent() {
|
||||||
|
const { data, error, isLoading } = useApiQuery(
|
||||||
|
api.auth.useGetProfile,
|
||||||
|
api.auth.getProfile,
|
||||||
|
{ revalidateOnFocus: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// ПРИМЕР 6: ПОЛЬЗОВАТЕЛЬСКИЙ FETCHER ДЛЯ SWR
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
// Создаём универсальный fetcher
|
||||||
|
const apiFetcher = async (key: string | string[]) => {
|
||||||
|
const path = Array.isArray(key) ? key[0] : key;
|
||||||
|
|
||||||
|
// Находим соответствующий метод API
|
||||||
|
// В реальном приложении можно использовать маппинг
|
||||||
|
return api.auth.getProfile(); // пример
|
||||||
|
};
|
||||||
|
|
||||||
|
// SWRConfig для всего приложения
|
||||||
|
import { SWRConfig } from 'swr';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<SWRConfig
|
||||||
|
value={{
|
||||||
|
fetcher: apiFetcher,
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
dedupingInterval: 2000,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<UserProfile />
|
||||||
|
<ProjectsList />
|
||||||
|
</SWRConfig>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
UserProfile,
|
||||||
|
ProjectDetails,
|
||||||
|
ProjectsList,
|
||||||
|
UserProfileWithReactQuery,
|
||||||
|
ProjectsListWithReactQuery,
|
||||||
|
ConditionalProfile,
|
||||||
|
DependentQueries,
|
||||||
|
useApiQuery,
|
||||||
|
};
|
||||||
|
|
||||||
136
example.ts
Normal file
136
example.ts
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
/**
|
||||||
|
* Пример использования сгенерированного API клиента
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Api, HttpClient } from './output/Api';
|
||||||
|
|
||||||
|
// 1. Создание HTTP клиента с базовыми настройками
|
||||||
|
const httpClient = new HttpClient({
|
||||||
|
baseUrl: 'https://cdn.example.com', // Базовый URL (уже установлен при генерации)
|
||||||
|
baseApiParams: {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Создание API клиента
|
||||||
|
const api = new Api(httpClient);
|
||||||
|
|
||||||
|
// 3. Пример использования
|
||||||
|
|
||||||
|
async function registerUser() {
|
||||||
|
try {
|
||||||
|
const result = await api.auth.register({
|
||||||
|
email: 'user@example.com',
|
||||||
|
password: 'SecurePassword123',
|
||||||
|
firstName: 'Иван',
|
||||||
|
lastName: 'Иванов',
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Пользователь зарегистрирован:', result);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка регистрации:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loginUser() {
|
||||||
|
try {
|
||||||
|
const result = await api.auth.login({
|
||||||
|
email: 'user@example.com',
|
||||||
|
password: 'SecurePassword123',
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Авторизация успешна');
|
||||||
|
|
||||||
|
// Сохраняем токен для последующих запросов
|
||||||
|
httpClient.setSecurityData({ token: result.access_token });
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка авторизации:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getUserProfile() {
|
||||||
|
try {
|
||||||
|
const profile = await api.auth.getProfile();
|
||||||
|
console.log('Профиль пользователя:', profile);
|
||||||
|
return profile;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка получения профиля:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createProject() {
|
||||||
|
try {
|
||||||
|
const project = await api.projects.create({
|
||||||
|
name: 'Мой CDN проект',
|
||||||
|
description: 'Проект для хранения статических файлов',
|
||||||
|
s3Endpoint: 'https://s3.amazonaws.com',
|
||||||
|
s3Bucket: 'my-cdn-bucket',
|
||||||
|
s3Region: 'us-east-1',
|
||||||
|
s3AccessKey: 'AKIAIOSFODNN7EXAMPLE',
|
||||||
|
s3SecretKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Проект создан:', project);
|
||||||
|
return project;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка создания проекта:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAllProjects() {
|
||||||
|
try {
|
||||||
|
const projects = await api.projects.findAll();
|
||||||
|
console.log('Список проектов:', projects);
|
||||||
|
return projects;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка получения проектов:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Главная функция для демонстрации
|
||||||
|
async function main() {
|
||||||
|
console.log('🚀 Пример использования API клиента\n');
|
||||||
|
|
||||||
|
// 1. Регистрация
|
||||||
|
console.log('1. Регистрация пользователя...');
|
||||||
|
await registerUser();
|
||||||
|
console.log('✅ Готово\n');
|
||||||
|
|
||||||
|
// 2. Авторизация
|
||||||
|
console.log('2. Авторизация...');
|
||||||
|
await loginUser();
|
||||||
|
console.log('✅ Готово\n');
|
||||||
|
|
||||||
|
// 3. Получение профиля
|
||||||
|
console.log('3. Получение профиля...');
|
||||||
|
await getUserProfile();
|
||||||
|
console.log('✅ Готово\n');
|
||||||
|
|
||||||
|
// 4. Создание проекта
|
||||||
|
console.log('4. Создание проекта...');
|
||||||
|
await createProject();
|
||||||
|
console.log('✅ Готово\n');
|
||||||
|
|
||||||
|
// 5. Получение всех проектов
|
||||||
|
console.log('5. Получение списка проектов...');
|
||||||
|
await getAllProjects();
|
||||||
|
console.log('✅ Готово\n');
|
||||||
|
|
||||||
|
console.log('✨ Все операции выполнены успешно!');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запуск примера (раскомментируйте для выполнения)
|
||||||
|
// main().catch(console.error);
|
||||||
|
|
||||||
|
export { registerUser, loginUser, getUserProfile, createProject, getAllProjects };
|
||||||
|
|
||||||
43
package.json
Normal file
43
package.json
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"name": "api-codegen",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "CLI tool to generate TypeScript API client from OpenAPI specification",
|
||||||
|
"type": "module",
|
||||||
|
"bin": {
|
||||||
|
"api-codegen": "./dist/cli.js"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "bun build src/cli.ts --target=node --outdir=dist --format=esm",
|
||||||
|
"dev": "bun run src/cli.ts",
|
||||||
|
"test": "bun test"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@biomejs/wasm-bundler": "^2.3.0",
|
||||||
|
"@biomejs/wasm-web": "^2.3.0",
|
||||||
|
"chalk": "^5.3.0",
|
||||||
|
"commander": "^12.1.0",
|
||||||
|
"ejs": "^3.1.10",
|
||||||
|
"js-yaml": "^4.1.0",
|
||||||
|
"swagger-typescript-api": "^13.0.22"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "latest",
|
||||||
|
"@types/ejs": "^3.1.5",
|
||||||
|
"@types/js-yaml": "^4.0.9",
|
||||||
|
"@types/node": "^22.10.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"openapi",
|
||||||
|
"swagger",
|
||||||
|
"api",
|
||||||
|
"codegen",
|
||||||
|
"typescript",
|
||||||
|
"generator",
|
||||||
|
"cli"
|
||||||
|
],
|
||||||
|
"author": "",
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
52
src/cli.ts
Normal file
52
src/cli.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { Command } from 'commander';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
import { validateConfig, type GeneratorConfig } from './config.js';
|
||||||
|
import { generate } from './generator.js';
|
||||||
|
import { fileExists } from './utils/file.js';
|
||||||
|
|
||||||
|
const program = new Command();
|
||||||
|
|
||||||
|
program
|
||||||
|
.name('api-codegen')
|
||||||
|
.description('Generate TypeScript API client from OpenAPI specification')
|
||||||
|
.version('1.0.0')
|
||||||
|
.requiredOption('-u, --url <url>', 'Base API URL (e.g., https://api.example.com)')
|
||||||
|
.requiredOption('-i, --input <path>', 'Path to OpenAPI specification file (JSON or YAML)')
|
||||||
|
.requiredOption('-o, --output <path>', 'Output directory for generated files')
|
||||||
|
.option('-n, --name <name>', 'Name of generated file (without extension)')
|
||||||
|
.action(async (options) => {
|
||||||
|
try {
|
||||||
|
// Создание конфигурации
|
||||||
|
const config: Partial<GeneratorConfig> = {
|
||||||
|
apiUrl: options.url,
|
||||||
|
inputPath: options.input,
|
||||||
|
outputPath: options.output,
|
||||||
|
fileName: options.name,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Валидация конфигурации
|
||||||
|
validateConfig(config);
|
||||||
|
|
||||||
|
// Проверка существования входного файла (только для локальных файлов)
|
||||||
|
if (!config.inputPath!.startsWith('http://') && !config.inputPath!.startsWith('https://')) {
|
||||||
|
if (!(await fileExists(config.inputPath!))) {
|
||||||
|
console.error(chalk.red(`\n❌ Error: Input file not found: ${config.inputPath}\n`));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Генерация API
|
||||||
|
await generate(config as GeneratorConfig);
|
||||||
|
|
||||||
|
console.log(chalk.green('\n✨ Done!\n'));
|
||||||
|
} catch (error) {
|
||||||
|
console.error(chalk.red('\n❌ Error:'), error instanceof Error ? error.message : error);
|
||||||
|
console.error();
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
program.parse();
|
||||||
|
|
||||||
53
src/config.ts
Normal file
53
src/config.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* Конфигурация генератора API
|
||||||
|
*/
|
||||||
|
export interface GeneratorConfig {
|
||||||
|
/** Базовый URL API */
|
||||||
|
apiUrl: string;
|
||||||
|
/** Путь к файлу OpenAPI спецификации */
|
||||||
|
inputPath: string;
|
||||||
|
/** Путь для сохранения сгенерированных файлов */
|
||||||
|
outputPath: string;
|
||||||
|
/** Имя сгенерированного файла (без расширения) */
|
||||||
|
fileName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Валидация конфигурации генератора
|
||||||
|
*/
|
||||||
|
export function validateConfig(config: Partial<GeneratorConfig>): config is GeneratorConfig {
|
||||||
|
const errors: string[] = [];
|
||||||
|
|
||||||
|
if (!config.apiUrl) {
|
||||||
|
errors.push('API URL is required (--url)');
|
||||||
|
} else if (!isValidUrl(config.apiUrl)) {
|
||||||
|
errors.push('API URL must be a valid URL');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.inputPath) {
|
||||||
|
errors.push('Input path is required (--input)');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.outputPath) {
|
||||||
|
errors.push('Output path is required (--output)');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
throw new Error(`Configuration validation failed:\n${errors.map(e => ` - ${e}`).join('\n')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверка валидности URL
|
||||||
|
*/
|
||||||
|
function isValidUrl(url: string): boolean {
|
||||||
|
try {
|
||||||
|
new URL(url);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
130
src/generator.ts
Normal file
130
src/generator.ts
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import { generateApi as swaggerGenerateApi } from 'swagger-typescript-api';
|
||||||
|
import { resolve, join } from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { dirname } from 'path';
|
||||||
|
import { unlink } from 'fs/promises';
|
||||||
|
import type { GeneratorConfig } from './config.js';
|
||||||
|
import { ensureDir, readJsonFile, writeFileWithDirs } from './utils/file.js';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Генерация API клиента из OpenAPI спецификации
|
||||||
|
*/
|
||||||
|
export async function generate(config: GeneratorConfig): Promise<void> {
|
||||||
|
// Убедимся, что выходная директория существует
|
||||||
|
await ensureDir(config.outputPath);
|
||||||
|
|
||||||
|
// Путь к кастомным шаблонам
|
||||||
|
const templatesPath = resolve(__dirname, '../src/templates');
|
||||||
|
|
||||||
|
|
||||||
|
// Читаем и модифицируем OpenAPI спецификацию
|
||||||
|
const inputPath = config.inputPath.startsWith('http://') || config.inputPath.startsWith('https://')
|
||||||
|
? config.inputPath
|
||||||
|
: resolve(config.inputPath);
|
||||||
|
const spec = await readJsonFile<any>(inputPath);
|
||||||
|
|
||||||
|
// Определяем имя файла
|
||||||
|
let fileName = config.fileName;
|
||||||
|
if (!fileName) {
|
||||||
|
// Пытаемся получить имя из OpenAPI спецификации
|
||||||
|
fileName = spec.info?.title
|
||||||
|
? spec.info.title.replace(/[^a-zA-Z0-9]/g, '')
|
||||||
|
: 'Api';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем или обновляем servers с baseUrl
|
||||||
|
spec.servers = [{ url: config.apiUrl }];
|
||||||
|
|
||||||
|
// Сохраняем модифицированную спецификацию во временный файл
|
||||||
|
const tempSpecPath = join(config.outputPath, '.openapi-temp.json');
|
||||||
|
await writeFileWithDirs(tempSpecPath, JSON.stringify(spec, null, 2));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await swaggerGenerateApi({
|
||||||
|
input: tempSpecPath,
|
||||||
|
output: resolve(config.outputPath),
|
||||||
|
fileName: `${fileName}.ts`,
|
||||||
|
httpClientType: 'fetch',
|
||||||
|
modular: false,
|
||||||
|
templates: templatesPath,
|
||||||
|
generateClient: true,
|
||||||
|
generateRouteTypes: true,
|
||||||
|
extractRequestParams: true,
|
||||||
|
extractRequestBody: true,
|
||||||
|
extractEnums: true,
|
||||||
|
cleanOutput: true,
|
||||||
|
singleHttpClient: true,
|
||||||
|
unwrapResponseData: true,
|
||||||
|
defaultResponseAsSuccess: true,
|
||||||
|
enumNamesAsValues: false,
|
||||||
|
moduleNameFirstTag: false,
|
||||||
|
generateUnionEnums: false,
|
||||||
|
extraTemplates: [],
|
||||||
|
addReadonly: false,
|
||||||
|
sortTypes: false,
|
||||||
|
sortRoutes: false,
|
||||||
|
extractResponseError: false,
|
||||||
|
fixInvalidEnumKeyPrefix: 'KEY',
|
||||||
|
silent: false,
|
||||||
|
defaultResponseType: 'void',
|
||||||
|
typePrefix: '',
|
||||||
|
typeSuffix: '',
|
||||||
|
enumKeyPrefix: '',
|
||||||
|
enumKeySuffix: '',
|
||||||
|
extractingOptions: {
|
||||||
|
requestBodySuffix: ['Payload', 'Body', 'Input'],
|
||||||
|
requestParamsSuffix: ['Params'],
|
||||||
|
responseBodySuffix: ['Data', 'Result', 'Output'],
|
||||||
|
responseErrorSuffix: ['Error', 'Fail', 'Fails', 'ErrorData', 'HttpError', 'BadResponse'],
|
||||||
|
},
|
||||||
|
hooks: {
|
||||||
|
onFormatRouteName: (routeInfo, templateRouteName) => {
|
||||||
|
// Убираем префикс с названием контроллера из имени метода
|
||||||
|
// Например: projectControllerUpdate -> update
|
||||||
|
// authControllerLogin -> login
|
||||||
|
const controllerPattern = /^(\w+)Controller(\w+)$/;
|
||||||
|
const match = templateRouteName.match(controllerPattern);
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
const [, , methodName] = match;
|
||||||
|
// Делаем первую букву строчной
|
||||||
|
return methodName ? methodName.charAt(0).toLowerCase() + methodName.slice(1) : templateRouteName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return templateRouteName;
|
||||||
|
},
|
||||||
|
onInit: (configuration, codeGenProcess) => {
|
||||||
|
// Передаём baseUrl в конфигурацию для шаблонов
|
||||||
|
(configuration as any).apiConfig = (configuration as any).apiConfig || {};
|
||||||
|
(configuration as any).apiConfig.baseUrl = config.apiUrl;
|
||||||
|
return configuration;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Generated files in ${config.outputPath}:`);
|
||||||
|
console.log(` - ${fileName}.ts (API endpoints)`);
|
||||||
|
console.log(' - http-client.ts (HTTP client)');
|
||||||
|
console.log(' - data-contracts.ts (TypeScript types)');
|
||||||
|
|
||||||
|
// Удаляем временный файл
|
||||||
|
try {
|
||||||
|
await unlink(tempSpecPath);
|
||||||
|
} catch (e) {
|
||||||
|
// Игнорируем ошибки удаления
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Generation failed:', error);
|
||||||
|
// Удаляем временный файл даже при ошибке
|
||||||
|
try {
|
||||||
|
await unlink(tempSpecPath);
|
||||||
|
} catch (e) {
|
||||||
|
// Игнорируем ошибки удаления
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
72
src/templates/api.ejs
Normal file
72
src/templates/api.ejs
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<%
|
||||||
|
const { apiConfig, routes, utils, config } = it;
|
||||||
|
const { info, servers, externalDocs } = apiConfig;
|
||||||
|
const { _, require, formatDescription } = utils;
|
||||||
|
|
||||||
|
const server = (servers && servers[0]) || { url: "" };
|
||||||
|
|
||||||
|
const descriptionLines = _.compact([
|
||||||
|
`@title ${info.title || "No title"}`,
|
||||||
|
info.version && `@version ${info.version}`,
|
||||||
|
info.license && `@license ${_.compact([
|
||||||
|
info.license.name,
|
||||||
|
info.license.url && `(${info.license.url})`,
|
||||||
|
]).join(" ")}`,
|
||||||
|
info.termsOfService && `@termsOfService ${info.termsOfService}`,
|
||||||
|
server.url && `@baseUrl ${server.url}`,
|
||||||
|
externalDocs.url && `@externalDocs ${externalDocs.url}`,
|
||||||
|
info.contact && `@contact ${_.compact([
|
||||||
|
info.contact.name,
|
||||||
|
info.contact.email && `<${info.contact.email}>`,
|
||||||
|
info.contact.url && `(${info.contact.url})`,
|
||||||
|
]).join(" ")}`,
|
||||||
|
info.description && " ",
|
||||||
|
info.description && _.replace(formatDescription(info.description), /\n/g, "\n * "),
|
||||||
|
]);
|
||||||
|
|
||||||
|
%>
|
||||||
|
|
||||||
|
import useSWR from "swr";
|
||||||
|
import { fetcher } from "./http-client";
|
||||||
|
|
||||||
|
<% if (config.httpClientType === config.constants.HTTP_CLIENT.AXIOS) { %> import type { AxiosRequestConfig, AxiosResponse } from "axios"; <% } %>
|
||||||
|
|
||||||
|
<% if (descriptionLines.length) { %>
|
||||||
|
/**
|
||||||
|
<% descriptionLines.forEach((descriptionLine) => { %>
|
||||||
|
* <%~ descriptionLine %>
|
||||||
|
|
||||||
|
<% }) %>
|
||||||
|
*/
|
||||||
|
<% } %>
|
||||||
|
export class <%~ config.apiClassName %><SecurityDataType extends unknown><% if (!config.singleHttpClient) { %> extends HttpClient<SecurityDataType> <% } %> {
|
||||||
|
|
||||||
|
<% if(config.singleHttpClient) { %>
|
||||||
|
http: HttpClient<SecurityDataType>;
|
||||||
|
|
||||||
|
constructor (http: HttpClient<SecurityDataType>) {
|
||||||
|
this.http = http;
|
||||||
|
}
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
|
||||||
|
<% if (routes.outOfModule) { %>
|
||||||
|
<% for (const route of routes.outOfModule) { %>
|
||||||
|
|
||||||
|
<%~ includeFile('./procedure-call.ejs', { ...it, route }) %>
|
||||||
|
|
||||||
|
<% } %>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<% if (routes.combined) { %>
|
||||||
|
<% for (const { routes: combinedRoutes = [], moduleName } of routes.combined) { %>
|
||||||
|
<%~ moduleName %> = {
|
||||||
|
<% for (const route of combinedRoutes) { %>
|
||||||
|
|
||||||
|
<%~ includeFile('./procedure-call.ejs', { ...it, route }) %>
|
||||||
|
|
||||||
|
<% } %>
|
||||||
|
}
|
||||||
|
<% } %>
|
||||||
|
<% } %>
|
||||||
|
}
|
||||||
37
src/templates/data-contract-jsdoc.ejs
Normal file
37
src/templates/data-contract-jsdoc.ejs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<%
|
||||||
|
const { data, utils } = it;
|
||||||
|
const { formatDescription, require, _ } = utils;
|
||||||
|
|
||||||
|
const stringify = (value) => _.isObject(value) ? JSON.stringify(value) : _.isString(value) ? `"${value}"` : value;
|
||||||
|
|
||||||
|
const jsDocLines = _.compact([
|
||||||
|
data.title,
|
||||||
|
data.description && formatDescription(data.description),
|
||||||
|
!_.isUndefined(data.deprecated) && data.deprecated && '@deprecated',
|
||||||
|
!_.isUndefined(data.format) && `@format ${data.format}`,
|
||||||
|
!_.isUndefined(data.minimum) && `@min ${data.minimum}`,
|
||||||
|
!_.isUndefined(data.multipleOf) && `@multipleOf ${data.multipleOf}`,
|
||||||
|
!_.isUndefined(data.exclusiveMinimum) && `@exclusiveMin ${data.exclusiveMinimum}`,
|
||||||
|
!_.isUndefined(data.maximum) && `@max ${data.maximum}`,
|
||||||
|
!_.isUndefined(data.minLength) && `@minLength ${data.minLength}`,
|
||||||
|
!_.isUndefined(data.maxLength) && `@maxLength ${data.maxLength}`,
|
||||||
|
!_.isUndefined(data.exclusiveMaximum) && `@exclusiveMax ${data.exclusiveMaximum}`,
|
||||||
|
!_.isUndefined(data.maxItems) && `@maxItems ${data.maxItems}`,
|
||||||
|
!_.isUndefined(data.minItems) && `@minItems ${data.minItems}`,
|
||||||
|
!_.isUndefined(data.uniqueItems) && `@uniqueItems ${data.uniqueItems}`,
|
||||||
|
!_.isUndefined(data.default) && `@default ${stringify(data.default)}`,
|
||||||
|
!_.isUndefined(data.pattern) && `@pattern ${data.pattern}`,
|
||||||
|
!_.isUndefined(data.example) && `@example ${stringify(data.example)}`
|
||||||
|
]).join('\n').split('\n');
|
||||||
|
%>
|
||||||
|
<% if (jsDocLines.every(_.isEmpty)) { %>
|
||||||
|
<% } else if (jsDocLines.length === 1) { %>
|
||||||
|
/** <%~ jsDocLines[0] %> */
|
||||||
|
<% } else if (jsDocLines.length) { %>
|
||||||
|
/**
|
||||||
|
<% for (jsDocLine of jsDocLines) { %>
|
||||||
|
* <%~ jsDocLine %>
|
||||||
|
|
||||||
|
<% } %>
|
||||||
|
*/
|
||||||
|
<% } %>
|
||||||
40
src/templates/data-contracts.ejs
Normal file
40
src/templates/data-contracts.ejs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<%
|
||||||
|
const { modelTypes, utils, config } = it;
|
||||||
|
const { formatDescription, require, _, Ts } = utils;
|
||||||
|
|
||||||
|
|
||||||
|
const buildGenerics = (contract) => {
|
||||||
|
if (!contract.genericArgs || !contract.genericArgs.length) return '';
|
||||||
|
|
||||||
|
return '<' + contract.genericArgs.map(({ name, default: defaultType, extends: extendsType }) => {
|
||||||
|
return [
|
||||||
|
name,
|
||||||
|
extendsType && `extends ${extendsType}`,
|
||||||
|
defaultType && `= ${defaultType}`,
|
||||||
|
].join('')
|
||||||
|
}).join(',') + '>'
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataContractTemplates = {
|
||||||
|
enum: (contract) => {
|
||||||
|
return `enum ${contract.name} {\r\n${contract.content} \r\n }`;
|
||||||
|
},
|
||||||
|
interface: (contract) => {
|
||||||
|
return `interface ${contract.name}${buildGenerics(contract)} {\r\n${contract.content}}`;
|
||||||
|
},
|
||||||
|
type: (contract) => {
|
||||||
|
return `type ${contract.name}${buildGenerics(contract)} = ${contract.content}`;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
%>
|
||||||
|
|
||||||
|
<% if (config.internalTemplateOptions.addUtilRequiredKeysType) { %>
|
||||||
|
type <%~ config.Ts.CodeGenKeyword.UtilRequiredKeys %><T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<% for (const contract of modelTypes) { %>
|
||||||
|
<%~ includeFile('./data-contract-jsdoc.ejs', { ...it, data: { ...contract, ...contract.typeData } }) %>
|
||||||
|
<%~ contract.internal ? '' : 'export'%> <%~ (dataContractTemplates[contract.typeIdentifier] || dataContractTemplates.type)(contract) %>
|
||||||
|
|
||||||
|
|
||||||
|
<% } %>
|
||||||
18
src/templates/enum-data-contract.ejs
Normal file
18
src/templates/enum-data-contract.ejs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<%
|
||||||
|
const { contract, utils, config } = it;
|
||||||
|
const { formatDescription, require, _ } = utils;
|
||||||
|
const { name, $content } = contract;
|
||||||
|
%>
|
||||||
|
<% if (config.generateUnionEnums) { %>
|
||||||
|
export type <%~ name %> = <%~ _.map($content, ({ value }) => value).join(" | ") %>
|
||||||
|
<% } else { %>
|
||||||
|
export enum <%~ name %> {
|
||||||
|
<%~ _.map($content, ({ key, value, description }) => {
|
||||||
|
let formattedDescription = description && formatDescription(description, true);
|
||||||
|
return [
|
||||||
|
formattedDescription && `/** ${formattedDescription} */`,
|
||||||
|
`${key} = ${value}`
|
||||||
|
].filter(Boolean).join("\n");
|
||||||
|
}).join(",\n") %>
|
||||||
|
}
|
||||||
|
<% } %>
|
||||||
250
src/templates/http-client.ejs
Normal file
250
src/templates/http-client.ejs
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
<%
|
||||||
|
const { apiConfig, generateResponses, config } = it;
|
||||||
|
const baseUrl = apiConfig?.baseUrl || "";
|
||||||
|
%>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Фетчер для SWR
|
||||||
|
* Принимает URL и возвращает Promise с данными
|
||||||
|
*/
|
||||||
|
export const fetcher = <T = any>(url: string): Promise<T> => {
|
||||||
|
return fetch(url, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}).then(res => {
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`HTTP Error ${res.status}`);
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export type QueryParamsType = Record<string | number, any>;
|
||||||
|
export type ResponseFormat = keyof Omit<Body, "body" | "bodyUsed">;
|
||||||
|
|
||||||
|
export interface FullRequestParams extends Omit<RequestInit, "body"> {
|
||||||
|
/** set parameter to `true` for call `securityWorker` for this request */
|
||||||
|
secure?: boolean;
|
||||||
|
/** request path */
|
||||||
|
path: string;
|
||||||
|
/** content type of request body */
|
||||||
|
type?: ContentType;
|
||||||
|
/** query params */
|
||||||
|
query?: QueryParamsType;
|
||||||
|
/** format of response (i.e. response.json() -> format: "json") */
|
||||||
|
format?: ResponseFormat;
|
||||||
|
/** request body */
|
||||||
|
body?: unknown;
|
||||||
|
/** base url */
|
||||||
|
baseUrl?: string;
|
||||||
|
/** request cancellation token */
|
||||||
|
cancelToken?: CancelToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RequestParams = Omit<FullRequestParams, "body" | "method" | "query" | "path">
|
||||||
|
|
||||||
|
|
||||||
|
export interface ApiConfig<SecurityDataType = unknown> {
|
||||||
|
baseUrl?: string;
|
||||||
|
baseApiParams?: Omit<RequestParams, "baseUrl" | "cancelToken" | "signal">;
|
||||||
|
securityWorker?: (securityData: SecurityDataType | null) => Promise<RequestParams | void> | RequestParams | void;
|
||||||
|
customFetch?: typeof fetch;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HttpResponse<D extends unknown, E extends unknown = unknown> extends Response {
|
||||||
|
data: D;
|
||||||
|
error: E;
|
||||||
|
}
|
||||||
|
|
||||||
|
type CancelToken = Symbol | string | number;
|
||||||
|
|
||||||
|
export enum ContentType {
|
||||||
|
Json = "application/json",
|
||||||
|
JsonApi = "application/vnd.api+json",
|
||||||
|
FormData = "multipart/form-data",
|
||||||
|
UrlEncoded = "application/x-www-form-urlencoded",
|
||||||
|
Text = "text/plain",
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HttpClient<SecurityDataType = unknown> {
|
||||||
|
public baseUrl: string = "<%~ baseUrl %>";
|
||||||
|
private securityData: SecurityDataType | null = null;
|
||||||
|
private securityWorker?: ApiConfig<SecurityDataType>["securityWorker"];
|
||||||
|
private abortControllers = new Map<CancelToken, AbortController>();
|
||||||
|
private customFetch = (...fetchParams: Parameters<typeof fetch>) => fetch(...fetchParams);
|
||||||
|
|
||||||
|
private baseApiParams: RequestParams = {
|
||||||
|
credentials: 'same-origin',
|
||||||
|
headers: {},
|
||||||
|
redirect: 'follow',
|
||||||
|
referrerPolicy: 'no-referrer',
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(apiConfig: ApiConfig<SecurityDataType> = {}) {
|
||||||
|
Object.assign(this, apiConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
public setSecurityData = (data: SecurityDataType | null) => {
|
||||||
|
this.securityData = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected encodeQueryParam(key: string, value: any) {
|
||||||
|
const encodedKey = encodeURIComponent(key);
|
||||||
|
return `${encodedKey}=${encodeURIComponent(typeof value === "number" ? value : `${value}`)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected addQueryParam(query: QueryParamsType, key: string) {
|
||||||
|
return this.encodeQueryParam(key, query[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected addArrayQueryParam(query: QueryParamsType, key: string) {
|
||||||
|
const value = query[key];
|
||||||
|
return value.map((v: any) => this.encodeQueryParam(key, v)).join("&");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected toQueryString(rawQuery?: QueryParamsType): string {
|
||||||
|
const query = rawQuery || {};
|
||||||
|
const keys = Object.keys(query).filter((key) => "undefined" !== typeof query[key]);
|
||||||
|
return keys
|
||||||
|
.map((key) =>
|
||||||
|
Array.isArray(query[key])
|
||||||
|
? this.addArrayQueryParam(query, key)
|
||||||
|
: this.addQueryParam(query, key),
|
||||||
|
)
|
||||||
|
.join("&");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected addQueryParams(rawQuery?: QueryParamsType): string {
|
||||||
|
const queryString = this.toQueryString(rawQuery);
|
||||||
|
return queryString ? `?${queryString}` : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private contentFormatters: Record<ContentType, (input: any) => any> = {
|
||||||
|
[ContentType.Json]: (input:any) => input !== null && (typeof input === "object" || typeof input === "string") ? JSON.stringify(input) : input,
|
||||||
|
[ContentType.JsonApi]: (input:any) => input !== null && (typeof input === "object" || typeof input === "string") ? JSON.stringify(input) : input,
|
||||||
|
[ContentType.Text]: (input:any) => input !== null && typeof input !== "string" ? JSON.stringify(input) : input,
|
||||||
|
[ContentType.FormData]: (input: any) => {
|
||||||
|
if (input instanceof FormData) {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.keys(input || {}).reduce((formData, key) => {
|
||||||
|
const property = input[key];
|
||||||
|
formData.append(
|
||||||
|
key,
|
||||||
|
property instanceof Blob ?
|
||||||
|
property :
|
||||||
|
typeof property === "object" && property !== null ?
|
||||||
|
JSON.stringify(property) :
|
||||||
|
`${property}`
|
||||||
|
);
|
||||||
|
return formData;
|
||||||
|
}, new FormData());
|
||||||
|
},
|
||||||
|
[ContentType.UrlEncoded]: (input: any) => this.toQueryString(input),
|
||||||
|
}
|
||||||
|
|
||||||
|
protected mergeRequestParams(params1: RequestParams, params2?: RequestParams): RequestParams {
|
||||||
|
return {
|
||||||
|
...this.baseApiParams,
|
||||||
|
...params1,
|
||||||
|
...(params2 || {}),
|
||||||
|
headers: {
|
||||||
|
...(this.baseApiParams.headers || {}),
|
||||||
|
...(params1.headers || {}),
|
||||||
|
...((params2 && params2.headers) || {}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected createAbortSignal = (cancelToken: CancelToken): AbortSignal | undefined => {
|
||||||
|
if (this.abortControllers.has(cancelToken)) {
|
||||||
|
const abortController = this.abortControllers.get(cancelToken);
|
||||||
|
if (abortController) {
|
||||||
|
return abortController.signal;
|
||||||
|
}
|
||||||
|
return void 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const abortController = new AbortController();
|
||||||
|
this.abortControllers.set(cancelToken, abortController);
|
||||||
|
return abortController.signal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abortRequest = (cancelToken: CancelToken) => {
|
||||||
|
const abortController = this.abortControllers.get(cancelToken)
|
||||||
|
|
||||||
|
if (abortController) {
|
||||||
|
abortController.abort();
|
||||||
|
this.abortControllers.delete(cancelToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public request = async <T = any, E = any>({
|
||||||
|
body,
|
||||||
|
secure,
|
||||||
|
path,
|
||||||
|
type,
|
||||||
|
query,
|
||||||
|
format,
|
||||||
|
baseUrl,
|
||||||
|
cancelToken,
|
||||||
|
...params
|
||||||
|
<% if (config.unwrapResponseData) { %>
|
||||||
|
}: FullRequestParams): Promise<T> => {
|
||||||
|
<% } else { %>
|
||||||
|
}: FullRequestParams): Promise<HttpResponse<T, E>> => {
|
||||||
|
<% } %>
|
||||||
|
const secureParams = ((typeof secure === 'boolean' ? secure : this.baseApiParams.secure) && this.securityWorker && await this.securityWorker(this.securityData)) || {};
|
||||||
|
const requestParams = this.mergeRequestParams(params, secureParams);
|
||||||
|
const queryString = query && this.toQueryString(query);
|
||||||
|
const payloadFormatter = this.contentFormatters[type || ContentType.Json];
|
||||||
|
const responseFormat = format || requestParams.format;
|
||||||
|
|
||||||
|
return this.customFetch(
|
||||||
|
`${baseUrl || this.baseUrl || ""}${path}${queryString ? `?${queryString}` : ""}`,
|
||||||
|
{
|
||||||
|
...requestParams,
|
||||||
|
headers: {
|
||||||
|
...(requestParams.headers || {}),
|
||||||
|
...(type && type !== ContentType.FormData ? { "Content-Type": type } : {}),
|
||||||
|
},
|
||||||
|
signal: (cancelToken ? this.createAbortSignal(cancelToken) : requestParams.signal) || null,
|
||||||
|
body: typeof body === "undefined" || body === null ? null : payloadFormatter(body),
|
||||||
|
}
|
||||||
|
).then(async (response) => {
|
||||||
|
const r = response as HttpResponse<T, E>;
|
||||||
|
r.data = (null as unknown) as T;
|
||||||
|
r.error = (null as unknown) as E;
|
||||||
|
|
||||||
|
const responseToParse = responseFormat ? response.clone() : response;
|
||||||
|
const data = !responseFormat ? r : await responseToParse[responseFormat]()
|
||||||
|
.then((data) => {
|
||||||
|
if (r.ok) {
|
||||||
|
r.data = data;
|
||||||
|
} else {
|
||||||
|
r.error = data;
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
r.error = e;
|
||||||
|
return r;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (cancelToken) {
|
||||||
|
this.abortControllers.delete(cancelToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
<% if (!config.disableThrowOnError) { %>
|
||||||
|
if (!response.ok) throw data;
|
||||||
|
<% } %>
|
||||||
|
<% if (config.unwrapResponseData) { %>
|
||||||
|
return data.data;
|
||||||
|
<% } else { %>
|
||||||
|
return data;
|
||||||
|
<% } %>
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
10
src/templates/interface-data-contract.ejs
Normal file
10
src/templates/interface-data-contract.ejs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<%
|
||||||
|
const { contract, utils } = it;
|
||||||
|
const { formatDescription, require, _ } = utils;
|
||||||
|
%>
|
||||||
|
export interface <%~ contract.name %> {
|
||||||
|
<% for (const field of contract.$content) { %>
|
||||||
|
<%~ includeFile('./object-field-jsdoc.ejs', { ...it, field }) %>
|
||||||
|
<%~ field.name %><%~ field.isRequired ? '' : '?' %>: <%~ field.value %><%~ field.isNullable ? ' | null' : ''%>;
|
||||||
|
<% } %>
|
||||||
|
}
|
||||||
28
src/templates/object-field-jsdoc.ejs
Normal file
28
src/templates/object-field-jsdoc.ejs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<%
|
||||||
|
const { field, utils } = it;
|
||||||
|
const { formatDescription, require, _ } = utils;
|
||||||
|
|
||||||
|
const comments = _.uniq(
|
||||||
|
_.compact([
|
||||||
|
field.title,
|
||||||
|
field.description,
|
||||||
|
field.deprecated && ` * @deprecated`,
|
||||||
|
!_.isUndefined(field.format) && `@format ${field.format}`,
|
||||||
|
!_.isUndefined(field.minimum) && `@min ${field.minimum}`,
|
||||||
|
!_.isUndefined(field.maximum) && `@max ${field.maximum}`,
|
||||||
|
!_.isUndefined(field.pattern) && `@pattern ${field.pattern}`,
|
||||||
|
!_.isUndefined(field.example) &&
|
||||||
|
`@example ${_.isObject(field.example) ? JSON.stringify(field.example) : field.example}`,
|
||||||
|
]).reduce((acc, comment) => [...acc, ...comment.split(/\n/g)], []),
|
||||||
|
);
|
||||||
|
%>
|
||||||
|
<% if (comments.length === 1) { %>
|
||||||
|
/** <%~ comments[0] %> */
|
||||||
|
<% } else if (comments.length) { %>
|
||||||
|
/**
|
||||||
|
<% comments.forEach(comment => { %>
|
||||||
|
* <%~ comment %>
|
||||||
|
|
||||||
|
<% }) %>
|
||||||
|
*/
|
||||||
|
<% } %>
|
||||||
140
src/templates/procedure-call.ejs
Normal file
140
src/templates/procedure-call.ejs
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
<%
|
||||||
|
const { utils, route, config } = it;
|
||||||
|
const { requestBodyInfo, responseBodyInfo, specificArgNameResolver } = route;
|
||||||
|
const { _, getInlineParseContent, getParseContent, parseSchema, getComponentByRef, require } = utils;
|
||||||
|
const { parameters, path, method, payload, query, formData, security, requestParams } = route.request;
|
||||||
|
const { type, errorType, contentTypes } = route.response;
|
||||||
|
const { HTTP_CLIENT, RESERVED_REQ_PARAMS_ARG_NAMES } = config.constants;
|
||||||
|
const routeDocs = includeFile("./route-docs", { config, route, utils });
|
||||||
|
const queryName = (query && query.name) || "query";
|
||||||
|
const pathParams = _.values(parameters);
|
||||||
|
const pathParamsNames = _.map(pathParams, "name");
|
||||||
|
|
||||||
|
const isFetchTemplate = config.httpClientType === HTTP_CLIENT.FETCH;
|
||||||
|
|
||||||
|
const requestConfigParam = {
|
||||||
|
name: specificArgNameResolver.resolve(RESERVED_REQ_PARAMS_ARG_NAMES),
|
||||||
|
optional: true,
|
||||||
|
type: "RequestParams",
|
||||||
|
defaultValue: "{}",
|
||||||
|
}
|
||||||
|
|
||||||
|
const argToTmpl = ({ name, optional, type, defaultValue }) => `${name}${!defaultValue && optional ? '?' : ''}: ${type}${defaultValue ? ` = ${defaultValue}` : ''}`;
|
||||||
|
|
||||||
|
const rawWrapperArgs = config.extractRequestParams ?
|
||||||
|
_.compact([
|
||||||
|
requestParams && {
|
||||||
|
name: pathParams.length ? `{ ${_.join(pathParamsNames, ", ")}, ...${queryName} }` : queryName,
|
||||||
|
optional: false,
|
||||||
|
type: getInlineParseContent(requestParams),
|
||||||
|
},
|
||||||
|
...(!requestParams ? pathParams : []),
|
||||||
|
payload,
|
||||||
|
requestConfigParam,
|
||||||
|
]) :
|
||||||
|
_.compact([
|
||||||
|
...pathParams,
|
||||||
|
query,
|
||||||
|
payload,
|
||||||
|
requestConfigParam,
|
||||||
|
])
|
||||||
|
|
||||||
|
const wrapperArgs = _
|
||||||
|
// Sort by optionality
|
||||||
|
.sortBy(rawWrapperArgs, [o => o.optional])
|
||||||
|
.map(argToTmpl)
|
||||||
|
.join(', ')
|
||||||
|
|
||||||
|
// RequestParams["type"]
|
||||||
|
const requestContentKind = {
|
||||||
|
"JSON": "ContentType.Json",
|
||||||
|
"JSON_API": "ContentType.JsonApi",
|
||||||
|
"URL_ENCODED": "ContentType.UrlEncoded",
|
||||||
|
"FORM_DATA": "ContentType.FormData",
|
||||||
|
"TEXT": "ContentType.Text",
|
||||||
|
}
|
||||||
|
// RequestParams["format"]
|
||||||
|
const responseContentKind = {
|
||||||
|
"JSON": '"json"',
|
||||||
|
"IMAGE": '"blob"',
|
||||||
|
"FORM_DATA": isFetchTemplate ? '"formData"' : '"document"'
|
||||||
|
}
|
||||||
|
|
||||||
|
const bodyTmpl = _.get(payload, "name") || null;
|
||||||
|
const queryTmpl = (query != null && queryName) || null;
|
||||||
|
const bodyContentKindTmpl = requestContentKind[requestBodyInfo.contentKind] || null;
|
||||||
|
const responseFormatTmpl = responseContentKind[responseBodyInfo.success && responseBodyInfo.success.schema && responseBodyInfo.success.schema.contentKind] || null;
|
||||||
|
const securityTmpl = security ? 'true' : null;
|
||||||
|
|
||||||
|
const describeReturnType = () => {
|
||||||
|
if (!config.toJS) return "";
|
||||||
|
|
||||||
|
switch(config.httpClientType) {
|
||||||
|
case HTTP_CLIENT.AXIOS: {
|
||||||
|
return `Promise<AxiosResponse<${type}>>`
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return `Promise<HttpResponse<${type}, ${errorType}>`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValidIdentifier = (name) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name);
|
||||||
|
|
||||||
|
%>
|
||||||
|
/**
|
||||||
|
<%~ routeDocs.description %>
|
||||||
|
|
||||||
|
*<% /* Here you can add some other JSDoc tags */ %>
|
||||||
|
|
||||||
|
<%~ routeDocs.lines %>
|
||||||
|
|
||||||
|
*/
|
||||||
|
<% if (isValidIdentifier(route.routeName.usage)) { %><%~ route.routeName.usage %><%~ route.namespace ? ': ' : ' = ' %><% } else { %>"<%~ route.routeName.usage %>"<%~ route.namespace ? ': ' : ' = ' %><% } %>(<%~ wrapperArgs %>)<%~ config.toJS ? `: ${describeReturnType()}` : "" %> =>
|
||||||
|
<%~ config.singleHttpClient ? 'this.http.request' : 'this.request' %><<%~ type %>, <%~ errorType %>>({
|
||||||
|
path: `<%~ path %>`,
|
||||||
|
method: '<%~ _.upperCase(method) %>',
|
||||||
|
<%~ queryTmpl ? `query: ${queryTmpl},` : '' %>
|
||||||
|
<%~ bodyTmpl ? `body: ${bodyTmpl},` : '' %>
|
||||||
|
<%~ securityTmpl ? `secure: ${securityTmpl},` : '' %>
|
||||||
|
<%~ bodyContentKindTmpl ? `type: ${bodyContentKindTmpl},` : '' %>
|
||||||
|
<%~ responseFormatTmpl ? `format: ${responseFormatTmpl},` : '' %>
|
||||||
|
...<%~ _.get(requestConfigParam, "name") %>,
|
||||||
|
})<%~ route.namespace ? ',' : '' %>
|
||||||
|
<%
|
||||||
|
// Генерируем use* функцию для GET запросов
|
||||||
|
const isGetRequest = _.upperCase(method) === 'GET';
|
||||||
|
if (isGetRequest) {
|
||||||
|
const useMethodName = 'use' + _.upperFirst(route.routeName.usage);
|
||||||
|
const argsWithoutParams = rawWrapperArgs.filter(arg => arg.name !== requestConfigParam.name);
|
||||||
|
const useWrapperArgs = _
|
||||||
|
.sortBy(argsWithoutParams, [o => o.optional])
|
||||||
|
.map(argToTmpl)
|
||||||
|
.join(', ');
|
||||||
|
|
||||||
|
// Определяем обязательные параметры для проверки
|
||||||
|
const requiredArgs = argsWithoutParams.filter(arg => !arg.optional);
|
||||||
|
const requiredArgsNames = requiredArgs.map(arg => {
|
||||||
|
// Извлекаем имя из деструктуризации типа "{ id, ...query }"
|
||||||
|
const match = arg.name.match(/^\{\s*([^,}]+)/);
|
||||||
|
return match ? match[1].trim() : arg.name;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Генерируем условие для проверки всех обязательных параметров
|
||||||
|
const hasRequiredArgs = requiredArgsNames.length > 0;
|
||||||
|
const conditionCheck = hasRequiredArgs
|
||||||
|
? requiredArgsNames.join(' && ')
|
||||||
|
: 'true';
|
||||||
|
%>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SWR hook для <%~ route.routeName.usage %>
|
||||||
|
<%~ routeDocs.lines %>
|
||||||
|
*/
|
||||||
|
<% if (isValidIdentifier(useMethodName)) { %><%~ useMethodName %><%~ route.namespace ? ': ' : ' = ' %><% } else { %>"<%~ useMethodName %>"<%~ route.namespace ? ': ' : ' = ' %><% } %>(<%~ useWrapperArgs %>) => {
|
||||||
|
return useSWR<<%~ type %>>(
|
||||||
|
<%~ conditionCheck %> ? `<%~ path %>` : null,
|
||||||
|
fetcher
|
||||||
|
);
|
||||||
|
}<%~ route.namespace ? ',' : '' %>
|
||||||
|
<% } %>
|
||||||
30
src/templates/route-docs.ejs
Normal file
30
src/templates/route-docs.ejs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<%
|
||||||
|
const { config, route, utils } = it;
|
||||||
|
const { _, formatDescription, fmtToJSDocLine, pascalCase, require } = utils;
|
||||||
|
const { raw, request, routeName } = route;
|
||||||
|
|
||||||
|
const jsDocDescription = raw.description ?
|
||||||
|
` * @description ${formatDescription(raw.description, true)}` :
|
||||||
|
fmtToJSDocLine('No description', { eol: false });
|
||||||
|
const jsDocLines = _.compact([
|
||||||
|
_.size(raw.tags) && ` * @tags ${raw.tags.join(", ")}`,
|
||||||
|
` * @name ${pascalCase(routeName.usage)}`,
|
||||||
|
raw.summary && ` * @summary ${raw.summary}`,
|
||||||
|
` * @request ${_.upperCase(request.method)}:${raw.route}`,
|
||||||
|
raw.deprecated && ` * @deprecated`,
|
||||||
|
routeName.duplicate && ` * @originalName ${routeName.original}`,
|
||||||
|
routeName.duplicate && ` * @duplicate`,
|
||||||
|
request.security && ` * @secure`,
|
||||||
|
...(config.generateResponses && raw.responsesTypes.length
|
||||||
|
? raw.responsesTypes.map(
|
||||||
|
({ type, status, description, isSuccess }) =>
|
||||||
|
` * @response \`${status}\` \`${_.replace(_.replace(type, /\/\*/g, "\\*"), /\*\//g, "*\\")}\` ${description}`,
|
||||||
|
)
|
||||||
|
: []),
|
||||||
|
]).map(str => str.trimEnd()).join("\n");
|
||||||
|
|
||||||
|
return {
|
||||||
|
description: jsDocDescription,
|
||||||
|
lines: jsDocLines,
|
||||||
|
}
|
||||||
|
%>
|
||||||
43
src/templates/route-name.ejs
Normal file
43
src/templates/route-name.ejs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<%
|
||||||
|
const { routeInfo, utils } = it;
|
||||||
|
const {
|
||||||
|
operationId,
|
||||||
|
method,
|
||||||
|
route,
|
||||||
|
moduleName,
|
||||||
|
responsesTypes,
|
||||||
|
description,
|
||||||
|
tags,
|
||||||
|
summary,
|
||||||
|
pathArgs,
|
||||||
|
} = routeInfo;
|
||||||
|
const { _, fmtToJSDocLine, require } = utils;
|
||||||
|
|
||||||
|
const methodAliases = {
|
||||||
|
get: (pathName, hasPathInserts) =>
|
||||||
|
_.camelCase(`${pathName}_${hasPathInserts ? "detail" : "list"}`),
|
||||||
|
post: (pathName, hasPathInserts) => _.camelCase(`${pathName}_create`),
|
||||||
|
put: (pathName, hasPathInserts) => _.camelCase(`${pathName}_update`),
|
||||||
|
patch: (pathName, hasPathInserts) => _.camelCase(`${pathName}_partial_update`),
|
||||||
|
delete: (pathName, hasPathInserts) => _.camelCase(`${pathName}_delete`),
|
||||||
|
};
|
||||||
|
|
||||||
|
const createCustomOperationId = (method, route, moduleName) => {
|
||||||
|
const hasPathInserts = /\{(\w){1,}\}$/g.test(route);
|
||||||
|
const splittedRouteBySlash = _.compact(_.replace(route, /\{(\w){1,}\}/g, "").split("/"));
|
||||||
|
const routeParts = (splittedRouteBySlash.length > 1
|
||||||
|
? splittedRouteBySlash.splice(1)
|
||||||
|
: splittedRouteBySlash
|
||||||
|
).join("_");
|
||||||
|
return routeParts.length > 3 && methodAliases[method]
|
||||||
|
? methodAliases[method](routeParts, hasPathInserts)
|
||||||
|
: _.camelCase(_.lowerCase(method) + "_" + [moduleName].join("_")) || "index";
|
||||||
|
};
|
||||||
|
|
||||||
|
if (operationId)
|
||||||
|
return _.camelCase(operationId);
|
||||||
|
if (route === "/")
|
||||||
|
return _.camelCase(`${_.lowerCase(method)}Root`);
|
||||||
|
|
||||||
|
return createCustomOperationId(method, route, moduleName);
|
||||||
|
%>
|
||||||
24
src/templates/route-type.ejs
Normal file
24
src/templates/route-type.ejs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<%
|
||||||
|
const { route, utils, config } = it;
|
||||||
|
const { _, pascalCase, require } = utils;
|
||||||
|
const { query, payload, pathParams, headers } = route.request;
|
||||||
|
|
||||||
|
const routeDocs = includeFile("./route-docs", { config, route, utils });
|
||||||
|
const isValidIdentifier = (name) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name);
|
||||||
|
const routeNamespace = pascalCase(route.routeName.usage);
|
||||||
|
|
||||||
|
%>
|
||||||
|
|
||||||
|
/**
|
||||||
|
<%~ routeDocs.description %>
|
||||||
|
|
||||||
|
<%~ routeDocs.lines %>
|
||||||
|
|
||||||
|
*/
|
||||||
|
export namespace <% if (isValidIdentifier(routeNamespace)) { %><%~ routeNamespace %><% } else { %>"<%~ routeNamespace %>"<% } %> {
|
||||||
|
export type RequestParams = <%~ (pathParams && pathParams.type) || '{}' %>;
|
||||||
|
export type RequestQuery = <%~ (query && query.type) || '{}' %>;
|
||||||
|
export type RequestBody = <%~ (payload && payload.type) || 'never' %>;
|
||||||
|
export type RequestHeaders = <%~ (headers && headers.type) || '{}' %>;
|
||||||
|
export type ResponseBody = <%~ route.response.type %>;
|
||||||
|
}
|
||||||
32
src/templates/route-types.ejs
Normal file
32
src/templates/route-types.ejs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<%
|
||||||
|
const { utils, config, routes, modelTypes } = it;
|
||||||
|
const { _, pascalCase } = utils;
|
||||||
|
const dataContracts = config.modular ? _.map(modelTypes, "name") : [];
|
||||||
|
%>
|
||||||
|
|
||||||
|
<% if (dataContracts.length) { %>
|
||||||
|
import { <%~ dataContracts.join(", ") %> } from "./<%~ config.fileNames.dataContracts %>"
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<%
|
||||||
|
/* TODO: outOfModule, combined should be attributes of route, which will allow to avoid duplication of code */
|
||||||
|
%>
|
||||||
|
|
||||||
|
<% if (routes.outOfModule) { %>
|
||||||
|
<% for (const { routes: outOfModuleRoutes = [] } of routes.outOfModule) { %>
|
||||||
|
<% for (const route of outOfModuleRoutes) { %>
|
||||||
|
<%~ includeFile('./route-type.ejs', { ...it, route }) %>
|
||||||
|
<% } %>
|
||||||
|
<% } %>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<% if (routes.combined) { %>
|
||||||
|
<% for (const { routes: combinedRoutes = [], moduleName } of routes.combined) { %>
|
||||||
|
export namespace <%~ pascalCase(moduleName) %> {
|
||||||
|
<% for (const route of combinedRoutes) { %>
|
||||||
|
<%~ includeFile('./route-type.ejs', { ...it, route }) %>
|
||||||
|
<% } %>
|
||||||
|
}
|
||||||
|
|
||||||
|
<% } %>
|
||||||
|
<% } %>
|
||||||
15
src/templates/type-data-contract.ejs
Normal file
15
src/templates/type-data-contract.ejs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<%
|
||||||
|
const { contract, utils } = it;
|
||||||
|
const { formatDescription, require, _ } = utils;
|
||||||
|
|
||||||
|
%>
|
||||||
|
<% if (contract.$content.length) { %>
|
||||||
|
export type <%~ contract.name %> = {
|
||||||
|
<% for (const field of contract.$content) { %>
|
||||||
|
<%~ includeFile('./object-field-jsdoc.ejs', { ...it, field }) %>
|
||||||
|
<%~ field.field %>;
|
||||||
|
<% } %>
|
||||||
|
}<%~ utils.isNeedToAddNull(contract) ? ' | null' : ''%>
|
||||||
|
<% } else { %>
|
||||||
|
export type <%~ contract.name %> = Record<string, any>;
|
||||||
|
<% } %>
|
||||||
66
src/utils/file.ts
Normal file
66
src/utils/file.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { mkdir, writeFile, readFile, access } from 'fs/promises';
|
||||||
|
import { dirname, join } from 'path';
|
||||||
|
import { constants } from 'fs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверка существования файла
|
||||||
|
*/
|
||||||
|
export async function fileExists(path: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await access(path, constants.F_OK);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Чтение файла как текст
|
||||||
|
*/
|
||||||
|
export async function readTextFile(path: string): Promise<string> {
|
||||||
|
return await readFile(path, 'utf-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Чтение JSON файла
|
||||||
|
*/
|
||||||
|
export async function readJsonFile<T = unknown>(path: string): Promise<T> {
|
||||||
|
// Проверяем, является ли путь URL
|
||||||
|
if (path.startsWith('http://') || path.startsWith('https://')) {
|
||||||
|
// Используем нативный fetch в Node.js 18+
|
||||||
|
const response = await fetch(path);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch OpenAPI spec from ${path}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
const content = await response.text();
|
||||||
|
return JSON.parse(content) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Иначе читаем из локального файла
|
||||||
|
const content = await readTextFile(path);
|
||||||
|
return JSON.parse(content) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Запись файла с автоматическим созданием директорий
|
||||||
|
*/
|
||||||
|
export async function writeFileWithDirs(path: string, content: string): Promise<void> {
|
||||||
|
const dir = dirname(path);
|
||||||
|
await mkdir(dir, { recursive: true });
|
||||||
|
await writeFile(path, content, 'utf-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Создание директории (рекурсивно)
|
||||||
|
*/
|
||||||
|
export async function ensureDir(path: string): Promise<void> {
|
||||||
|
await mkdir(path, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получение абсолютного пути
|
||||||
|
*/
|
||||||
|
export function resolvePath(path: string): string {
|
||||||
|
return join(process.cwd(), path);
|
||||||
|
}
|
||||||
|
|
||||||
29
tsconfig.json
Normal file
29
tsconfig.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
// Environment setup & latest features
|
||||||
|
"lib": ["ESNext"],
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "Preserve",
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"allowJs": true,
|
||||||
|
|
||||||
|
// Bundler mode
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
// Best practices
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
|
||||||
|
// Some stricter flags (disabled by default)
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"noPropertyAccessFromIndexSignature": false
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user