diff --git a/.gitignore b/.gitignore index db31a2892..38b545990 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ local/ # Build artifacts coverage/ *.tsbuildinfo +server/src/metadata.ts # Test results (generated artifacts - uploaded to CI, not committed) test-results/ diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 863221462..3d1cd04be 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -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`. +### 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 | +
Running workspaces separately @@ -209,14 +224,15 @@ 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. -| Variable | Purpose | Default (dev) | -| --------------- | ---------------------------------- | --------------------------------------------------------- | -| `DATABASE_URL` | PostgreSQL connection string | `postgres://bookorbit:bookorbit@localhost:5432/bookorbit` | -| `PORT` | Server listen port | `3000` | -| `NODE_ENV` | Runtime mode | `development` | -| `JWT_SECRET` | Signing key for auth tokens | `change-me-in-production` | -| `APP_DATA_PATH` | Storage for covers, avatars, cache | `../local/data` | -| `APP_URL` | Base URL for email links | `http://localhost:5173` | +| Variable | Purpose | Default (dev) | +| ----------------- | ---------------------------------- | --------------------------------------------------------- | +| `DATABASE_URL` | PostgreSQL connection string | `postgres://bookorbit:bookorbit@localhost:5432/bookorbit` | +| `PORT` | Server listen port | `3000` | +| `NODE_ENV` | Runtime mode | `development` | +| `JWT_SECRET` | Signing key for auth tokens | `change-me-in-production` | +| `APP_DATA_PATH` | Storage for covers, avatars, cache | `../local/data` | +| `APP_URL` | Base URL for email links | `http://localhost:5173` | +| `SWAGGER_ENABLED` | Serve Swagger UI and OpenAPI JSON | `false` | --- diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7b258743f..02e39a38d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -276,6 +276,9 @@ importers: "@nestjs/schedule": 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) + "@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": 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) @@ -3865,6 +3868,12 @@ packages: } engines: { node: ">=8" } + "@microsoft/tsdoc@0.16.0": + resolution: + { + integrity: sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==, + } + "@napi-rs/nice-android-arm-eabi@1.1.1": resolution: { @@ -4111,6 +4120,22 @@ packages: peerDependencies: "@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": resolution: { @@ -4167,6 +4192,26 @@ packages: prettier: 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": resolution: { @@ -4571,136 +4616,6 @@ packages: 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": resolution: { @@ -5189,6 +5104,12 @@ packages: cpu: [x64] os: [win32] + "@scarf/scarf@1.4.0": + resolution: + { + integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==, + } + "@sec-ant/readable-stream@0.4.1": resolution: { @@ -11095,12 +11016,6 @@ packages: integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==, } - node-addon-api@7.1.1: - resolution: - { - integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==, - } - node-emoji@1.11.0: resolution: { @@ -13192,6 +13107,12 @@ packages: } engines: { node: ">=18" } + swagger-ui-dist@5.32.6: + resolution: + { + integrity: sha512-75ttZNaYCLoFPnozPZcTUU6mS3wKT8l7WLjU5zJSHFeJa23i5vtnze6IiCl4jDMPeQTXVXIgovq4M11NNfQvSA==, + } + symbol-observable@4.0.0: resolution: { @@ -16966,6 +16887,8 @@ snapshots: "@lukeed/ms@2.0.2": {} + "@microsoft/tsdoc@0.16.0": {} + "@napi-rs/nice-android-arm-eabi@1.1.1": optional: true @@ -17126,6 +17049,14 @@ snapshots: "@types/jsonwebtoken": 9.0.10 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)": 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) @@ -17192,6 +17123,22 @@ snapshots: transitivePeerDependencies: - 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)": 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) @@ -17367,66 +17314,6 @@ snapshots: dependencies: "@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": dependencies: pako: 1.0.11 @@ -17629,6 +17516,8 @@ snapshots: "@rollup/rollup-win32-x64-msvc@4.62.2": optional: true + "@scarf/scarf@1.4.0": {} + "@sec-ant/readable-stream@0.4.1": {} "@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-addon-api@7.1.1: {} - node-emoji@1.11.0: dependencies: lodash: 4.18.1 @@ -22592,6 +22479,10 @@ snapshots: transitivePeerDependencies: - "@typescript-eslint/types" + swagger-ui-dist@5.32.6: + dependencies: + "@scarf/scarf": 1.4.0 + symbol-observable@4.0.0: {} symbol-tree@3.2.4: {} diff --git a/server/.env.example b/server/.env.example index 0a6d7441a..d01465b0b 100644 --- a/server/.env.example +++ b/server/.env.example @@ -5,6 +5,10 @@ DATABASE_URL=postgres://bookorbit:bookorbit@localhost:5432/bookorbit PORT=3000 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 JWT_SECRET=change-me-in-production JWT_EXPIRES_IN=15m diff --git a/server/.prettierignore b/server/.prettierignore new file mode 100644 index 000000000..1f7fd1a72 --- /dev/null +++ b/server/.prettierignore @@ -0,0 +1 @@ +src/metadata.ts diff --git a/server/README.md b/server/README.md index 41645a95e..f4201dda3 100644 --- a/server/README.md +++ b/server/README.md @@ -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. +## 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 Each feature is a self-contained NestJS module under `src/modules/`: diff --git a/server/eslint.config.mjs b/server/eslint.config.mjs index c8163be9d..bf88496d4 100644 --- a/server/eslint.config.mjs +++ b/server/eslint.config.mjs @@ -6,7 +6,7 @@ import tseslint from 'typescript-eslint'; export default tseslint.config( { - ignores: ['eslint.config.mjs', 'dist/**', 'coverage/**'], + ignores: ['eslint.config.mjs', 'dist/**', 'coverage/**', 'src/metadata.ts'], }, eslint.configs.recommended, ...tseslint.configs.recommendedTypeChecked, diff --git a/server/nest-cli.json b/server/nest-cli.json index c9ff8240e..88730e3af 100644 --- a/server/nest-cli.json +++ b/server/nest-cli.json @@ -4,6 +4,16 @@ "sourceRoot": "src", "compilerOptions": { "deleteOutDir": true, - "builder": "swc" + "builder": "swc", + "typeCheck": true, + "plugins": [ + { + "name": "@nestjs/swagger", + "options": { + "classValidatorShim": true, + "dtoFileNameSuffix": [".dto.ts"] + } + } + ] } } diff --git a/server/package.json b/server/package.json index 74faae7e0..68ce4b0d7 100644 --- a/server/package.json +++ b/server/package.json @@ -64,6 +64,7 @@ "@nestjs/platform-fastify": "^11.1.27", "@nestjs/platform-socket.io": "^11.1.27", "@nestjs/schedule": "^6.1.3", + "@nestjs/swagger": "^11.4.4", "@nestjs/terminus": "^11.1.1", "@nestjs/throttler": "^6.5.0", "@nestjs/websockets": "^11.1.27", diff --git a/server/src/common/decorators/public.decorator.ts b/server/src/common/decorators/public.decorator.ts index b3845e122..cef2f44ff 100644 --- a/server/src/common/decorators/public.decorator.ts +++ b/server/src/common/decorators/public.decorator.ts @@ -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 Public = () => SetMetadata(IS_PUBLIC_KEY, true); +export const Public = () => applyDecorators(SetMetadata(IS_PUBLIC_KEY, true), ApiExtension('x-public', true)); diff --git a/server/src/config/config.test.ts b/server/src/config/config.test.ts index f5271fd77..c90c73135 100644 --- a/server/src/config/config.test.ts +++ b/server/src/config/config.test.ts @@ -10,7 +10,9 @@ function resetEnv(): void { delete process.env.APP_URL; delete process.env.APP_VERSION; delete process.env.OIDC_ALLOW_LOCAL_ISSUERS; + delete process.env.SWAGGER_ENABLED; delete process.env.KOBO_CLOUDSCRAPER_PYTHON; + delete process.env.KOREADER_PLUGIN_PATH; delete process.env.DATABASE_URL; delete process.env.JWT_SECRET; delete process.env.JWT_EXPIRES_IN; @@ -46,7 +48,9 @@ describe('config', () => { githubReleasesRepo: 'bookorbit/bookorbit', githubReleasesToken: undefined, oidcAllowLocalIssuers: false, + swaggerEnabled: false, koboCloudscraperPython: undefined, + koreaderPluginSourcePath: undefined, }); }); @@ -55,7 +59,9 @@ describe('config', () => { process.env.APP_URL = 'https://bookorbit.local'; process.env.APP_VERSION = 'v2.3.4'; process.env.OIDC_ALLOW_LOCAL_ISSUERS = 'true'; + process.env.SWAGGER_ENABLED = 'true'; 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_TOKEN = 'ghp_example'; @@ -66,7 +72,9 @@ describe('config', () => { githubReleasesRepo: 'acme/app', githubReleasesToken: 'ghp_example', oidcAllowLocalIssuers: true, + swaggerEnabled: true, koboCloudscraperPython: '/opt/bookorbit-python/bin/python', + koreaderPluginSourcePath: '/opt/koreader/bookorbit.koplugin', }); }); diff --git a/server/src/config/config.ts b/server/src/config/config.ts index a624eded1..4ad5bc6c4 100644 --- a/server/src/config/config.ts +++ b/server/src/config/config.ts @@ -8,6 +8,7 @@ export const appConfig = registerAs('app', () => ({ githubReleasesRepo: process.env.GITHUB_RELEASES_REPO?.trim() || 'bookorbit/bookorbit', githubReleasesToken: process.env.GITHUB_RELEASES_TOKEN?.trim() || undefined, oidcAllowLocalIssuers: parseBooleanFlag(process.env.OIDC_ALLOW_LOCAL_ISSUERS, false), + swaggerEnabled: parseBooleanFlag(process.env.SWAGGER_ENABLED, false), koboCloudscraperPython: process.env.KOBO_CLOUDSCRAPER_PYTHON?.trim() || undefined, koreaderPluginSourcePath: process.env.KOREADER_PLUGIN_PATH?.trim() || undefined, })); diff --git a/server/src/config/env.validation.test.ts b/server/src/config/env.validation.test.ts index a39436313..b7acdbb4a 100644 --- a/server/src/config/env.validation.test.ts +++ b/server/src/config/env.validation.test.ts @@ -79,6 +79,26 @@ describe('validateEnv', () => { ).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', () => { expect(() => validateEnv({ diff --git a/server/src/config/env.validation.ts b/server/src/config/env.validation.ts index e87bacb29..b604e49b8 100644 --- a/server/src/config/env.validation.ts +++ b/server/src/config/env.validation.ts @@ -1,6 +1,8 @@ import { parseIntoClientConfig } from 'pg-connection-string'; import { z } from 'zod'; +const BOOLEAN_ENV_VALUES = ['true', 'false', '1', '0', 'yes', 'no', 'on', 'off']; + function isValidPostgresConnectionString(value: string): boolean { if (!value.trim()) { 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({ 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(), @@ -43,22 +56,9 @@ const envSchema = z.object({ TRUST_PROXY: z.string().optional(), EMAIL_ENCRYPTION_KEY: z.string().optional(), LOG_LEVEL: z.enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace']).optional(), - OIDC_ALLOW_LOCAL_ISSUERS: z - .string() - .trim() - .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(), + OIDC_ALLOW_LOCAL_ISSUERS: booleanEnvFlag('OIDC_ALLOW_LOCAL_ISSUERS'), + CSP_ALLOW_CLOUDFLARE_INSIGHTS: booleanEnvFlag('CSP_ALLOW_CLOUDFLARE_INSIGHTS'), + SWAGGER_ENABLED: booleanEnvFlag('SWAGGER_ENABLED'), KOBO_CLOUDSCRAPER_PYTHON: z .string() .transform((val) => val.trim()) diff --git a/server/src/main.ts b/server/src/main.ts index 93782fb10..539554a6d 100644 --- a/server/src/main.ts +++ b/server/src/main.ts @@ -3,6 +3,7 @@ import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify import { ValidationPipe } from '@nestjs/common'; import { IoAdapter } from '@nestjs/platform-socket.io'; import { Logger } from 'nestjs-pino'; +import type { ConfigType } from '@nestjs/config'; import { AppModule } from './app.module'; import { GlobalExceptionFilter } from './common/filters/http-exception.filter'; import { join } from 'path'; @@ -11,6 +12,8 @@ import fastifyMultipart from '@fastify/multipart'; import fastifyStatic from '@fastify/static'; import fastifyHelmet from '@fastify/helmet'; import fastifyCompress from '@fastify/compress'; +import { appConfig } from './config/config'; +import { setupSwaggerDocs } from './swagger'; import { parseBooleanEnv, parseTrustProxy, @@ -67,6 +70,11 @@ async function bootstrap() { app.useGlobalFilters(new GlobalExceptionFilter()); + const appConfiguration = app.get>(appConfig.KEY); + if (appConfiguration.swaggerEnabled) { + await setupSwaggerDocs(app, appConfiguration); + } + await app.register(fastifyHelmet as never, { crossOriginOpenerPolicy: false, contentSecurityPolicy: { diff --git a/server/src/modules/app-settings/dto/create-oidc-provider.dto.ts b/server/src/modules/app-settings/dto/create-oidc-provider.dto.ts index 179b78c85..141cbc7c3 100644 --- a/server/src/modules/app-settings/dto/create-oidc-provider.dto.ts +++ b/server/src/modules/app-settings/dto/create-oidc-provider.dto.ts @@ -1,7 +1,7 @@ import { Type } from 'class-transformer'; import { IsArray, IsBoolean, IsOptional, IsString, IsUrl, Matches, MaxLength, MinLength, ValidateNested } from 'class-validator'; -class ClaimMappingDto { +export class ClaimMappingDto { @IsString() @MaxLength(256) username: string; @@ -19,7 +19,7 @@ class ClaimMappingDto { groups: string; } -class AutoProvisionDto { +export class AutoProvisionDto { @IsBoolean() enabled: boolean; diff --git a/server/src/modules/app-settings/dto/update-oidc-config.dto.ts b/server/src/modules/app-settings/dto/update-oidc-config.dto.ts index ecc310b60..b43a0c218 100644 --- a/server/src/modules/app-settings/dto/update-oidc-config.dto.ts +++ b/server/src/modules/app-settings/dto/update-oidc-config.dto.ts @@ -1,7 +1,7 @@ import { Type } from 'class-transformer'; import { IsArray, IsBoolean, IsOptional, IsString, IsUrl, MaxLength, ValidateNested } from 'class-validator'; -class ClaimMappingDto { +export class ClaimMappingDto { @IsString() @MaxLength(256) username: string; @@ -19,7 +19,7 @@ class ClaimMappingDto { groups: string; } -class AutoProvisionDto { +export class AutoProvisionDto { @IsBoolean() enabled: boolean; diff --git a/server/src/modules/app-settings/dto/update-oidc-provider.dto.ts b/server/src/modules/app-settings/dto/update-oidc-provider.dto.ts index 422bb4375..13ff64aab 100644 --- a/server/src/modules/app-settings/dto/update-oidc-provider.dto.ts +++ b/server/src/modules/app-settings/dto/update-oidc-provider.dto.ts @@ -1,7 +1,7 @@ import { Type } from 'class-transformer'; import { IsArray, IsBoolean, IsOptional, IsString, IsUrl, MaxLength, MinLength, ValidateNested } from 'class-validator'; -class ClaimMappingDto { +export class ClaimMappingDto { @IsString() @MaxLength(256) username: string; @@ -19,7 +19,7 @@ class ClaimMappingDto { groups: string; } -class AutoProvisionDto { +export class AutoProvisionDto { @IsBoolean() enabled: boolean; diff --git a/server/src/modules/book-metadata-fetch/dto/update-config.dto.ts b/server/src/modules/book-metadata-fetch/dto/update-config.dto.ts index 87e21c57d..0e1a4e3d3 100644 --- a/server/src/modules/book-metadata-fetch/dto/update-config.dto.ts +++ b/server/src/modules/book-metadata-fetch/dto/update-config.dto.ts @@ -10,7 +10,7 @@ import type { import { ALL_METADATA_FIELDS } from '@bookorbit/types'; -class ScoreConditionDto { +export class ScoreConditionDto { @IsDefined() @IsBoolean() enabled: boolean; @@ -22,7 +22,7 @@ class ScoreConditionDto { threshold: number; } -class MissingFieldsConditionDto { +export class MissingFieldsConditionDto { @IsDefined() @IsBoolean() enabled: boolean; @@ -33,13 +33,13 @@ class MissingFieldsConditionDto { fields: MetadataField[]; } -class NeverFetchedConditionDto { +export class NeverFetchedConditionDto { @IsDefined() @IsBoolean() enabled: boolean; } -class ConditionsDto implements BookMetadataFetchConditions { +export class ConditionsDto implements BookMetadataFetchConditions { @IsDefined() @IsObject() @ValidateNested() @@ -59,7 +59,7 @@ class ConditionsDto implements BookMetadataFetchConditions { neverFetched: NeverFetchedConditionDto; } -class ScoreConditionOverrideDto implements Partial { +export class ScoreConditionOverrideDto implements Partial { @IsOptional() @IsBoolean() enabled?: boolean; @@ -71,7 +71,7 @@ class ScoreConditionOverrideDto implements Partial { threshold?: number; } -class MissingFieldsConditionOverrideDto implements Partial { +export class MissingFieldsConditionOverrideDto implements Partial { @IsOptional() @IsBoolean() enabled?: boolean; @@ -82,13 +82,13 @@ class MissingFieldsConditionOverrideDto implements Partial { +export class NeverFetchedConditionOverrideDto implements Partial { @IsOptional() @IsBoolean() enabled?: boolean; } -class ConditionsOverrideDto implements BookMetadataFetchConditionsOverride { +export class ConditionsOverrideDto implements BookMetadataFetchConditionsOverride { @IsOptional() @IsObject() @ValidateNested() diff --git a/server/src/modules/collection/dto/reorder-collections.dto.ts b/server/src/modules/collection/dto/reorder-collections.dto.ts index 4dd8ab206..190c99694 100644 --- a/server/src/modules/collection/dto/reorder-collections.dto.ts +++ b/server/src/modules/collection/dto/reorder-collections.dto.ts @@ -1,7 +1,7 @@ import { Type } from 'class-transformer'; import { ArrayMinSize, IsArray, IsInt, Min, ValidateNested } from 'class-validator'; -class CollectionOrderItem { +export class CollectionOrderItem { @IsInt() @Min(1) id: number; diff --git a/server/src/modules/koreader/dto/koreader-progress.dto.test.ts b/server/src/modules/koreader/dto/koreader-progress.dto.test.ts index c9ee0b715..9d67dd5c7 100644 --- a/server/src/modules/koreader/dto/koreader-progress.dto.test.ts +++ b/server/src/modules/koreader/dto/koreader-progress.dto.test.ts @@ -3,13 +3,13 @@ import 'reflect-metadata'; import { plainToInstance } from 'class-transformer'; import { validate } from 'class-validator'; -import { SaveProgressDto } from './koreader-progress.dto'; +import { KoreaderSaveProgressDto } from './koreader-progress.dto'; const BASE = { document: 'abc123', percentage: 0.42 }; -describe('SaveProgressDto', () => { +describe('KoreaderSaveProgressDto', () => { 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); expect(errors).toHaveLength(0); @@ -17,7 +17,7 @@ describe('SaveProgressDto', () => { }); 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); expect(errors).toHaveLength(0); @@ -25,7 +25,7 @@ describe('SaveProgressDto', () => { }); it('keeps progress undefined when omitted', async () => { - const dto = plainToInstance(SaveProgressDto, { ...BASE }); + const dto = plainToInstance(KoreaderSaveProgressDto, { ...BASE }); const errors = await validate(dto); expect(errors).toHaveLength(0); @@ -33,7 +33,7 @@ describe('SaveProgressDto', () => { }); 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); expect(errors.length).toBeGreaterThan(0); diff --git a/server/src/modules/koreader/dto/koreader-progress.dto.ts b/server/src/modules/koreader/dto/koreader-progress.dto.ts index 3e8412bff..99b912cbb 100644 --- a/server/src/modules/koreader/dto/koreader-progress.dto.ts +++ b/server/src/modules/koreader/dto/koreader-progress.dto.ts @@ -1,7 +1,7 @@ import { Transform } from 'class-transformer'; import { IsNumber, IsOptional, IsString, Max, Min } from 'class-validator'; -export class SaveProgressDto { +export class KoreaderSaveProgressDto { @IsString() document!: string; diff --git a/server/src/modules/koreader/koreader.controller.ts b/server/src/modules/koreader/koreader.controller.ts index 1227b8b29..1f1a42327 100644 --- a/server/src/modules/koreader/koreader.controller.ts +++ b/server/src/modules/koreader/koreader.controller.ts @@ -24,7 +24,7 @@ import type { RequestUser } from '../../common/types/request-user'; import { KoreaderAuthGuard } from './koreader-auth.guard'; import { KoreaderPackageService } from './koreader-package.service'; import { KoreaderService } from './koreader.service'; -import { CreateKoreaderUserDto, DownloadPluginPackageDto, SaveProgressDto, TestConnectionDto, UpdateKoreaderUserDto } from './dto'; +import { CreateKoreaderUserDto, DownloadPluginPackageDto, KoreaderSaveProgressDto, TestConnectionDto, UpdateKoreaderUserDto } from './dto'; @Controller('koreader') export class KoreaderController { @@ -51,7 +51,7 @@ export class KoreaderController { @Public() @UseGuards(KoreaderAuthGuard) @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); } diff --git a/server/src/modules/library/dto/reorder-libraries.dto.ts b/server/src/modules/library/dto/reorder-libraries.dto.ts index f185167dc..c94b0ee4d 100644 --- a/server/src/modules/library/dto/reorder-libraries.dto.ts +++ b/server/src/modules/library/dto/reorder-libraries.dto.ts @@ -1,7 +1,7 @@ import { Type } from 'class-transformer'; import { IsArray, IsInt, Min, ValidateNested } from 'class-validator'; -class LibraryOrderItem { +export class LibraryOrderItem { @IsInt() @Min(1) id: number; diff --git a/server/src/modules/metadata-preferences/dto/update-global-preferences.dto.ts b/server/src/modules/metadata-preferences/dto/update-global-preferences.dto.ts index 52e2bb5c5..8c671862f 100644 --- a/server/src/modules/metadata-preferences/dto/update-global-preferences.dto.ts +++ b/server/src/modules/metadata-preferences/dto/update-global-preferences.dto.ts @@ -30,7 +30,7 @@ export class FieldPreferenceDto { mergeStrategy!: MergeStrategy; } -class GenreOptionsDto { +export class GenreOptionsDto { @IsIn(GENRE_MERGE_MODES) mode!: GenreMergeMode; diff --git a/server/src/modules/migration/dto/create-migration-profile.dto.ts b/server/src/modules/migration/dto/create-migration-profile.dto.ts index 2dcb4b666..af3171fb9 100644 --- a/server/src/modules/migration/dto/create-migration-profile.dto.ts +++ b/server/src/modules/migration/dto/create-migration-profile.dto.ts @@ -1,7 +1,7 @@ import { Type } from 'class-transformer'; import { IsArray, IsInt, IsNotEmpty, IsObject, IsOptional, IsString, MaxLength, Min, ValidateNested } from 'class-validator'; -class UserMappingDto { +export class UserMappingDto { @IsString() @IsNotEmpty() @MaxLength(255) @@ -13,7 +13,7 @@ class UserMappingDto { targetUserId!: number; } -class PathMappingDto { +export class PathMappingDto { @IsString() @IsNotEmpty() @MaxLength(1024) diff --git a/server/src/modules/migration/dto/validate-path-mappings.dto.ts b/server/src/modules/migration/dto/validate-path-mappings.dto.ts index 1d40c3e0e..e0f315dde 100644 --- a/server/src/modules/migration/dto/validate-path-mappings.dto.ts +++ b/server/src/modules/migration/dto/validate-path-mappings.dto.ts @@ -1,7 +1,7 @@ import { Type } from 'class-transformer'; import { IsArray, IsInt, IsNotEmpty, IsOptional, IsString, Max, MaxLength, Min, ValidateNested } from 'class-validator'; -class PathMappingInputDto { +export class PathMappingInputDto { @IsString() @IsNotEmpty() @MaxLength(1024) diff --git a/server/src/modules/smart-scope/dto/reorder-smart-scopes.dto.ts b/server/src/modules/smart-scope/dto/reorder-smart-scopes.dto.ts index 1af93b8ce..db2c4c834 100644 --- a/server/src/modules/smart-scope/dto/reorder-smart-scopes.dto.ts +++ b/server/src/modules/smart-scope/dto/reorder-smart-scopes.dto.ts @@ -1,7 +1,7 @@ import { Type } from 'class-transformer'; import { ArrayMinSize, ArrayUnique, IsArray, IsInt, Min, ValidateNested } from 'class-validator'; -class SmartScopeOrderItem { +export class SmartScopeOrderItem { @IsInt() @Min(1) id: number; diff --git a/server/src/swagger.test.ts b/server/src/swagger.test.ts new file mode 100644 index 000000000..0f1d7008c --- /dev/null +++ b/server/src/swagger.test.ts @@ -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: [] }]); + }); +}); diff --git a/server/src/swagger.ts b/server/src/swagger.ts new file mode 100644 index 000000000..dadfcfc2b --- /dev/null +++ b/server/src/swagger.ts @@ -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; +type SwaggerPluginMetadataFactory = () => Promise>; +type SwaggerOperation = { + security?: Array>; + [PUBLIC_OPERATION_EXTENSION]?: boolean; +}; +type SwaggerPathItem = OpenAPIObject['paths'][string] & Record; + +export async function setupSwaggerDocs(app: NestFastifyApplication, config: AppConfiguration): Promise { + 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 { + 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); +} diff --git a/server/tsconfig.build.json b/server/tsconfig.build.json index cb63862ba..390beb6a2 100644 --- a/server/tsconfig.build.json +++ b/server/tsconfig.build.json @@ -1,4 +1,4 @@ { "extends": "./tsconfig.json", - "exclude": ["node_modules", "test", "dist", "**/*.test.ts"] + "exclude": ["node_modules", "test", "dist", "src/metadata.ts", "**/*.test.ts"] }