mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
feat(server): add swagger setup for public api documentation
Introduces Swagger and OpenAPI documentation generation for the backend API. Configures bearer authentication globally while allowing explicit bypasses for public endpoints. Exports nested DTO classes across several modules so the NestJS Swagger plugin can correctly reflect their schema definitions. Closes #399
This commit is contained in:
@@ -29,6 +29,7 @@ local/
|
|||||||
# Build artifacts
|
# Build artifacts
|
||||||
coverage/
|
coverage/
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
server/src/metadata.ts
|
||||||
|
|
||||||
# Test results (generated artifacts - uploaded to CI, not committed)
|
# Test results (generated artifacts - uploaded to CI, not committed)
|
||||||
test-results/
|
test-results/
|
||||||
|
|||||||
+17
-1
@@ -183,6 +183,21 @@ This starts the server, client, and types watcher concurrently with hot reload.
|
|||||||
|
|
||||||
If PostgreSQL is not running, start it first with `pnpm run db:up`.
|
If PostgreSQL is not running, start it first with `pnpm run db:up`.
|
||||||
|
|
||||||
|
### API Docs
|
||||||
|
|
||||||
|
Generated OpenAPI docs are disabled by default. To enable them for local development, set `SWAGGER_ENABLED=true` in `server/.env` or prefix the dev command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SWAGGER_ENABLED=true pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
When enabled, open:
|
||||||
|
|
||||||
|
| Docs | URL |
|
||||||
|
| ------------ | ----------------------------------- |
|
||||||
|
| Swagger UI | http://localhost:3000/api/docs |
|
||||||
|
| OpenAPI JSON | http://localhost:3000/api/docs-json |
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Running workspaces separately</summary>
|
<summary>Running workspaces separately</summary>
|
||||||
|
|
||||||
@@ -210,13 +225,14 @@ pnpm --filter @bookorbit/types dev
|
|||||||
These are the variables you are most likely to need during development. See `server/.env.example` for the full list with comments.
|
These are the variables you are most likely to need during development. See `server/.env.example` for the full list with comments.
|
||||||
|
|
||||||
| Variable | Purpose | Default (dev) |
|
| Variable | Purpose | Default (dev) |
|
||||||
| --------------- | ---------------------------------- | --------------------------------------------------------- |
|
| ----------------- | ---------------------------------- | --------------------------------------------------------- |
|
||||||
| `DATABASE_URL` | PostgreSQL connection string | `postgres://bookorbit:bookorbit@localhost:5432/bookorbit` |
|
| `DATABASE_URL` | PostgreSQL connection string | `postgres://bookorbit:bookorbit@localhost:5432/bookorbit` |
|
||||||
| `PORT` | Server listen port | `3000` |
|
| `PORT` | Server listen port | `3000` |
|
||||||
| `NODE_ENV` | Runtime mode | `development` |
|
| `NODE_ENV` | Runtime mode | `development` |
|
||||||
| `JWT_SECRET` | Signing key for auth tokens | `change-me-in-production` |
|
| `JWT_SECRET` | Signing key for auth tokens | `change-me-in-production` |
|
||||||
| `APP_DATA_PATH` | Storage for covers, avatars, cache | `../local/data` |
|
| `APP_DATA_PATH` | Storage for covers, avatars, cache | `../local/data` |
|
||||||
| `APP_URL` | Base URL for email links | `http://localhost:5173` |
|
| `APP_URL` | Base URL for email links | `http://localhost:5173` |
|
||||||
|
| `SWAGGER_ENABLED` | Serve Swagger UI and OpenAPI JSON | `false` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Generated
+89
-198
@@ -276,6 +276,9 @@ importers:
|
|||||||
"@nestjs/schedule":
|
"@nestjs/schedule":
|
||||||
specifier: ^6.1.3
|
specifier: ^6.1.3
|
||||||
version: 6.1.3(@nestjs/common@11.1.27(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.27)
|
version: 6.1.3(@nestjs/common@11.1.27(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.27)
|
||||||
|
"@nestjs/swagger":
|
||||||
|
specifier: ^11.4.4
|
||||||
|
version: 11.4.4(@fastify/static@9.1.3)(@nestjs/common@11.1.27(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.27)(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)
|
||||||
"@nestjs/terminus":
|
"@nestjs/terminus":
|
||||||
specifier: ^11.1.1
|
specifier: ^11.1.1
|
||||||
version: 11.1.1(@nestjs/common@11.1.27(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.27)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
version: 11.1.1(@nestjs/common@11.1.27(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.27)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
@@ -3865,6 +3868,12 @@ packages:
|
|||||||
}
|
}
|
||||||
engines: { node: ">=8" }
|
engines: { node: ">=8" }
|
||||||
|
|
||||||
|
"@microsoft/tsdoc@0.16.0":
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==,
|
||||||
|
}
|
||||||
|
|
||||||
"@napi-rs/nice-android-arm-eabi@1.1.1":
|
"@napi-rs/nice-android-arm-eabi@1.1.1":
|
||||||
resolution:
|
resolution:
|
||||||
{
|
{
|
||||||
@@ -4111,6 +4120,22 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
"@nestjs/common": ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0
|
"@nestjs/common": ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0
|
||||||
|
|
||||||
|
"@nestjs/mapped-types@2.1.1":
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-SCCoMEJ6jdeI5h/N+KCVF1+pmg/hmEkNA5nHTS8Gvww7T/LCl4o1gFLinw2iQ60w7slFkszHcGLKGdazVI4F8A==,
|
||||||
|
}
|
||||||
|
peerDependencies:
|
||||||
|
"@nestjs/common": ^10.0.0 || ^11.0.0
|
||||||
|
class-transformer: ^0.4.0 || ^0.5.0
|
||||||
|
class-validator: ^0.13.0 || ^0.14.0 || ^0.15.0
|
||||||
|
reflect-metadata: ^0.1.12 || ^0.2.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
class-transformer:
|
||||||
|
optional: true
|
||||||
|
class-validator:
|
||||||
|
optional: true
|
||||||
|
|
||||||
"@nestjs/passport@11.0.5":
|
"@nestjs/passport@11.0.5":
|
||||||
resolution:
|
resolution:
|
||||||
{
|
{
|
||||||
@@ -4167,6 +4192,26 @@ packages:
|
|||||||
prettier:
|
prettier:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
"@nestjs/swagger@11.4.4":
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-VaIo1ruV2G7b+f2zPzkBSUNy9a/WQ9sg8TLKhWlrTfg4O6U10M/PA7Xi6XMXadOVhwOqoesijba8jH3i/3adrA==,
|
||||||
|
}
|
||||||
|
peerDependencies:
|
||||||
|
"@fastify/static": ^8.0.0 || ^9.0.0
|
||||||
|
"@nestjs/common": ^11.0.1
|
||||||
|
"@nestjs/core": ^11.0.1
|
||||||
|
class-transformer: "*"
|
||||||
|
class-validator: "*"
|
||||||
|
reflect-metadata: ^0.1.12 || ^0.2.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
"@fastify/static":
|
||||||
|
optional: true
|
||||||
|
class-transformer:
|
||||||
|
optional: true
|
||||||
|
class-validator:
|
||||||
|
optional: true
|
||||||
|
|
||||||
"@nestjs/terminus@11.1.1":
|
"@nestjs/terminus@11.1.1":
|
||||||
resolution:
|
resolution:
|
||||||
{
|
{
|
||||||
@@ -4571,136 +4616,6 @@ packages:
|
|||||||
integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==,
|
integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==,
|
||||||
}
|
}
|
||||||
|
|
||||||
"@parcel/watcher-android-arm64@2.5.6":
|
|
||||||
resolution:
|
|
||||||
{
|
|
||||||
integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==,
|
|
||||||
}
|
|
||||||
engines: { node: ">= 10.0.0" }
|
|
||||||
cpu: [arm64]
|
|
||||||
os: [android]
|
|
||||||
|
|
||||||
"@parcel/watcher-darwin-arm64@2.5.6":
|
|
||||||
resolution:
|
|
||||||
{
|
|
||||||
integrity: sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==,
|
|
||||||
}
|
|
||||||
engines: { node: ">= 10.0.0" }
|
|
||||||
cpu: [arm64]
|
|
||||||
os: [darwin]
|
|
||||||
|
|
||||||
"@parcel/watcher-darwin-x64@2.5.6":
|
|
||||||
resolution:
|
|
||||||
{
|
|
||||||
integrity: sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==,
|
|
||||||
}
|
|
||||||
engines: { node: ">= 10.0.0" }
|
|
||||||
cpu: [x64]
|
|
||||||
os: [darwin]
|
|
||||||
|
|
||||||
"@parcel/watcher-freebsd-x64@2.5.6":
|
|
||||||
resolution:
|
|
||||||
{
|
|
||||||
integrity: sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==,
|
|
||||||
}
|
|
||||||
engines: { node: ">= 10.0.0" }
|
|
||||||
cpu: [x64]
|
|
||||||
os: [freebsd]
|
|
||||||
|
|
||||||
"@parcel/watcher-linux-arm-glibc@2.5.6":
|
|
||||||
resolution:
|
|
||||||
{
|
|
||||||
integrity: sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==,
|
|
||||||
}
|
|
||||||
engines: { node: ">= 10.0.0" }
|
|
||||||
cpu: [arm]
|
|
||||||
os: [linux]
|
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
"@parcel/watcher-linux-arm-musl@2.5.6":
|
|
||||||
resolution:
|
|
||||||
{
|
|
||||||
integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==,
|
|
||||||
}
|
|
||||||
engines: { node: ">= 10.0.0" }
|
|
||||||
cpu: [arm]
|
|
||||||
os: [linux]
|
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
"@parcel/watcher-linux-arm64-glibc@2.5.6":
|
|
||||||
resolution:
|
|
||||||
{
|
|
||||||
integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==,
|
|
||||||
}
|
|
||||||
engines: { node: ">= 10.0.0" }
|
|
||||||
cpu: [arm64]
|
|
||||||
os: [linux]
|
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
"@parcel/watcher-linux-arm64-musl@2.5.6":
|
|
||||||
resolution:
|
|
||||||
{
|
|
||||||
integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==,
|
|
||||||
}
|
|
||||||
engines: { node: ">= 10.0.0" }
|
|
||||||
cpu: [arm64]
|
|
||||||
os: [linux]
|
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
"@parcel/watcher-linux-x64-glibc@2.5.6":
|
|
||||||
resolution:
|
|
||||||
{
|
|
||||||
integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==,
|
|
||||||
}
|
|
||||||
engines: { node: ">= 10.0.0" }
|
|
||||||
cpu: [x64]
|
|
||||||
os: [linux]
|
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
"@parcel/watcher-linux-x64-musl@2.5.6":
|
|
||||||
resolution:
|
|
||||||
{
|
|
||||||
integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==,
|
|
||||||
}
|
|
||||||
engines: { node: ">= 10.0.0" }
|
|
||||||
cpu: [x64]
|
|
||||||
os: [linux]
|
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
"@parcel/watcher-win32-arm64@2.5.6":
|
|
||||||
resolution:
|
|
||||||
{
|
|
||||||
integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==,
|
|
||||||
}
|
|
||||||
engines: { node: ">= 10.0.0" }
|
|
||||||
cpu: [arm64]
|
|
||||||
os: [win32]
|
|
||||||
|
|
||||||
"@parcel/watcher-win32-ia32@2.5.6":
|
|
||||||
resolution:
|
|
||||||
{
|
|
||||||
integrity: sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==,
|
|
||||||
}
|
|
||||||
engines: { node: ">= 10.0.0" }
|
|
||||||
cpu: [ia32]
|
|
||||||
os: [win32]
|
|
||||||
|
|
||||||
"@parcel/watcher-win32-x64@2.5.6":
|
|
||||||
resolution:
|
|
||||||
{
|
|
||||||
integrity: sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==,
|
|
||||||
}
|
|
||||||
engines: { node: ">= 10.0.0" }
|
|
||||||
cpu: [x64]
|
|
||||||
os: [win32]
|
|
||||||
|
|
||||||
"@parcel/watcher@2.5.6":
|
|
||||||
resolution:
|
|
||||||
{
|
|
||||||
integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==,
|
|
||||||
}
|
|
||||||
engines: { node: ">= 10.0.0" }
|
|
||||||
|
|
||||||
"@pdf-lib/standard-fonts@1.0.0":
|
"@pdf-lib/standard-fonts@1.0.0":
|
||||||
resolution:
|
resolution:
|
||||||
{
|
{
|
||||||
@@ -5189,6 +5104,12 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
|
"@scarf/scarf@1.4.0":
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==,
|
||||||
|
}
|
||||||
|
|
||||||
"@sec-ant/readable-stream@0.4.1":
|
"@sec-ant/readable-stream@0.4.1":
|
||||||
resolution:
|
resolution:
|
||||||
{
|
{
|
||||||
@@ -11095,12 +11016,6 @@ packages:
|
|||||||
integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==,
|
integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==,
|
||||||
}
|
}
|
||||||
|
|
||||||
node-addon-api@7.1.1:
|
|
||||||
resolution:
|
|
||||||
{
|
|
||||||
integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==,
|
|
||||||
}
|
|
||||||
|
|
||||||
node-emoji@1.11.0:
|
node-emoji@1.11.0:
|
||||||
resolution:
|
resolution:
|
||||||
{
|
{
|
||||||
@@ -13192,6 +13107,12 @@ packages:
|
|||||||
}
|
}
|
||||||
engines: { node: ">=18" }
|
engines: { node: ">=18" }
|
||||||
|
|
||||||
|
swagger-ui-dist@5.32.6:
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-75ttZNaYCLoFPnozPZcTUU6mS3wKT8l7WLjU5zJSHFeJa23i5vtnze6IiCl4jDMPeQTXVXIgovq4M11NNfQvSA==,
|
||||||
|
}
|
||||||
|
|
||||||
symbol-observable@4.0.0:
|
symbol-observable@4.0.0:
|
||||||
resolution:
|
resolution:
|
||||||
{
|
{
|
||||||
@@ -16966,6 +16887,8 @@ snapshots:
|
|||||||
|
|
||||||
"@lukeed/ms@2.0.2": {}
|
"@lukeed/ms@2.0.2": {}
|
||||||
|
|
||||||
|
"@microsoft/tsdoc@0.16.0": {}
|
||||||
|
|
||||||
"@napi-rs/nice-android-arm-eabi@1.1.1":
|
"@napi-rs/nice-android-arm-eabi@1.1.1":
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -17126,6 +17049,14 @@ snapshots:
|
|||||||
"@types/jsonwebtoken": 9.0.10
|
"@types/jsonwebtoken": 9.0.10
|
||||||
jsonwebtoken: 9.0.3
|
jsonwebtoken: 9.0.3
|
||||||
|
|
||||||
|
"@nestjs/mapped-types@2.1.1(@nestjs/common@11.1.27(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)":
|
||||||
|
dependencies:
|
||||||
|
"@nestjs/common": 11.1.27(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
|
reflect-metadata: 0.2.2
|
||||||
|
optionalDependencies:
|
||||||
|
class-transformer: 0.5.1
|
||||||
|
class-validator: 0.15.1
|
||||||
|
|
||||||
"@nestjs/passport@11.0.5(@nestjs/common@11.1.27(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0)":
|
"@nestjs/passport@11.0.5(@nestjs/common@11.1.27(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0)":
|
||||||
dependencies:
|
dependencies:
|
||||||
"@nestjs/common": 11.1.27(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
"@nestjs/common": 11.1.27(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
@@ -17192,6 +17123,22 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- chokidar
|
- chokidar
|
||||||
|
|
||||||
|
"@nestjs/swagger@11.4.4(@fastify/static@9.1.3)(@nestjs/common@11.1.27(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.27)(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)":
|
||||||
|
dependencies:
|
||||||
|
"@microsoft/tsdoc": 0.16.0
|
||||||
|
"@nestjs/common": 11.1.27(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
|
"@nestjs/core": 11.1.27(@nestjs/common@11.1.27(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.27)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
|
"@nestjs/mapped-types": 2.1.1(@nestjs/common@11.1.27(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)
|
||||||
|
js-yaml: 4.1.1
|
||||||
|
lodash: 4.18.1
|
||||||
|
path-to-regexp: 8.4.2
|
||||||
|
reflect-metadata: 0.2.2
|
||||||
|
swagger-ui-dist: 5.32.6
|
||||||
|
optionalDependencies:
|
||||||
|
"@fastify/static": 9.1.3
|
||||||
|
class-transformer: 0.5.1
|
||||||
|
class-validator: 0.15.1
|
||||||
|
|
||||||
"@nestjs/terminus@11.1.1(@nestjs/common@11.1.27(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.27)(reflect-metadata@0.2.2)(rxjs@7.8.2)":
|
"@nestjs/terminus@11.1.1(@nestjs/common@11.1.27(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.27)(reflect-metadata@0.2.2)(rxjs@7.8.2)":
|
||||||
dependencies:
|
dependencies:
|
||||||
"@nestjs/common": 11.1.27(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
"@nestjs/common": 11.1.27(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
@@ -17367,66 +17314,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@noble/hashes": 1.8.0
|
"@noble/hashes": 1.8.0
|
||||||
|
|
||||||
"@parcel/watcher-android-arm64@2.5.6":
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
"@parcel/watcher-darwin-arm64@2.5.6":
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
"@parcel/watcher-darwin-x64@2.5.6":
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
"@parcel/watcher-freebsd-x64@2.5.6":
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
"@parcel/watcher-linux-arm-glibc@2.5.6":
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
"@parcel/watcher-linux-arm-musl@2.5.6":
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
"@parcel/watcher-linux-arm64-glibc@2.5.6":
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
"@parcel/watcher-linux-arm64-musl@2.5.6":
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
"@parcel/watcher-linux-x64-glibc@2.5.6":
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
"@parcel/watcher-linux-x64-musl@2.5.6":
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
"@parcel/watcher-win32-arm64@2.5.6":
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
"@parcel/watcher-win32-ia32@2.5.6":
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
"@parcel/watcher-win32-x64@2.5.6":
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
"@parcel/watcher@2.5.6":
|
|
||||||
dependencies:
|
|
||||||
detect-libc: 2.1.2
|
|
||||||
is-glob: 4.0.3
|
|
||||||
node-addon-api: 7.1.1
|
|
||||||
picomatch: 4.0.4
|
|
||||||
optionalDependencies:
|
|
||||||
"@parcel/watcher-android-arm64": 2.5.6
|
|
||||||
"@parcel/watcher-darwin-arm64": 2.5.6
|
|
||||||
"@parcel/watcher-darwin-x64": 2.5.6
|
|
||||||
"@parcel/watcher-freebsd-x64": 2.5.6
|
|
||||||
"@parcel/watcher-linux-arm-glibc": 2.5.6
|
|
||||||
"@parcel/watcher-linux-arm-musl": 2.5.6
|
|
||||||
"@parcel/watcher-linux-arm64-glibc": 2.5.6
|
|
||||||
"@parcel/watcher-linux-arm64-musl": 2.5.6
|
|
||||||
"@parcel/watcher-linux-x64-glibc": 2.5.6
|
|
||||||
"@parcel/watcher-linux-x64-musl": 2.5.6
|
|
||||||
"@parcel/watcher-win32-arm64": 2.5.6
|
|
||||||
"@parcel/watcher-win32-ia32": 2.5.6
|
|
||||||
"@parcel/watcher-win32-x64": 2.5.6
|
|
||||||
|
|
||||||
"@pdf-lib/standard-fonts@1.0.0":
|
"@pdf-lib/standard-fonts@1.0.0":
|
||||||
dependencies:
|
dependencies:
|
||||||
pako: 1.0.11
|
pako: 1.0.11
|
||||||
@@ -17629,6 +17516,8 @@ snapshots:
|
|||||||
"@rollup/rollup-win32-x64-msvc@4.62.2":
|
"@rollup/rollup-win32-x64-msvc@4.62.2":
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
"@scarf/scarf@1.4.0": {}
|
||||||
|
|
||||||
"@sec-ant/readable-stream@0.4.1": {}
|
"@sec-ant/readable-stream@0.4.1": {}
|
||||||
|
|
||||||
"@semantic-release/commit-analyzer@13.0.1(semantic-release@25.0.5(typescript@6.0.3))":
|
"@semantic-release/commit-analyzer@13.0.1(semantic-release@25.0.5(typescript@6.0.3))":
|
||||||
@@ -21225,8 +21114,6 @@ snapshots:
|
|||||||
|
|
||||||
node-abort-controller@3.1.1: {}
|
node-abort-controller@3.1.1: {}
|
||||||
|
|
||||||
node-addon-api@7.1.1: {}
|
|
||||||
|
|
||||||
node-emoji@1.11.0:
|
node-emoji@1.11.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
lodash: 4.18.1
|
lodash: 4.18.1
|
||||||
@@ -22592,6 +22479,10 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- "@typescript-eslint/types"
|
- "@typescript-eslint/types"
|
||||||
|
|
||||||
|
swagger-ui-dist@5.32.6:
|
||||||
|
dependencies:
|
||||||
|
"@scarf/scarf": 1.4.0
|
||||||
|
|
||||||
symbol-observable@4.0.0: {}
|
symbol-observable@4.0.0: {}
|
||||||
|
|
||||||
symbol-tree@3.2.4: {}
|
symbol-tree@3.2.4: {}
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ DATABASE_URL=postgres://bookorbit:bookorbit@localhost:5432/bookorbit
|
|||||||
PORT=3000
|
PORT=3000
|
||||||
NODE_ENV=development
|
NODE_ENV=development
|
||||||
|
|
||||||
|
# API docs (optional)
|
||||||
|
# Set to true to serve Swagger UI at /api/docs and OpenAPI JSON at /api/docs-json.
|
||||||
|
# SWAGGER_ENABLED=false
|
||||||
|
|
||||||
# Auth
|
# Auth
|
||||||
JWT_SECRET=change-me-in-production
|
JWT_SECRET=change-me-in-production
|
||||||
JWT_EXPIRES_IN=15m
|
JWT_EXPIRES_IN=15m
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
src/metadata.ts
|
||||||
@@ -10,6 +10,19 @@ Server only: `pnpm start:dev` (from this directory).
|
|||||||
|
|
||||||
The server loads environment variables from `.env` in this directory. See `.env.example` for all available options.
|
The server loads environment variables from `.env` in this directory. See `.env.example` for all available options.
|
||||||
|
|
||||||
|
## API docs
|
||||||
|
|
||||||
|
Generated OpenAPI docs are disabled by default. Enable them with `SWAGGER_ENABLED=true`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SWAGGER_ENABLED=true pnpm start:dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Then open:
|
||||||
|
|
||||||
|
- Swagger UI: http://localhost:3000/api/docs
|
||||||
|
- OpenAPI JSON: http://localhost:3000/api/docs-json
|
||||||
|
|
||||||
## Module structure
|
## Module structure
|
||||||
|
|
||||||
Each feature is a self-contained NestJS module under `src/modules/`:
|
Each feature is a self-contained NestJS module under `src/modules/`:
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import tseslint from 'typescript-eslint';
|
|||||||
|
|
||||||
export default tseslint.config(
|
export default tseslint.config(
|
||||||
{
|
{
|
||||||
ignores: ['eslint.config.mjs', 'dist/**', 'coverage/**'],
|
ignores: ['eslint.config.mjs', 'dist/**', 'coverage/**', 'src/metadata.ts'],
|
||||||
},
|
},
|
||||||
eslint.configs.recommended,
|
eslint.configs.recommended,
|
||||||
...tseslint.configs.recommendedTypeChecked,
|
...tseslint.configs.recommendedTypeChecked,
|
||||||
|
|||||||
+11
-1
@@ -4,6 +4,16 @@
|
|||||||
"sourceRoot": "src",
|
"sourceRoot": "src",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"deleteOutDir": true,
|
"deleteOutDir": true,
|
||||||
"builder": "swc"
|
"builder": "swc",
|
||||||
|
"typeCheck": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "@nestjs/swagger",
|
||||||
|
"options": {
|
||||||
|
"classValidatorShim": true,
|
||||||
|
"dtoFileNameSuffix": [".dto.ts"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
"@nestjs/platform-fastify": "^11.1.27",
|
"@nestjs/platform-fastify": "^11.1.27",
|
||||||
"@nestjs/platform-socket.io": "^11.1.27",
|
"@nestjs/platform-socket.io": "^11.1.27",
|
||||||
"@nestjs/schedule": "^6.1.3",
|
"@nestjs/schedule": "^6.1.3",
|
||||||
|
"@nestjs/swagger": "^11.4.4",
|
||||||
"@nestjs/terminus": "^11.1.1",
|
"@nestjs/terminus": "^11.1.1",
|
||||||
"@nestjs/throttler": "^6.5.0",
|
"@nestjs/throttler": "^6.5.0",
|
||||||
"@nestjs/websockets": "^11.1.27",
|
"@nestjs/websockets": "^11.1.27",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { SetMetadata } from '@nestjs/common';
|
import { applyDecorators, SetMetadata } from '@nestjs/common';
|
||||||
|
import { ApiExtension } from '@nestjs/swagger';
|
||||||
|
|
||||||
export const IS_PUBLIC_KEY = 'isPublic';
|
export const IS_PUBLIC_KEY = 'isPublic';
|
||||||
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
|
export const Public = () => applyDecorators(SetMetadata(IS_PUBLIC_KEY, true), ApiExtension('x-public', true));
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ function resetEnv(): void {
|
|||||||
delete process.env.APP_URL;
|
delete process.env.APP_URL;
|
||||||
delete process.env.APP_VERSION;
|
delete process.env.APP_VERSION;
|
||||||
delete process.env.OIDC_ALLOW_LOCAL_ISSUERS;
|
delete process.env.OIDC_ALLOW_LOCAL_ISSUERS;
|
||||||
|
delete process.env.SWAGGER_ENABLED;
|
||||||
delete process.env.KOBO_CLOUDSCRAPER_PYTHON;
|
delete process.env.KOBO_CLOUDSCRAPER_PYTHON;
|
||||||
|
delete process.env.KOREADER_PLUGIN_PATH;
|
||||||
delete process.env.DATABASE_URL;
|
delete process.env.DATABASE_URL;
|
||||||
delete process.env.JWT_SECRET;
|
delete process.env.JWT_SECRET;
|
||||||
delete process.env.JWT_EXPIRES_IN;
|
delete process.env.JWT_EXPIRES_IN;
|
||||||
@@ -46,7 +48,9 @@ describe('config', () => {
|
|||||||
githubReleasesRepo: 'bookorbit/bookorbit',
|
githubReleasesRepo: 'bookorbit/bookorbit',
|
||||||
githubReleasesToken: undefined,
|
githubReleasesToken: undefined,
|
||||||
oidcAllowLocalIssuers: false,
|
oidcAllowLocalIssuers: false,
|
||||||
|
swaggerEnabled: false,
|
||||||
koboCloudscraperPython: undefined,
|
koboCloudscraperPython: undefined,
|
||||||
|
koreaderPluginSourcePath: undefined,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -55,7 +59,9 @@ describe('config', () => {
|
|||||||
process.env.APP_URL = 'https://bookorbit.local';
|
process.env.APP_URL = 'https://bookorbit.local';
|
||||||
process.env.APP_VERSION = 'v2.3.4';
|
process.env.APP_VERSION = 'v2.3.4';
|
||||||
process.env.OIDC_ALLOW_LOCAL_ISSUERS = 'true';
|
process.env.OIDC_ALLOW_LOCAL_ISSUERS = 'true';
|
||||||
|
process.env.SWAGGER_ENABLED = 'true';
|
||||||
process.env.KOBO_CLOUDSCRAPER_PYTHON = '/opt/bookorbit-python/bin/python';
|
process.env.KOBO_CLOUDSCRAPER_PYTHON = '/opt/bookorbit-python/bin/python';
|
||||||
|
process.env.KOREADER_PLUGIN_PATH = '/opt/koreader/bookorbit.koplugin';
|
||||||
process.env.GITHUB_RELEASES_REPO = 'acme/app';
|
process.env.GITHUB_RELEASES_REPO = 'acme/app';
|
||||||
process.env.GITHUB_RELEASES_TOKEN = 'ghp_example';
|
process.env.GITHUB_RELEASES_TOKEN = 'ghp_example';
|
||||||
|
|
||||||
@@ -66,7 +72,9 @@ describe('config', () => {
|
|||||||
githubReleasesRepo: 'acme/app',
|
githubReleasesRepo: 'acme/app',
|
||||||
githubReleasesToken: 'ghp_example',
|
githubReleasesToken: 'ghp_example',
|
||||||
oidcAllowLocalIssuers: true,
|
oidcAllowLocalIssuers: true,
|
||||||
|
swaggerEnabled: true,
|
||||||
koboCloudscraperPython: '/opt/bookorbit-python/bin/python',
|
koboCloudscraperPython: '/opt/bookorbit-python/bin/python',
|
||||||
|
koreaderPluginSourcePath: '/opt/koreader/bookorbit.koplugin',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export const appConfig = registerAs('app', () => ({
|
|||||||
githubReleasesRepo: process.env.GITHUB_RELEASES_REPO?.trim() || 'bookorbit/bookorbit',
|
githubReleasesRepo: process.env.GITHUB_RELEASES_REPO?.trim() || 'bookorbit/bookorbit',
|
||||||
githubReleasesToken: process.env.GITHUB_RELEASES_TOKEN?.trim() || undefined,
|
githubReleasesToken: process.env.GITHUB_RELEASES_TOKEN?.trim() || undefined,
|
||||||
oidcAllowLocalIssuers: parseBooleanFlag(process.env.OIDC_ALLOW_LOCAL_ISSUERS, false),
|
oidcAllowLocalIssuers: parseBooleanFlag(process.env.OIDC_ALLOW_LOCAL_ISSUERS, false),
|
||||||
|
swaggerEnabled: parseBooleanFlag(process.env.SWAGGER_ENABLED, false),
|
||||||
koboCloudscraperPython: process.env.KOBO_CLOUDSCRAPER_PYTHON?.trim() || undefined,
|
koboCloudscraperPython: process.env.KOBO_CLOUDSCRAPER_PYTHON?.trim() || undefined,
|
||||||
koreaderPluginSourcePath: process.env.KOREADER_PLUGIN_PATH?.trim() || undefined,
|
koreaderPluginSourcePath: process.env.KOREADER_PLUGIN_PATH?.trim() || undefined,
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -79,6 +79,26 @@ describe('validateEnv', () => {
|
|||||||
).toThrow('OIDC_ALLOW_LOCAL_ISSUERS must be one of true/false/1/0/yes/no/on/off');
|
).toThrow('OIDC_ALLOW_LOCAL_ISSUERS must be one of true/false/1/0/yes/no/on/off');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('accepts boolean-like values for SWAGGER_ENABLED', () => {
|
||||||
|
for (const SWAGGER_ENABLED of ['true', 'false', '1', '0', 'yes', 'no', 'on', 'off']) {
|
||||||
|
expect(() =>
|
||||||
|
validateEnv({
|
||||||
|
...BASE_ENV,
|
||||||
|
SWAGGER_ENABLED,
|
||||||
|
}),
|
||||||
|
).not.toThrow();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects invalid SWAGGER_ENABLED values', () => {
|
||||||
|
expect(() =>
|
||||||
|
validateEnv({
|
||||||
|
...BASE_ENV,
|
||||||
|
SWAGGER_ENABLED: 'maybe',
|
||||||
|
}),
|
||||||
|
).toThrow('SWAGGER_ENABLED must be one of true/false/1/0/yes/no/on/off');
|
||||||
|
});
|
||||||
|
|
||||||
it('accepts a custom Book Dock container path', () => {
|
it('accepts a custom Book Dock container path', () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
validateEnv({
|
validateEnv({
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { parseIntoClientConfig } from 'pg-connection-string';
|
import { parseIntoClientConfig } from 'pg-connection-string';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const BOOLEAN_ENV_VALUES = ['true', 'false', '1', '0', 'yes', 'no', 'on', 'off'];
|
||||||
|
|
||||||
function isValidPostgresConnectionString(value: string): boolean {
|
function isValidPostgresConnectionString(value: string): boolean {
|
||||||
if (!value.trim()) {
|
if (!value.trim()) {
|
||||||
return false;
|
return false;
|
||||||
@@ -18,6 +20,17 @@ function isValidPostgresConnectionString(value: string): boolean {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function booleanEnvFlag(name: string) {
|
||||||
|
return z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.refine((val) => BOOLEAN_ENV_VALUES.includes(val), {
|
||||||
|
message: `${name} must be one of ${BOOLEAN_ENV_VALUES.join('/')}`,
|
||||||
|
})
|
||||||
|
.optional();
|
||||||
|
}
|
||||||
|
|
||||||
const envSchema = z.object({
|
const envSchema = z.object({
|
||||||
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
|
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
|
||||||
DATABASE_URL: z.string().refine(isValidPostgresConnectionString, 'DATABASE_URL must be a valid PostgreSQL connection string').optional(),
|
DATABASE_URL: z.string().refine(isValidPostgresConnectionString, 'DATABASE_URL must be a valid PostgreSQL connection string').optional(),
|
||||||
@@ -43,22 +56,9 @@ const envSchema = z.object({
|
|||||||
TRUST_PROXY: z.string().optional(),
|
TRUST_PROXY: z.string().optional(),
|
||||||
EMAIL_ENCRYPTION_KEY: z.string().optional(),
|
EMAIL_ENCRYPTION_KEY: z.string().optional(),
|
||||||
LOG_LEVEL: z.enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace']).optional(),
|
LOG_LEVEL: z.enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace']).optional(),
|
||||||
OIDC_ALLOW_LOCAL_ISSUERS: z
|
OIDC_ALLOW_LOCAL_ISSUERS: booleanEnvFlag('OIDC_ALLOW_LOCAL_ISSUERS'),
|
||||||
.string()
|
CSP_ALLOW_CLOUDFLARE_INSIGHTS: booleanEnvFlag('CSP_ALLOW_CLOUDFLARE_INSIGHTS'),
|
||||||
.trim()
|
SWAGGER_ENABLED: booleanEnvFlag('SWAGGER_ENABLED'),
|
||||||
.toLowerCase()
|
|
||||||
.refine((val) => ['true', 'false', '1', '0', 'yes', 'no', 'on', 'off'].includes(val), {
|
|
||||||
message: 'OIDC_ALLOW_LOCAL_ISSUERS must be one of true/false/1/0/yes/no/on/off',
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
CSP_ALLOW_CLOUDFLARE_INSIGHTS: z
|
|
||||||
.string()
|
|
||||||
.trim()
|
|
||||||
.toLowerCase()
|
|
||||||
.refine((val) => ['true', 'false', '1', '0', 'yes', 'no', 'on', 'off'].includes(val), {
|
|
||||||
message: 'CSP_ALLOW_CLOUDFLARE_INSIGHTS must be one of true/false/1/0/yes/no/on/off',
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
KOBO_CLOUDSCRAPER_PYTHON: z
|
KOBO_CLOUDSCRAPER_PYTHON: z
|
||||||
.string()
|
.string()
|
||||||
.transform((val) => val.trim())
|
.transform((val) => val.trim())
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify
|
|||||||
import { ValidationPipe } from '@nestjs/common';
|
import { ValidationPipe } from '@nestjs/common';
|
||||||
import { IoAdapter } from '@nestjs/platform-socket.io';
|
import { IoAdapter } from '@nestjs/platform-socket.io';
|
||||||
import { Logger } from 'nestjs-pino';
|
import { Logger } from 'nestjs-pino';
|
||||||
|
import type { ConfigType } from '@nestjs/config';
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
import { GlobalExceptionFilter } from './common/filters/http-exception.filter';
|
import { GlobalExceptionFilter } from './common/filters/http-exception.filter';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
@@ -11,6 +12,8 @@ import fastifyMultipart from '@fastify/multipart';
|
|||||||
import fastifyStatic from '@fastify/static';
|
import fastifyStatic from '@fastify/static';
|
||||||
import fastifyHelmet from '@fastify/helmet';
|
import fastifyHelmet from '@fastify/helmet';
|
||||||
import fastifyCompress from '@fastify/compress';
|
import fastifyCompress from '@fastify/compress';
|
||||||
|
import { appConfig } from './config/config';
|
||||||
|
import { setupSwaggerDocs } from './swagger';
|
||||||
import {
|
import {
|
||||||
parseBooleanEnv,
|
parseBooleanEnv,
|
||||||
parseTrustProxy,
|
parseTrustProxy,
|
||||||
@@ -67,6 +70,11 @@ async function bootstrap() {
|
|||||||
|
|
||||||
app.useGlobalFilters(new GlobalExceptionFilter());
|
app.useGlobalFilters(new GlobalExceptionFilter());
|
||||||
|
|
||||||
|
const appConfiguration = app.get<ConfigType<typeof appConfig>>(appConfig.KEY);
|
||||||
|
if (appConfiguration.swaggerEnabled) {
|
||||||
|
await setupSwaggerDocs(app, appConfiguration);
|
||||||
|
}
|
||||||
|
|
||||||
await app.register(fastifyHelmet as never, {
|
await app.register(fastifyHelmet as never, {
|
||||||
crossOriginOpenerPolicy: false,
|
crossOriginOpenerPolicy: false,
|
||||||
contentSecurityPolicy: {
|
contentSecurityPolicy: {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Type } from 'class-transformer';
|
import { Type } from 'class-transformer';
|
||||||
import { IsArray, IsBoolean, IsOptional, IsString, IsUrl, Matches, MaxLength, MinLength, ValidateNested } from 'class-validator';
|
import { IsArray, IsBoolean, IsOptional, IsString, IsUrl, Matches, MaxLength, MinLength, ValidateNested } from 'class-validator';
|
||||||
|
|
||||||
class ClaimMappingDto {
|
export class ClaimMappingDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
@MaxLength(256)
|
@MaxLength(256)
|
||||||
username: string;
|
username: string;
|
||||||
@@ -19,7 +19,7 @@ class ClaimMappingDto {
|
|||||||
groups: string;
|
groups: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class AutoProvisionDto {
|
export class AutoProvisionDto {
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Type } from 'class-transformer';
|
import { Type } from 'class-transformer';
|
||||||
import { IsArray, IsBoolean, IsOptional, IsString, IsUrl, MaxLength, ValidateNested } from 'class-validator';
|
import { IsArray, IsBoolean, IsOptional, IsString, IsUrl, MaxLength, ValidateNested } from 'class-validator';
|
||||||
|
|
||||||
class ClaimMappingDto {
|
export class ClaimMappingDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
@MaxLength(256)
|
@MaxLength(256)
|
||||||
username: string;
|
username: string;
|
||||||
@@ -19,7 +19,7 @@ class ClaimMappingDto {
|
|||||||
groups: string;
|
groups: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class AutoProvisionDto {
|
export class AutoProvisionDto {
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Type } from 'class-transformer';
|
import { Type } from 'class-transformer';
|
||||||
import { IsArray, IsBoolean, IsOptional, IsString, IsUrl, MaxLength, MinLength, ValidateNested } from 'class-validator';
|
import { IsArray, IsBoolean, IsOptional, IsString, IsUrl, MaxLength, MinLength, ValidateNested } from 'class-validator';
|
||||||
|
|
||||||
class ClaimMappingDto {
|
export class ClaimMappingDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
@MaxLength(256)
|
@MaxLength(256)
|
||||||
username: string;
|
username: string;
|
||||||
@@ -19,7 +19,7 @@ class ClaimMappingDto {
|
|||||||
groups: string;
|
groups: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class AutoProvisionDto {
|
export class AutoProvisionDto {
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import type {
|
|||||||
|
|
||||||
import { ALL_METADATA_FIELDS } from '@bookorbit/types';
|
import { ALL_METADATA_FIELDS } from '@bookorbit/types';
|
||||||
|
|
||||||
class ScoreConditionDto {
|
export class ScoreConditionDto {
|
||||||
@IsDefined()
|
@IsDefined()
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@@ -22,7 +22,7 @@ class ScoreConditionDto {
|
|||||||
threshold: number;
|
threshold: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
class MissingFieldsConditionDto {
|
export class MissingFieldsConditionDto {
|
||||||
@IsDefined()
|
@IsDefined()
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@@ -33,13 +33,13 @@ class MissingFieldsConditionDto {
|
|||||||
fields: MetadataField[];
|
fields: MetadataField[];
|
||||||
}
|
}
|
||||||
|
|
||||||
class NeverFetchedConditionDto {
|
export class NeverFetchedConditionDto {
|
||||||
@IsDefined()
|
@IsDefined()
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ConditionsDto implements BookMetadataFetchConditions {
|
export class ConditionsDto implements BookMetadataFetchConditions {
|
||||||
@IsDefined()
|
@IsDefined()
|
||||||
@IsObject()
|
@IsObject()
|
||||||
@ValidateNested()
|
@ValidateNested()
|
||||||
@@ -59,7 +59,7 @@ class ConditionsDto implements BookMetadataFetchConditions {
|
|||||||
neverFetched: NeverFetchedConditionDto;
|
neverFetched: NeverFetchedConditionDto;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ScoreConditionOverrideDto implements Partial<ScoreConditionDto> {
|
export class ScoreConditionOverrideDto implements Partial<ScoreConditionDto> {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
@@ -71,7 +71,7 @@ class ScoreConditionOverrideDto implements Partial<ScoreConditionDto> {
|
|||||||
threshold?: number;
|
threshold?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
class MissingFieldsConditionOverrideDto implements Partial<MissingFieldsConditionDto> {
|
export class MissingFieldsConditionOverrideDto implements Partial<MissingFieldsConditionDto> {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
@@ -82,13 +82,13 @@ class MissingFieldsConditionOverrideDto implements Partial<MissingFieldsConditio
|
|||||||
fields?: MetadataField[];
|
fields?: MetadataField[];
|
||||||
}
|
}
|
||||||
|
|
||||||
class NeverFetchedConditionOverrideDto implements Partial<NeverFetchedConditionDto> {
|
export class NeverFetchedConditionOverrideDto implements Partial<NeverFetchedConditionDto> {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ConditionsOverrideDto implements BookMetadataFetchConditionsOverride {
|
export class ConditionsOverrideDto implements BookMetadataFetchConditionsOverride {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsObject()
|
@IsObject()
|
||||||
@ValidateNested()
|
@ValidateNested()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Type } from 'class-transformer';
|
import { Type } from 'class-transformer';
|
||||||
import { ArrayMinSize, IsArray, IsInt, Min, ValidateNested } from 'class-validator';
|
import { ArrayMinSize, IsArray, IsInt, Min, ValidateNested } from 'class-validator';
|
||||||
|
|
||||||
class CollectionOrderItem {
|
export class CollectionOrderItem {
|
||||||
@IsInt()
|
@IsInt()
|
||||||
@Min(1)
|
@Min(1)
|
||||||
id: number;
|
id: number;
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ import 'reflect-metadata';
|
|||||||
import { plainToInstance } from 'class-transformer';
|
import { plainToInstance } from 'class-transformer';
|
||||||
import { validate } from 'class-validator';
|
import { validate } from 'class-validator';
|
||||||
|
|
||||||
import { SaveProgressDto } from './koreader-progress.dto';
|
import { KoreaderSaveProgressDto } from './koreader-progress.dto';
|
||||||
|
|
||||||
const BASE = { document: 'abc123', percentage: 0.42 };
|
const BASE = { document: 'abc123', percentage: 0.42 };
|
||||||
|
|
||||||
describe('SaveProgressDto', () => {
|
describe('KoreaderSaveProgressDto', () => {
|
||||||
it('accepts an xpointer string progress from reflowable documents', async () => {
|
it('accepts an xpointer string progress from reflowable documents', async () => {
|
||||||
const dto = plainToInstance(SaveProgressDto, { ...BASE, progress: '/body/DocFragment[8]/body/p[12]/text().0' });
|
const dto = plainToInstance(KoreaderSaveProgressDto, { ...BASE, progress: '/body/DocFragment[8]/body/p[12]/text().0' });
|
||||||
const errors = await validate(dto);
|
const errors = await validate(dto);
|
||||||
|
|
||||||
expect(errors).toHaveLength(0);
|
expect(errors).toHaveLength(0);
|
||||||
@@ -17,7 +17,7 @@ describe('SaveProgressDto', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('coerces the numeric page progress sent for paged documents into a string', async () => {
|
it('coerces the numeric page progress sent for paged documents into a string', async () => {
|
||||||
const dto = plainToInstance(SaveProgressDto, { ...BASE, progress: 42 });
|
const dto = plainToInstance(KoreaderSaveProgressDto, { ...BASE, progress: 42 });
|
||||||
const errors = await validate(dto);
|
const errors = await validate(dto);
|
||||||
|
|
||||||
expect(errors).toHaveLength(0);
|
expect(errors).toHaveLength(0);
|
||||||
@@ -25,7 +25,7 @@ describe('SaveProgressDto', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('keeps progress undefined when omitted', async () => {
|
it('keeps progress undefined when omitted', async () => {
|
||||||
const dto = plainToInstance(SaveProgressDto, { ...BASE });
|
const dto = plainToInstance(KoreaderSaveProgressDto, { ...BASE });
|
||||||
const errors = await validate(dto);
|
const errors = await validate(dto);
|
||||||
|
|
||||||
expect(errors).toHaveLength(0);
|
expect(errors).toHaveLength(0);
|
||||||
@@ -33,7 +33,7 @@ describe('SaveProgressDto', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('rejects progress values that are neither string nor number', async () => {
|
it('rejects progress values that are neither string nor number', async () => {
|
||||||
const dto = plainToInstance(SaveProgressDto, { ...BASE, progress: true });
|
const dto = plainToInstance(KoreaderSaveProgressDto, { ...BASE, progress: true });
|
||||||
const errors = await validate(dto);
|
const errors = await validate(dto);
|
||||||
|
|
||||||
expect(errors.length).toBeGreaterThan(0);
|
expect(errors.length).toBeGreaterThan(0);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Transform } from 'class-transformer';
|
import { Transform } from 'class-transformer';
|
||||||
import { IsNumber, IsOptional, IsString, Max, Min } from 'class-validator';
|
import { IsNumber, IsOptional, IsString, Max, Min } from 'class-validator';
|
||||||
|
|
||||||
export class SaveProgressDto {
|
export class KoreaderSaveProgressDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
document!: string;
|
document!: string;
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import type { RequestUser } from '../../common/types/request-user';
|
|||||||
import { KoreaderAuthGuard } from './koreader-auth.guard';
|
import { KoreaderAuthGuard } from './koreader-auth.guard';
|
||||||
import { KoreaderPackageService } from './koreader-package.service';
|
import { KoreaderPackageService } from './koreader-package.service';
|
||||||
import { KoreaderService } from './koreader.service';
|
import { KoreaderService } from './koreader.service';
|
||||||
import { CreateKoreaderUserDto, DownloadPluginPackageDto, SaveProgressDto, TestConnectionDto, UpdateKoreaderUserDto } from './dto';
|
import { CreateKoreaderUserDto, DownloadPluginPackageDto, KoreaderSaveProgressDto, TestConnectionDto, UpdateKoreaderUserDto } from './dto';
|
||||||
|
|
||||||
@Controller('koreader')
|
@Controller('koreader')
|
||||||
export class KoreaderController {
|
export class KoreaderController {
|
||||||
@@ -51,7 +51,7 @@ export class KoreaderController {
|
|||||||
@Public()
|
@Public()
|
||||||
@UseGuards(KoreaderAuthGuard)
|
@UseGuards(KoreaderAuthGuard)
|
||||||
@Put('syncs/progress')
|
@Put('syncs/progress')
|
||||||
async saveProgress(@CurrentUser() user: RequestUser, @Body() dto: SaveProgressDto) {
|
async saveProgress(@CurrentUser() user: RequestUser, @Body() dto: KoreaderSaveProgressDto) {
|
||||||
return this.koreaderService.saveProgress(user.id, dto);
|
return this.koreaderService.saveProgress(user.id, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Type } from 'class-transformer';
|
import { Type } from 'class-transformer';
|
||||||
import { IsArray, IsInt, Min, ValidateNested } from 'class-validator';
|
import { IsArray, IsInt, Min, ValidateNested } from 'class-validator';
|
||||||
|
|
||||||
class LibraryOrderItem {
|
export class LibraryOrderItem {
|
||||||
@IsInt()
|
@IsInt()
|
||||||
@Min(1)
|
@Min(1)
|
||||||
id: number;
|
id: number;
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export class FieldPreferenceDto {
|
|||||||
mergeStrategy!: MergeStrategy;
|
mergeStrategy!: MergeStrategy;
|
||||||
}
|
}
|
||||||
|
|
||||||
class GenreOptionsDto {
|
export class GenreOptionsDto {
|
||||||
@IsIn(GENRE_MERGE_MODES)
|
@IsIn(GENRE_MERGE_MODES)
|
||||||
mode!: GenreMergeMode;
|
mode!: GenreMergeMode;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Type } from 'class-transformer';
|
import { Type } from 'class-transformer';
|
||||||
import { IsArray, IsInt, IsNotEmpty, IsObject, IsOptional, IsString, MaxLength, Min, ValidateNested } from 'class-validator';
|
import { IsArray, IsInt, IsNotEmpty, IsObject, IsOptional, IsString, MaxLength, Min, ValidateNested } from 'class-validator';
|
||||||
|
|
||||||
class UserMappingDto {
|
export class UserMappingDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@MaxLength(255)
|
@MaxLength(255)
|
||||||
@@ -13,7 +13,7 @@ class UserMappingDto {
|
|||||||
targetUserId!: number;
|
targetUserId!: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
class PathMappingDto {
|
export class PathMappingDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@MaxLength(1024)
|
@MaxLength(1024)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Type } from 'class-transformer';
|
import { Type } from 'class-transformer';
|
||||||
import { IsArray, IsInt, IsNotEmpty, IsOptional, IsString, Max, MaxLength, Min, ValidateNested } from 'class-validator';
|
import { IsArray, IsInt, IsNotEmpty, IsOptional, IsString, Max, MaxLength, Min, ValidateNested } from 'class-validator';
|
||||||
|
|
||||||
class PathMappingInputDto {
|
export class PathMappingInputDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@MaxLength(1024)
|
@MaxLength(1024)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Type } from 'class-transformer';
|
import { Type } from 'class-transformer';
|
||||||
import { ArrayMinSize, ArrayUnique, IsArray, IsInt, Min, ValidateNested } from 'class-validator';
|
import { ArrayMinSize, ArrayUnique, IsArray, IsInt, Min, ValidateNested } from 'class-validator';
|
||||||
|
|
||||||
class SmartScopeOrderItem {
|
export class SmartScopeOrderItem {
|
||||||
@IsInt()
|
@IsInt()
|
||||||
@Min(1)
|
@Min(1)
|
||||||
id: number;
|
id: number;
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import type { OpenAPIObject } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
import { normalizeSwaggerDocument } from './swagger';
|
||||||
|
|
||||||
|
describe('normalizeSwaggerDocument', () => {
|
||||||
|
it('uses operation-level security so public routes do not inherit bearer auth', () => {
|
||||||
|
const document = {
|
||||||
|
openapi: '3.0.0',
|
||||||
|
info: { title: 'BookOrbit API', version: 'test' },
|
||||||
|
paths: {
|
||||||
|
'/api/v1/auth/login': {
|
||||||
|
post: {
|
||||||
|
'x-public': true,
|
||||||
|
responses: { '200': { description: '' } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'/api/v1/books': {
|
||||||
|
get: {
|
||||||
|
responses: { '200': { description: '' } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
security: [{ bearer: [] }],
|
||||||
|
} as unknown as OpenAPIObject;
|
||||||
|
|
||||||
|
const result = normalizeSwaggerDocument(document);
|
||||||
|
|
||||||
|
expect(result.security).toBeUndefined();
|
||||||
|
expect(result.paths['/api/v1/auth/login']?.post?.security).toEqual([]);
|
||||||
|
expect(result.paths['/api/v1/books']?.get?.security).toEqual([{ bearer: [] }]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import type { ConfigType } from '@nestjs/config';
|
||||||
|
import { DocumentBuilder, SwaggerModule, type OpenAPIObject } from '@nestjs/swagger';
|
||||||
|
import type { NestFastifyApplication } from '@nestjs/platform-fastify';
|
||||||
|
|
||||||
|
import { appConfig } from './config/config';
|
||||||
|
|
||||||
|
const SWAGGER_METADATA_MODULE_PATH = './metadata.js';
|
||||||
|
const PUBLIC_OPERATION_EXTENSION = 'x-public';
|
||||||
|
const HTTP_METHODS = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace', 'search'] as const;
|
||||||
|
|
||||||
|
type AppConfiguration = ConfigType<typeof appConfig>;
|
||||||
|
type SwaggerPluginMetadataFactory = () => Promise<Record<string, any>>;
|
||||||
|
type SwaggerOperation = {
|
||||||
|
security?: Array<Record<string, string[]>>;
|
||||||
|
[PUBLIC_OPERATION_EXTENSION]?: boolean;
|
||||||
|
};
|
||||||
|
type SwaggerPathItem = OpenAPIObject['paths'][string] & Record<string, SwaggerOperation | unknown>;
|
||||||
|
|
||||||
|
export async function setupSwaggerDocs(app: NestFastifyApplication, config: AppConfiguration): Promise<void> {
|
||||||
|
await loadSwaggerPluginMetadata();
|
||||||
|
|
||||||
|
const openApiConfig = new DocumentBuilder()
|
||||||
|
.setTitle('BookOrbit API')
|
||||||
|
.setDescription('BookOrbit server API')
|
||||||
|
.setVersion(config.version)
|
||||||
|
.addBearerAuth({ type: 'http', scheme: 'bearer', bearerFormat: 'JWT' }, 'bearer')
|
||||||
|
.build();
|
||||||
|
|
||||||
|
const documentFactory = () => normalizeSwaggerDocument(SwaggerModule.createDocument(app, openApiConfig));
|
||||||
|
|
||||||
|
SwaggerModule.setup('api/docs', app, documentFactory, {
|
||||||
|
customSiteTitle: 'BookOrbit API Docs',
|
||||||
|
jsonDocumentUrl: 'api/docs-json',
|
||||||
|
raw: ['json'],
|
||||||
|
swaggerOptions: {
|
||||||
|
persistAuthorization: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeSwaggerDocument(document: OpenAPIObject): OpenAPIObject {
|
||||||
|
delete document.security;
|
||||||
|
|
||||||
|
for (const pathItem of Object.values(document.paths)) {
|
||||||
|
applyOperationSecurity(pathItem as SwaggerPathItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
return document;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyOperationSecurity(pathItem: SwaggerPathItem): void {
|
||||||
|
for (const method of HTTP_METHODS) {
|
||||||
|
const operation = pathItem[method];
|
||||||
|
if (!isSwaggerOperation(operation)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
operation.security = operation[PUBLIC_OPERATION_EXTENSION] === true ? [] : [{ bearer: [] }];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSwaggerOperation(value: unknown): value is SwaggerOperation {
|
||||||
|
return typeof value === 'object' && value !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadSwaggerPluginMetadata(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const metadataModule = (await import(SWAGGER_METADATA_MODULE_PATH)) as { default?: SwaggerPluginMetadataFactory };
|
||||||
|
if (typeof metadataModule.default === 'function') {
|
||||||
|
await SwaggerModule.loadPluginMetadata(metadataModule.default);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (isMissingSwaggerMetadata(err)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMissingSwaggerMetadata(err: unknown): boolean {
|
||||||
|
const code = typeof err === 'object' && err !== null && 'code' in err ? String((err as { code?: unknown }).code) : '';
|
||||||
|
if (code !== 'ERR_MODULE_NOT_FOUND' && code !== 'MODULE_NOT_FOUND') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = err instanceof Error ? err.message : '';
|
||||||
|
return message.includes('/metadata') || message.includes('\\metadata') || message.includes(SWAGGER_METADATA_MODULE_PATH);
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"extends": "./tsconfig.json",
|
"extends": "./tsconfig.json",
|
||||||
"exclude": ["node_modules", "test", "dist", "**/*.test.ts"]
|
"exclude": ["node_modules", "test", "dist", "src/metadata.ts", "**/*.test.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user