diff --git a/src/index.ts b/src/index.ts index 4fc45df..c6f2e00 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ -import { Elysia, type InternalRoute } from 'elysia' +import { Elysia, TSchema, type InternalRoute } from 'elysia' import { SwaggerUIRender } from './swagger' import { ScalarRender } from './scalar' @@ -130,7 +130,8 @@ export const swagger = ({ path: route.path, // @ts-ignore models: app.getGlobalDefinitions?.().type, - contentType: route.hooks.type + contentType: route.hooks.type, + standaloneValidators: route.standaloneValidators }) }) else @@ -141,11 +142,21 @@ export const swagger = ({ path: route.path, // @ts-ignore models: app.getGlobalDefinitions?.().type, - contentType: route.hooks.type + contentType: route.hooks.type, + standaloneValidators: route.standaloneValidators }) }) } + // @ts-ignore + const globalDefinitions = { ...app.getGlobalDefinitions?.().type } + // remove $id from globalDefinitions as it breaks OpenAPI compliance + Object.entries(globalDefinitions).map(([key, value]) => { + if (value.$id) { + delete globalDefinitions[key].$id + } + }) + return { openapi: '3.0.3', ...{ diff --git a/src/utils.ts b/src/utils.ts index 19f5170..c187ae7 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,7 +1,13 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ /* eslint-disable @typescript-eslint/no-unused-vars */ import { normalize } from 'pathe' -import { replaceSchemaType, t, type HTTPMethod, type LocalHook } from 'elysia' +import { + InputSchema, + replaceSchemaType, + t, + type HTTPMethod, + type LocalHook +} from 'elysia' import { Kind, type TSchema } from '@sinclair/typebox' import type { OpenAPIV3 } from 'openapi-types' @@ -128,6 +134,43 @@ export const generateOperationId = (method: string, paths: string) => { return operationId } +function resolveModel( + model: TSchema | string | undefined, + models: Record +) { + if (typeof model === 'object') return model + // this intentionally doesn't include the 'modelname[]' check + // because it doesn't make sense to merge t.Array using t.Composite + if (typeof model === 'string') return models[model] + return undefined +} + +function compositeStandalone( + hook: InputSchema, + validators: InputSchema[] | undefined, + models: Record +) { + if (!validators) return hook + if (validators.length === 0) return hook + + const mergeProp = (prop: keyof Omit) => { + const array = [hook[prop], ...validators.map((x) => x[prop])] + .map((x) => resolveModel(x, models)) + .filter((x) => x !== undefined) + if (array.length === 0) return undefined + if (array.length === 1) return array[0] + return t.Composite(array) + } + + return { + ...hook, + body: mergeProp('body'), + params: mergeProp('params'), + headers: mergeProp('headers'), + query: mergeProp('query') + } +} + const cloneHook = (hook: T) => { if (!hook) return if (typeof hook === 'string') return hook @@ -140,7 +183,8 @@ export const registerSchemaPath = ({ path, method, hook, - models + models, + standaloneValidators }: { schema: Partial contentType?: string | string[] @@ -148,8 +192,10 @@ export const registerSchemaPath = ({ method: HTTPMethod hook?: LocalHook models: Record + standaloneValidators?: InputSchema[] }) => { hook = cloneHook(hook) + hook = compositeStandalone(hook, standaloneValidators, models) if (hook.parse && !Array.isArray(hook.parse)) hook.parse = [hook.parse] @@ -216,6 +262,7 @@ export const registerSchemaPath = ({ additionalProperties, patternProperties, $ref, + items, ...rest } = responseSchema as typeof responseSchema & { type: string @@ -234,7 +281,7 @@ export const registerSchemaPath = ({ type, properties, patternProperties, - items: responseSchema.items, + items, required } as any) : responseSchema @@ -353,7 +400,14 @@ export const registerSchemaPath = ({ required: true, content: mapTypesResponse( contentTypes, - typeof bodySchema === 'string' + typeof bodySchema === 'string' && bodySchema.slice(-2) === '[]' + ? { + type: 'array', + items: { + $ref: `#/components/schemas/${bodySchema.slice(0, -2)}` + } + } + : typeof bodySchema === 'string' ? { $ref: `#/components/schemas/${bodySchema}` } diff --git a/test/validate-schema.test.ts b/test/validate-schema.test.ts index ee9558e..9e5ed89 100644 --- a/test/validate-schema.test.ts +++ b/test/validate-schema.test.ts @@ -16,15 +16,28 @@ it('returns a valid Swagger/OpenAPI json config for many routes', async () => { .get('/unpath/:id', ({ params: { id } }) => id, { response: t.String({ description: 'sample description' }) }) + .model({ + thing: t.Object({ + bar: t.String() + }) + }) .get( '/unpath/:id/:name/:age', ({ params: { id, name } }) => `${id} ${name}`, { type: 'json', response: t.String({ description: 'sample description' }), - params: t.Object({ id: t.String(), name: t.String() }) + params: t.Object({ id: t.String(), name: t.String() }), + body: "thing[]" } ) + .guard({ + schema: "standalone", + body: t.Object({ + foo: t.String() + }), + query: "thing" + }) .post( '/json/:id', ({ body, params: { id }, query: { name, email, birthday } }) => ({