epiphone / routing-controllers-openapi

Runtime OpenAPI v3 schema generation for routing-controllers.
MIT License
306 stars 58 forks source link

Swagger-UI fails to resolve reference when class passed as @ResponseSchema and @Body #82

Open rdfedor opened 3 years ago

rdfedor commented 3 years ago

Have a project set up using the following structure,

./controllers/auth.ts
./validator/auth.ts
./main.ts

Here's a rough implementation of how the files are implemented,

./main.ts

import { routingControllersToSpec } from 'routing-controllers-openapi'
import { getMetadataArgsStorage } from 'routing-controllers'
import { validationMetadatasToSchemas } from 'class-validator-jsonschema'
import { defaultMetadataStorage } from 'class-transformer/cjs/storage'
import * as swaggerUi from 'swagger-ui-express'
import { useExpressServer } from 'routing-controllers'
import * as http from 'http'
import * as express from 'express'

const app = express()

const schemas = validationMetadatasToSchemas({
  classTransformerMetadataStorage: defaultMetadataStorage,
  refPointerPrefix: '#/components/schemas/',
})

app.use(
      '/api-docs',
      swaggerUi.serve,
      swaggerUi.setup(
        routingControllersToSpec(
          getMetadataArgsStorage(),
          {
            controllers: importClassesFromDirectories(
              routerControllerOptions.controllers,
            ),
          },
          {
            openapi: '3.0.0',
            schemas,
          },
        ),
      ),
    )

useExpressServer(app, {
  controllers: [`${__dirname}/controllers/**/*.ts`]
})

server.listen(8080, () => {
  console.log(`Server started on port 8080`)
})

./validator/auth.ts

import { IsNotEmpty, IsEmail, MaxLength } from 'class-validator'

export class AuthenticateUserRequest {
  @IsNotEmpty()
  @IsEmail()
  @MaxLength(254)
  public email: string

  @IsNotEmpty()
  public password: string
}

export class LoginToken {
  accessToken: string
  refreshToken: string
  maxAge?: string | number
  iat?: number
  roles?: string[]
  userUuid: string
  email: string
  firstName?: string
  lastName?: string
  verifiedEmail?: boolean
}

./controllers/auth.ts

import {
  JsonController,
  Post,
  ContentType,
  Body,
  ForbiddenError,
} from 'routing-controllers'
import { AuthenticateUserRequest, LoginToken } from '../validator/auth'

@JsonController('/api/auth')
export default class AuthController {
  @Post('/')
  @ContentType('application/json')
  @ResponseSchema(LoginToken)
  public async authenticateUser(
    @Body() req: AuthenticateUserRequest,
  ): Promise<LoginToken> {
    try {
      return await authService.processAuthenticateUserRequest(req)
    } catch (err) {
      if (
        err instanceof AccessDeniedError ||
        err instanceof NotFoundError ||
        (err instanceof Array &&
          err.filter(er => er instanceof ValidationError).length > 0)
      ) {
        throw new ForbiddenError('Invalid email and/or password.')
      }
      throw err
    }
  }
}

However when I try to expand the authenticateUser section of the request in swagger-ui I get the following error.

image

rdfedor commented 3 years ago

I was able to fix part of the problem by moving the useExpressServer to before the app.use('/api-docs' line. It was able to get rid of the AuthenticateUserRequest error, but the LoginToken error still exists. Seems to only error out on classes that don't use class-validator.

BlackRider97 commented 3 years ago

I am also getting schemas: {} even while using @ResponsesSchema

judos commented 2 years ago

Same issue, workaround I use now is to annotate every field with an appropriate annotation e.g. IsNotEmpty() for strings. The dependencies we use are:

"dependencies": {
    "@google-cloud/firestore": "4.15.1",
    "bcrypt": "5.0.1",
    "body-parser": "1.19.0",
    "chalk": "4.1.2",
    "class-transformer": "^0.3.1",
    "class-validator": "^0.12.2",
    "class-validator-jsonschema": "2.2.0",
    "console-stamp": "3.0.3",
    "cors": "2.8.5",
    "express": "4.17.1",
    "express-jwt": "6.1.0",
    "jsonwebtoken": "8.5.1",
    "multer": "1.4.3",
    "routing-controllers": "0.9.0",
    "routing-controllers-openapi": "3.1.0",
    "swagger-ui-express": "4.1.3"
  },
thebrokenbar commented 2 years ago

@judos workaround works, thanks

Any permanent solution planned on this?

JamesDAdams commented 10 months ago

Same error :/

JamesDAdams commented 10 months ago

solution : downgrave version : https://github.com/epiphone/routing-controllers-openapi/blob/master/sample/01-basic/package.json

oleksandr-andrushchenko commented 10 months ago

There is not well documented property type schemas

I got Screenshot from 2024-01-18 15-19-23

but then I just ignored it and all works well, final working variant

const { defaultMetadataStorage } = require('class-transformer/cjs/storage');
        const spec = routingControllersToSpec(getMetadataArgsStorage(), routingControllersOptions, {
            components: {
                // @ts-ignore
                schemas: validationMetadatasToSchemas({
                    classTransformerMetadataStorage: defaultMetadataStorage,
                    refPointerPrefix: '#/components/schemas/',
                })
            }
        });

deps

   "dependencies": {
      "bcrypt": "^5.1.1",
      "body-parser": "^1.20.2",
      "class-transformer": "^0.5.1",
      "class-validator": "^0.14.0",
      "class-validator-jsonschema": "^5.0.0",
      "compression": "^1.7.4",
      "event-dispatch": "^0.4.1",
      "express": "^4.18.2",
      "express-basic-auth": "^1.2.1",
      "joi": "^17.11.0",
      "jsonwebtoken": "^9.0.2",
      "morgan": "^1.10.0",
      "multer": "^1.4.5-lts.1",
      "reflect-metadata": "^0.1.14",
      "routing-controllers": "^0.10.4",
      "routing-controllers-openapi": "^4.0.0",
      "swagger-ui-express": "^5.0.0",
      "typedi": "^0.10.0",
      "winston": "^3.11.0"
   }

also, this lib relies on class-validator, so classes without this lib's annotation will be ignored

ernestyouniverse commented 9 months ago

@oleksandr-andrushchenko Could you please show your whole solution? For me, routingControllersToSpec() throws generateSpec.ts:341 )[index], TypeError: Cannot read properties of undefined (reading '0'), if i use @Body() of routing-controllers. All steps i made, what the documentation said. Im using Node 20, TypeScript, ESM, and a Koa2 server.

oleksandr-andrushchenko commented 9 months ago

@ernestyouniverse @all https://github.com/oleksandr-andrushchenko/ExamMeApi/blob/main/src/application.ts

if it helps - give me a star, thank you!

ernestyouniverse commented 9 months ago

@oleksandr-andrushchenko thank You for Your help! :) Indeed with a commonJS setup it works smoothly. Sadly for ESM it doesn't.

Fortur commented 2 months ago

Same problem. Why @ResponseSchema() does not working? I look in generated schema and see this:

{"components":{"schemas":{}}

But reference is exists:

{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}},"description":""}}