lukeautry / tsoa

Build OpenAPI-compliant REST APIs using TypeScript and Node
MIT License
3.55k stars 499 forks source link

Issue between TSOA and Sequelize - No matching model found #205

Closed Esya closed 5 years ago

Esya commented 6 years ago

Hi,

We're having some serious issues with our Sequelize models and the tsoa swagger/routes generation. The metadata generator fails when we do something as simple as this :

    // Dummy controller
    public userJson(@Request() request: express.Request): User {
        return request.user.toJSON();
    }

Where User is a simple Sequelize model like so :

import { Model } from "sequelize-typescript";
import { Column } from "sequelize-typescript/lib/annotations/Column";

export default class User extends Model<User> {
  /** email id of the user */
  @Column emailId: string;
  /** First name of the user */
  @Column firstName: string;
  /** Lastname of the user */
  @Column lastName: string;
}

When we try generating the routes we get :

There was a problem resolving type of 'SequelizeOrigin'.
There was a problem resolving type of 'Sequelize'.
There was a problem resolving type of 'Model'.
There was a problem resolving type of 'User'.
Generate swagger error.
 Error: No matching model found for referenced type SequelizeOrigin.
    at new GenerateMetadataError (/home/esya/Documents/Git/microservice-tsoa-working/node_modules/tsoa/dist/metadataGeneration/exceptions.js:17:28)
    at getModelTypeDeclaration (/home/esya/Documents/Git/microservice-tsoa-working/node_modules/tsoa/dist/metadataGeneration/resolveType.js:419:15)
    at getReferenceType (/home/esya/Documents/Git/microservice-tsoa-working/node_modules/tsoa/dist/metadataGeneration/resolveType.js:280:25)
    at /home/esya/Documents/Git/microservice-tsoa-working/node_modules/tsoa/dist/metadataGeneration/resolveType.js:572:33
    at Array.forEach (<anonymous>)
    at /home/esya/Documents/Git/microservice-tsoa-working/node_modules/tsoa/dist/metadataGeneration/resolveType.js:570:22
    at Array.forEach (<anonymous>)
    at getModelInheritedProperties (/home/esya/Documents/Git/microservice-tsoa-working/node_modules/tsoa/dist/metadataGeneration/resolveType.js:566:21)
    at getReferenceType (/home/esya/Documents/Git/microservice-tsoa-working/node_modules/tsoa/dist/metadataGeneration/resolveType.js:283:35)
    at resolveType (/home/esya/Documents/Git/microservice-tsoa-working/node_modules/tsoa/dist/metadataGeneration/resolveType.js:95:25)

We've found a very very nasty work-around which is to block the typedetection when it reaches "Model" or "SequelizeOrigin" but that means a fork of tsoa with a quick & dirty fix.

Would you have a solution for this? Thanks!

lukeautry commented 6 years ago

Generally with these sorts of conflicts, the best bet is to use the "ignore" configuration. From a project I use:

{
  "swagger": {
      "outputDirectory": "./dist",
      "entryFile": "./src/server.ts",
      "basePath": "/api",
      "name": "-----"
  },
  "routes": {
      "basePath": "/api",
      "entryFile": "./src/server.ts",
      "routesDir": "./src/api/routes"
  },
  "ignore": [
    "**/node_modules/**"
  ]
}
Esya commented 6 years ago

I've tried using this :

"ignore": [
        "**/node_modules/sequelize/**",
        "**/node_modules/sequelize-typescript/**"
    ]

But to no avail; the error becomes :

There was a problem resolving type of 'Model'.
There was a problem resolving type of 'User'.
Generate swagger error.
 Error: No matching model found for referenced type Model.
    at new GenerateMetadataError (/home/esya/Documents/Git/microservice-tsoa-working/node_modules/tsoa/dist/metadataGeneration/exceptions.js:17:28)
    at getModelTypeDeclaration (/home/esya/Documents/Git/microservice-tsoa-working/node_modules/tsoa/dist/metadataGeneration/resolveType.js:419:15)
    at getReferenceType (/home/esya/Documents/Git/microservice-tsoa-working/node_modules/tsoa/dist/metadataGeneration/resolveType.js:280:25)
    at /home/esya/Documents/Git/microservice-tsoa-working/node_modules/tsoa/dist/metadataGeneration/resolveType.js:572:33
    at Array.forEach (<anonymous>)
    at /home/esya/Documents/Git/microservice-tsoa-working/node_modules/tsoa/dist/metadataGeneration/resolveType.js:570:22
    at Array.forEach (<anonymous>)
    at getModelInheritedProperties (/home/esya/Documents/Git/microservice-tsoa-working/node_modules/tsoa/dist/metadataGeneration/resolveType.js:566:21)
    at getReferenceType (/home/esya/Documents/Git/microservice-tsoa-working/node_modules/tsoa/dist/metadataGeneration/resolveType.js:283:35)
    at resolveType (/home/esya/Documents/Git/microservice-tsoa-working/node_modules/tsoa/dist/metadataGeneration/resolveType.js:95:25)

Model is defined in an ignored folder (sequelize-typescript). User extends Model, but we only need the properties of User. Do you see any reason why it would not be ignored?

Superd22 commented 6 years ago

I'm encountering the exact same issue.

lukeautry commented 6 years ago

The problem here, I think, is that tsoa actually needs to know about Model in order to correctly form the definition for User.

Depending on the structure of sequelize, you may be able to ignore everything except the file that Model is in, but I realize that's not a great solution. I think we're going to need to some way to mark a model as the "real" matching model.

lukeautry commented 6 years ago

Just a follow up on this - do you get 'No matching model found for referenced type SequelizeOrigin.' even without any ignore setting in tsoa.json? Is it failing to find matching models because that directory is ignored?

If we are ignoring that directory, then there's a new feature where you can mark a model as the designated model with @tsoaModel which should reduce the need to ignore external libraries.

KyleGalvin commented 6 years ago

Could this be solved with an interface? From the code above: export default class User extends Model<User> what happens if we change this to: export default class User extends Model<User> implements IUser and changing your route to match: public userJson(@Request() request: express.Request): IUser {

Then you can define IUser to be just the parts of Model you wish to serialize without dragging along all the extra baggage sequelize might add on.

hienqnguyen commented 6 years ago

I'm running into similar issue but slightly different scenario where my Typescript types are generated from a protobuffer file.

Protobuf file:

syntax = "proto3";
package "mypackage.test";

message User {
   string id = 1;
}

The generated code (using protobufjs) looks like below: compiled.d.ts:

export namespace mypackage {
    namespace test {
        interface IUser {
            id: string;
        }
    }
}

The generated code is in organized into its own npm package, @hien/domain. and used in the controller as:

import proto from "@hien/domain"

class Test extends Controller {

    @Get()
    public async get(): proto.mypackage.test.IUser {
         ...
    }
}

I get Error: No matching module declarations found for proto.

This issue seems related but I can create a separate issue if needed.

Thanks!

Entities commented 6 years ago

I was able to resolve this by extending the interface to a class, then decorating the interface with the @tsoaModel reference. NOTE: the property in question had to be moved out of the interface and into the class for this to work. While admittedly this does go against the purpose of using an interface as a contract for a class (and for anyone who only uses interfaces it may be a dealbreaker), this at least works as an immediate solution until a better one is implemented.

Here are the specifics: CASE: I have a User interface with a property of type CollectionReference (from the node_modules reference to Google Firestore types). When running 'tsoa routes' I would get the error "No matching model found for referenced type CollectionReference."

SOLUTION: Extending the User interface with a User class, I moved the 'CollectionReference' property into the class. Then I decorated the interface with @tsoaModel

/**
 * @tsoaModel
 */
export interface User {
    uid: string;
    email: string;
    password: string;
    displayName?: string;
    phoneNumber?: string;
    photoURL: string
}

export class User implements User{
    subUsers: CollectionReference
}

Also important to note that this will exclude the property from your api doc, but for my situation it is acceptable since the CollectionReference is a Firestore property which is processed internally, and cannot be passed in or returned as it would warrant the inclusion of all associated Firebase subtypes into my docs

KyleGalvin commented 6 years ago

This seems reasonable and expected.

Database objects probably shouldn't be exposed to the rest API. Typically there is a process in the controller that maps the lightweight serializeable json view models to heavyweight domain objects.

To tightly couple the rest layer interface with the domain level data model would break both SOLID principles and standard domain-driven design.

I'm no authority here, but aside from @hienqnguyen (who has a separate issue from the reported one. I suspect tsoa cannot find the reference to his auto-generated models) I don't see any actionable item in this particular issue

dgreene1 commented 5 years ago

Database objects probably shouldn't be exposed to the rest API. Typically there is a process in the controller that maps the lightweight serializeable json view models to heavyweight domain objects.

I agree.

The solution is to not expose any interfaces from dependencies in your swagger/tsoa models. Basically, just make a copy of the interfaces you got from other libraries and use those instead. Trust me it will work (see this explanation here on "structural subtyping" as to why this works).