Skip to content
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

Elysia schema creates malformed swagger schema #8

Open
SwatDoge opened this issue Jun 21, 2023 · 10 comments · May be fixed by #20
Open

Elysia schema creates malformed swagger schema #8

SwatDoge opened this issue Jun 21, 2023 · 10 comments · May be fixed by #20

Comments

@SwatDoge
Copy link

SwatDoge commented Jun 21, 2023

Ola,

I want to make a nice swagger schema view of my api. While the @elysia/swagger plugin gets there half of the way, it seems to struggle with schemas. I've dug through all the examples, but none seem to mix params and responses together, or the examples are outdated. (Like the readme of this repo). I've tried different ways of creating layouts for the schemas, but non of them seem to work properly.


I've got an Elysia route set up like the following:

import { Elysia, t } from "elysia"
import { song } from "../../interfaces";
import { db } from "../..";

export const getSkip = (app: Elysia) => app
  .get("/skip/:id", ({ params }) => {
    return db.query("SELECT user_id, start, end FROM skip WHERE `song_id`=$songId").all({
      $songId: params.id
    }) as song[];
  }, {
    params: t.Object({
      id: t.Number({
        description: "The ID of the song you want to fetch skips from."
      })
    }, {
      description: "Request body to get all skips belonging to a song."
    }),
    response: {
      200: t.Array(
        t.Object({
          user_id: t.Number({
            description: "The Soundcloud id of the uploader.",
            minimum: 0
          }),
          start: t.Number({
            description: "Timestamp where the song skip starts.",
            minimum: 0
          }),
          end: t.Number({
            description: "Timestamp where the song skip ends.",
            minimum: 0
          })
        }), {
        description: "Returns an array of skip items belonging to the song id."
      }
      ),
    },
    type: ["arrayBuffer", "application/json"],
    description: "Gets all skips of a song.",
  })

Then I run the route like this:

new Elysia()
   .use(swagger(swaggerDocs)) //Basic dcoumentation options
    .group("/skip", app => app
      .use(getSkip) //The route from above
    )
   .listen(port, () => console.log(`Listening on port ${port}...`));

My swagger starts up successfully, but some functionality is broken. I can not inspect the schema, and can not log into my oauth.
image

I threw the generated swagger schema into insomnia to inspect any errors, it came out with the following:

❌ Elysia generated

{
  "openapi": "3.0.3",
  "components": {
    "schemas": {}
  },
  "security": [
    {
      "ApiKeyAuth": [] ❌ <--- Api "security" values must match a scheme defined in the "components.securitySchemes" object
    }
  ],
  "servers": [
    {
      "description": "Local host",
      "url": "http://localhost:3001"
    }
  ],
  "info": {
    "title": "Skipcloud API",
    "description": "API used to upload sections of soundcloud songs to skip over.",
    "version": "0.0.1",
    "contact": {
      "name": "@hang_yourself on discord"
    }
  },
  "paths": {
    "/skip/{id}": {
      "get": {
        "parameters": [
          {
            ❌ <--- "0" property must have required property "schema".
            "description": "The ID of the song you want to fetch skips from.",
            "type": "number",
            "in": "path",
            "name": "id",
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Returns an array of skip items belonging to the song id.",
            "items": { ❌ <--- Property "items" is not expected to be here
              "type": "object",
              "properties": {
                "user_id": {
                  "description": "The Soundcloud id of the uploader.",
                  "minimum": 0,
                  "type": "number"
                },
                "start": {
                  "description": "Timestamp where the song skip starts.",
                  "minimum": 0,
                  "type": "number"
                },
                "end": {
                  "description": "Timestamp where the song skip ends.",
                  "minimum": 0,
                  "type": "number"
                }
              },
              "required": [
                "user_id",
                "start",
                "end"
              ]
            },
            "content": {
              "arrayBuffer": {
                "schema": {
                  "type": "array"
                }
              },
              "application/json": {
                "schema": {
                  "type": "array"
                }
              }
            }
          }
        },
        "operationId": "getSkipById"
      }
    }
  }
}

Fixes

✅ Error 1

I have an API key in my header, I have defined my swagger schema as such:

import { ElysiaSwaggerConfig } from "@elysiajs/swagger/src/types";

export default {
  documentation: {
    components: {
      securitySchemes: {
        ApiKeyAuth: {
          type: "apiKey",
          in: "header",
          name: "Authorization",
          description: "Key used to log into the soundcloud API."
        }
      },
     //... etc
   }
} as ElysiaSwaggerConfig;

This however does not show up in the schema. The schema stays empty.

✅ Error 2

The first error seems to be caused by not putting "type: number" in a schema object:

"parameters": [
  {
    "description": "The ID of the song you want to fetch skips from.",
    "schema": {
       "type": "number"
    },
    "in": "path",
    "name": "id",
    "required": true
  }
],

✅ Error 3

This error seems to be caused by the response schema directly being in the 200 object, instead of inside the content objects.
image

"responses": {
  "200": {
    "description": "Returns an array of skip items belonging to the song id.",
    "content": {
      "arrayBuffer": {
        "schema": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "user_id": {
                "description": "The Soundcloud id of the uploader.",
                "minimum": 0,
                "type": "number"
              },
              "start": {
                "description": "Timestamp where the song skip starts.",
                "minimum": 0,
                "type": "number"
              },
              "end": {
                "description": "Timestamp where the song skip ends.",
                "minimum": 0,
                "type": "number"
              }
            },
            "required": [
              "user_id",
              "start",
              "end"
            ]
          }
        }
      },
      "application/json": {
        "schema": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "user_id": {
                "description": "The Soundcloud id of the uploader.",
                "minimum": 0,
                "type": "number"
              },
              "start": {
                "description": "Timestamp where the song skip starts.",
                "minimum": 0,
                "type": "number"
              },
              "end": {
                "description": "Timestamp where the song skip ends.",
                "minimum": 0,
                "type": "number"
              }
            }
          }
        }
      }
    }
  }
}

