diff --git a/src/utils.ts b/src/utils.ts index 6812237..45eaf47 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,28 +1,28 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ /* eslint-disable @typescript-eslint/no-unused-vars */ -import type { HTTPMethod, LocalHook } from 'elysia' +import type { HTTPMethod, LocalHook } from 'elysia'; -import { Kind, type TSchema } from '@sinclair/typebox' -import type { OpenAPIV3 } from 'openapi-types' +import { Kind, type TSchema, type TAnySchema } from '@sinclair/typebox'; +import type { OpenAPIV3 } from 'openapi-types'; -import deepClone from 'lodash.clonedeep' +import deepClone from 'lodash.clonedeep'; export const toOpenAPIPath = (path: string) => path .split('/') .map((x) => (x.startsWith(':') ? `{${x.slice(1, x.length)}}` : x)) - .join('/') + .join('/'); export const mapProperties = ( name: string, schema: TSchema | string | undefined, - models: Record + models: Record, ) => { - if (schema === undefined) return [] + if (schema === undefined) return []; if (typeof schema === 'string') - if (schema in models) schema = models[schema] - else throw new Error(`Can't find model ${schema}`) + if (schema in models) schema = models[schema]; + else throw new Error(`Can't find model ${schema}`); return Object.entries(schema?.properties ?? []).map(([key, value]) => { const { type: valueType = undefined, ...rest } = value as any; @@ -36,182 +36,116 @@ export const mapProperties = ( required: schema!.required?.includes(key) ?? false, }; }); -} - -const mapTypesResponse = ( - types: string[], - schema: - | string - | { - type: string - properties: Object - required: string[] - } -) => { - if (typeof schema === 'object' - && ['void', 'undefined', 'null'].includes(schema.type)) return; +}; - const responses: Record = {} +const mapTypesResponse = (types: string[], schema: TAnySchema) => { + const responses: Record = {}; for (const type of types) responses[type] = { schema: typeof schema === 'string' ? { - $ref: `#/components/schemas/${schema}` + $ref: `#/components/schemas/${schema}`, } - : { ...(schema as any) } - } + : { ...(schema as any) }, + }; - return responses -} + return responses; +}; export const capitalize = (word: string) => - word.charAt(0).toUpperCase() + word.slice(1) + word.charAt(0).toUpperCase() + word.slice(1); export const generateOperationId = (method: string, paths: string) => { - let operationId = method.toLowerCase() + let operationId = method.toLowerCase(); - if (paths === '/') return operationId + 'Index' + if (paths === '/') return operationId + 'Index'; for (const path of paths.split('/')) { if (path.charCodeAt(0) === 123) { - operationId += 'By' + capitalize(path.slice(1, -1)) + operationId += 'By' + capitalize(path.slice(1, -1)); } else { - operationId += capitalize(path) + operationId += capitalize(path); } } - return operationId -} + return operationId; +}; export const registerSchemaPath = ({ schema, path, method, hook, - models + models, }: { - schema: Partial - contentType?: string | string[] - path: string - method: HTTPMethod - hook?: LocalHook - models: Record + schema: Partial; + contentType?: string | string[]; + path: string; + method: HTTPMethod; + hook?: LocalHook; + models: Record; }) => { - if (hook) hook = deepClone(hook) + if (hook) hook = deepClone(hook); const contentType = hook?.type ?? [ 'application/json', 'multipart/form-data', - 'text/plain' - ] + 'text/plain', + ]; - path = toOpenAPIPath(path) + path = toOpenAPIPath(path); const contentTypes = typeof contentType === 'string' ? [contentType] - : contentType ?? ['application/json'] - - const bodySchema = hook?.body - const paramsSchema = hook?.params - const headerSchema = hook?.headers - const querySchema = hook?.query - let responseSchema = hook?.response as unknown as OpenAPIV3.ResponsesObject - - if (typeof responseSchema === 'object') { - if (Kind in responseSchema) { - const { type, properties, required, additionalProperties, ...rest } = - responseSchema as typeof responseSchema & { - type: string - properties: Object - required: string[] - } + : contentType ?? ['application/json']; + + const bodySchema = hook?.body; + const paramsSchema = hook?.params; + const headerSchema = hook?.headers; + const querySchema = hook?.query; + const responseSchema: OpenAPIV3.ResponsesObject = {}; + + const addToResponseSchema = ( + code: string, + { description, ...schema }: TAnySchema, + ) => { + const ignore = ['Undefined', 'Null', 'Void'].includes(schema[Kind]); + + responseSchema[code] = { + description: description as string, + content: ignore + ? undefined + : mapTypesResponse(contentTypes, schema), + }; + }; - responseSchema = { - '200': { - ...rest, - description: rest.description as any, - content: mapTypesResponse( - contentTypes, - type === 'object' || type === 'array' - ? ({ - type, - properties, - required - } as any) - : responseSchema - ) - } - } - } else { - Object.entries(responseSchema as Record).forEach( - ([key, value]) => { - if (typeof value === 'string') { - if(!models[value]) return - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { type, properties, required, additionalProperties: _, ...rest } = models[ - value - ] as TSchema & { - type: string - properties: Object - required: string[] - } - - responseSchema[key] = { - ...rest, - description: rest.description as any, - content: mapTypesResponse(contentTypes, value) - } - } else { - const { type, properties, required, additionalProperties, ...rest } = - value as typeof value & { - type: string - properties: Object - required: string[] - } - - responseSchema[key] = { - ...rest, - description: rest.description as any, - content: mapTypesResponse(contentTypes, { - type, - properties, - required - }) - } - } - } - ) - } - } else if (typeof responseSchema === 'string') { - if(!(responseSchema in models)) return - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { type, properties, required, additionalProperties: _, ...rest } = models[ - responseSchema - ] as TSchema & { - type: string - properties: Object - required: string[] - } + const userProvidedResponseSchema = hook?.response; - responseSchema = { - // @ts-ignore - '200': { - ...rest, - content: mapTypesResponse(contentTypes, responseSchema) - } + if (typeof userProvidedResponseSchema === 'object') { + if (Kind in userProvidedResponseSchema) { + addToResponseSchema('200', userProvidedResponseSchema); + } else { + Object.entries( + userProvidedResponseSchema as Record, + ).forEach(([key, value]) => { + addToResponseSchema( + key, + typeof value === 'string' ? models[value] : value, + ); + }); } + } else if (typeof userProvidedResponseSchema === 'string') { + addToResponseSchema('200', models[userProvidedResponseSchema]); } const parameters = [ ...mapProperties('header', headerSchema, models), ...mapProperties('path', paramsSchema, models), - ...mapProperties('query', querySchema, models) - ] + ...mapProperties('query', querySchema, models), + ]; schema[path] = { ...(schema[path] ? schema[path] : {}), @@ -221,7 +155,7 @@ export const registerSchemaPath = ({ : {}) satisfies OpenAPIV3.ParameterObject), ...(responseSchema ? { - responses: responseSchema + responses: responseSchema, } : {}), operationId: @@ -234,45 +168,45 @@ export const registerSchemaPath = ({ contentTypes, typeof bodySchema === 'string' ? { - $ref: `#/components/schemas/${bodySchema}` + $ref: `#/components/schemas/${bodySchema}`, } - : (bodySchema as any) - ) - } + : (bodySchema as any), + ), + }, } - : null) - } satisfies OpenAPIV3.OperationObject - } -} + : null), + } satisfies OpenAPIV3.OperationObject, + }; +}; export const filterPaths = ( paths: Record, { excludeStaticFile = true, - exclude = [] + exclude = [], }: { - excludeStaticFile: boolean - exclude: (string | RegExp)[] - } + excludeStaticFile: boolean; + exclude: (string | RegExp)[]; + }, ) => { - const newPaths: Record = {} + const newPaths: Record = {}; for (const [key, value] of Object.entries(paths)) if ( !exclude.some((x) => { - if (typeof x === 'string') return key === x + if (typeof x === 'string') return key === x; - return x.test(key) + return x.test(key); }) && !key.includes('/swagger') && !key.includes('*') && (excludeStaticFile ? !key.includes('.') : true) ) { Object.keys(value).forEach((method) => { - const schema = value[method] + const schema = value[method]; if (key.includes('{')) { - if (!schema.parameters) schema.parameters = [] + if (!schema.parameters) schema.parameters = []; schema.parameters = [ ...key @@ -284,27 +218,27 @@ export const filterPaths = ( (params: Record) => params.in === 'path' && params.name === - x.slice(1, x.length - 1) - ) + x.slice(1, x.length - 1), + ), ) .map((x) => ({ schema: { type: 'string' }, in: 'path', name: x.slice(1, x.length - 1), - required: true + required: true, })), - ...schema.parameters - ] + ...schema.parameters, + ]; } if (!schema.responses) schema.responses = { - 200: {} - } - }) + 200: {}, + }; + }); - newPaths[key] = value + newPaths[key] = value; } - return newPaths -} + return newPaths; +};