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