With these manual fixes, I end up with the schema view that I want:

image

I hope this post clearly outlines the issues I am facing with the swagger plugin. If there is any context missing, please let me know.
Please don't be another stupid oversight by myself🙏

@igrschmidt
Copy link
Contributor

Hello @SwatDoge,
I opened a PR that I think it address part of your problem: #14

@david-plugge
Copy link

david-plugge commented Sep 11, 2023

The issue is still there and it currently stops us from switching to elysia as we have some clients that consume the openapi schema.

const app = new Elysia().post(
    '/test',
    ({ body }) => {
        return body.map(String);
    },
    {
        type: 'json',
        body: t.Array(t.Number()),
        response: {
            200: t.Array(t.String()),
        }
    },
);
{
    "/test": {
        "post": {
            "parameters": [],
            "responses": {
                "200": {
                    "items": {
                        "type": "string"
                    },
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "array"
                            }
                        }
                    }
                }
            },
            "operationId": "postTest",
            "requestBody": {
                "content": {
                    "application/json": {
                        "schema": {
                            "type": "array",
                            "items": {
                                "type": "number"
                            }
                        }
                    }
                }
            }
        }
    }
}

image

@igrschmidt
Copy link
Contributor

igrschmidt commented Sep 11, 2023

@david-plugge you need to declare the response in case of 200.
eg.:

app.get("/api", () => ({ id: 666, name: "John Wick" }), {
  response: {
    200: t.Object({
      id: t.Number({
        description: "Unique identifier.",
        default: 666,
      }),
      name: t.String({
        description: "Name of mercenary.",
        default: "John Wick",
      }),
    }),
  },
  detail: {
    summary: `Gets details of all available mercenaries.`,
    tags: ["Mercenaries"],
  },
});

Edit: Wrapped the schema properties inside the t.Object({})

@david-plugge
Copy link

@igrschmidt that results in the same incorrect schema

@igrschmidt
Copy link
Contributor

@david-plugge I'm sorry, I gave the wrong example. We need to wrap the schema properties inside the t.Object({})

response: {
  200: t.Object({
    id: t.Number({
      description: "Unique identifier.",
      default: 666,
    }),
    name: t.String({
      description: "Name of mercenary.",
      default: "John Wick",
    }),
  }),
}

This should work now

@david-plugge
Copy link

@igrschmidt that works unless you want to return an array:

response: {
  200: t.Array(
    t.Object({
      id: t.Number({
        description: "Unique identifier.",
        default: 666,
      }),
      name: t.String({
        description: "Name of mercenary.",
        default: "John Wick",
      }),
    }),
  ),
}

@david-plugge
Copy link

david-plugge commented Sep 15, 2023

I created a minimal reproduction so you can check it out yourself.

@KilianB
Copy link

KilianB commented Oct 28, 2023

Duplicated by: #28 & #39

@SaltyAom can this bug please be considered in the next release. The issues tackles a core use case of swagger, there are multiple issues opened and a pull request to fix it is available.

@BE-CH
Copy link

BE-CH commented Nov 23, 2023

@SaltyAom Please consider this... Arrays are crucial!

@mpandzo
Copy link

mpandzo commented Dec 13, 2023

@BE-CH, @david-plugge, @KilianB I put together a workaround until the pull request is merged (via #39)

For someone looking to return an array and to control the response description as well as the other parts of the schema (like marking query parameters as not required), here is an example of my approach:

import { Elysia, t } from 'elysia';

const ItemType = {
  name: t.String(),
};

const ItemResponseType = t.Object(ItemType , { title: 'Item' });
ItemResponseType.description = 'Retrieve a single item';

const ItemsResponseType = t.Array(
  t.Object(ItemType, {
    title: 'Item',
  }), {
  title: 'Items',
});
ItemsResponseType.description = 'Retrieve list of items';

const ItemsQuerySchema = t.Object({
  name: t.String(),
});
ItemsQuerySchema.required = [];

const sampleItem = {
  name: 'My item',
};

export const routes = new Elysia()
  .model({
    itemResponseType: ItemResponseType,
    itemsResponseType: ItemsResponseType
  })
  .get('/items', () => [sampleItem], {
    type: 'application/json',
    response: 'itemsResponseType',
    query: ItemsQuerySchema,
  })
  .get('/items/:id', () => sampleItem, {
    type: 'application/json',
    response: 'itemResponseType'
  })

This yields the following Swagger results:

items

items-schema

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants