Skip to content

getModelSchemaRef does not include the nested model definition #9959

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Telokis opened this issue Sep 19, 2023 · 2 comments
Closed

getModelSchemaRef does not include the nested model definition #9959

Telokis opened this issue Sep 19, 2023 · 2 comments
Labels

Comments

@Telokis
Copy link

Telokis commented Sep 19, 2023

Describe the bug

Hello, I am new to Loopback and started a new project today.

My use-case is that I want to provide a REST API scraping a specific webpage so it's easier to work with.

I now have a Controller working as expected and it returns a Model. This Model uses another Model that uses another Model as well. I've turned everything into models so the generated openapi docs would be clean and easier to read/process.

Here is a simplification of what I have:

Trimmed-down code showing my 3 models and my controller
import { inject } from "@loopback/core";
import { api, get, getModelSchemaRef, response } from "@loopback/rest";

import { Model, model, property } from "@loopback/repository";

///////////////////////////////////////
// Trimmed-down version of my MODELS //
///////////////////////////////////////

@model()
export class AlCharacterItem extends Model {
  @property({
    type: "string",
  })
  name: string;

  @property({
    type: "number",
  })
  level?: number;

  @property({
    type: "string",
  })
  p?: string;

  @property({
    type: "string",
  })
  // eslint-disable-next-line @typescript-eslint/naming-convention
  stat_type?: string;

  constructor(data?: Partial<AlCharacterItem>) {
    super(data);
  }
}

model();
class ItemSlots extends Model {
    @property()
    amulet: AlCharacterItem;

    @property()
    gloves: AlCharacterItem;
}

@model()
export class AlCharacter extends Model {
    @property({
        type: "string",
    })
    name: string;

    @property({
        type: "number",
    })
    level: number;

    @property({
        type: "string",
    })
    ctype: string;

    @property()
    slots: ItemSlots;

    constructor(data?: Partial<AlCharacter>) {
        super(data);
    }
}

///////////////////////////////////////////
// Trimmed-down version of my CONTROLLER //
///////////////////////////////////////////

@api({ basePath: "/api/v1" })
export class MyController {
    constructor(@inject("services.MyService") protected alService: MyService) {}

    @get("/character/{name}")
    @response(200, {
        description: "Returns the character as scraped from the html page.",
        content: {
            "application/json": {
                schema: getModelSchemaRef(AlCharacter),
            },
        },
    })
    async getCharacter(@param.path.string("name") name: string): Promise<AlCharacter> {
        // Hardcoded to demonstrate
        return {
            name,
            level: 16,
            ctype: "priest",
            slots: {
                gloves: {
                    name: "xgloves",
                    level: 3,
                    stat_type: "int",
                    p: "shiny",
                },
            },
        };
    }
}

To summarize: my controller returns a Model AlCharacter that contains a Model ItemSlots that contains two AlCharacterItem. In the real code, the models are bigger, with enums containing lots of values.

As you can see in the controller, I use schema: getModelSchemaRef(AlCharacter) so the schema is generated based on my Models. It works fine for the top level but the definitions of ItemSlots and AlCharacterItem are not provided which results in an error.

Here is the generated json:

{
  "paths": {
    "/api/v1/character/{name}": {
      "get": {
        "x-controller-name": "MyController",
        "x-operation-name": "getCharacter",
        "responses": {
          "200": {
            "description": "Returns the character as scraped from the html page.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AlCharacter"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "schema": {
              "type": "string"
            },
            "required": true
          }
        ],
        "operationId": "MyController.getCharacter"
      }
    }
  },
  "components": {
    "schemas": {
      "AlCharacter": {
        "title": "AlCharacter",
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Name of the character."
          },
          "level": {
            "type": "number",
            "description": "Level of the character."
          },
          "ctype": {
            "type": "string",
            "description": "Class of the character.",
            "enum": [
              "mage",
              "merchant",
              "paladin",
              "priest",
              "ranger",
              "rogue",
              "warrior"
            ]
          },
          "slots": {
            "$ref": "#/components/schemas/ItemSlots"
          }
        },
        "additionalProperties": false
      }
    }
  }
}

As you can see, there is a ref to ItemSlots but it's not defined.

How am I supposed to fix this situation?

Reproduction

https://codesandbox.io/p/sandbox/practical-almeida-7x4yyp

@Telokis Telokis added the bug label Sep 19, 2023
@Telokis
Copy link
Author

Telokis commented Sep 19, 2023

At first, I thought I could create my own decorator to force a model to inject its own definitions into the global object but after looking through the rest.server.ts file inside @loopback/rest, I think everything is pretty much closed and despite Loopback's natural extensibility, it doesn't let the user do anything that isn't directly related to a controller.

I thought of using the API_SPEC binding but it's a single entrypoint and I'm not familiar with Loopback enough to properly see how I could fill that from multiple models at once.

@Telokis
Copy link
Author

Telokis commented Sep 20, 2023

It was a mistake on my end: I called model(); instead of @model()

@Telokis Telokis closed this as completed Sep 20, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant