{"version":3,"sources":["../../../src/server/openapi.ts"],"sourcesContent":["import { convert } from '@openapi-contrib/json-schema-to-openapi-schema'\nimport type { OpenAPIV3_1 } from 'openapi-types'\nimport { capitalize, type JsonSchema } from 'valleyed'\n\nimport { Instance } from '../instance'\nimport type { ServerConfig } from './pipes'\nimport { Router } from './routes'\nimport type { Route } from './types'\n\ndeclare module 'openapi-types' {\n\tnamespace OpenAPIV3 {\n\t\tinterface Document {\n\t\t\t'x-tagGroups': { name: string; tags: string[] }[]\n\t\t}\n\t\tinterface TagObject {\n\t\t\t'x-displayName': string\n\t\t}\n\t}\n}\n\nexport type OpenApiSchemaDef = {\n\trequest: Partial<Record<'body' | 'query' | 'params' | 'headers' | 'response', JsonSchema>>\n\tresponse: Partial<Record<'response' | 'responseHeaders', { status: number; schema: JsonSchema; contentType: string }[]>>\n}\n\nexport class OpenApi {\n\t#registeredTags: Record<string, boolean> = {}\n\t#registeredTagGroups: Record<string, { name: string; tags: string[] }> = {}\n\t#baseOpenapiDoc: OpenAPIV3_1.Document\n\n\tconstructor(private config: ServerConfig) {\n\t\tconst instance = Instance.get()\n\t\tconst { app } = instance.settings\n\t\tthis.#baseOpenapiDoc = {\n\t\t\topenapi: '3.0.0',\n\t\t\tinfo: {\n\t\t\t\ttitle: `${app.name} ${instance.id}`,\n\t\t\t\tversion: config.openapi.docsVersion ?? '',\n\t\t\t},\n\t\t\tservers: config.openapi.docsBaseUrl?.map((url) => ({ url })),\n\t\t\tpaths: {},\n\t\t\tcomponents: {\n\t\t\t\tschemas: {},\n\t\t\t\tsecuritySchemes: {\n\t\t\t\t\tAuthorization: {\n\t\t\t\t\t\ttype: 'apiKey',\n\t\t\t\t\t\tname: 'authorization',\n\t\t\t\t\t\tin: 'header',\n\t\t\t\t\t},\n\t\t\t\t\tRefreshToken: {\n\t\t\t\t\t\ttype: 'apiKey',\n\t\t\t\t\t\tname: 'x-refresh-token',\n\t\t\t\t\t\tin: 'header',\n\t\t\t\t\t},\n\t\t\t\t\tApiKey: {\n\t\t\t\t\t\ttype: 'apiKey',\n\t\t\t\t\t\tname: 'x-api-key',\n\t\t\t\t\t\tin: 'header',\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\ttags: [],\n\t\t\t'x-tagGroups': [],\n\t\t}\n\t}\n\n\tcleanPath(path: string) {\n\t\tlet cleaned = path.replace(/(\\/\\s*)+/g, '/')\n\t\tif (!cleaned.startsWith('/')) cleaned = `/${cleaned}`\n\t\tif (cleaned !== '/' && cleaned.endsWith('/')) cleaned = cleaned.slice(0, -1)\n\t\treturn cleaned\n\t}\n\n\tasync register(route: Route<any>, def: OpenApiSchemaDef) {\n\t\tif (route.hide) return\n\n\t\tconst tag = this.#buildTag(route.groups ?? [])\n\n\t\tconst cleanPath = this.cleanPath(route.path)\n\t\tconst operationId = `(${route.method.toUpperCase()}) ${cleanPath}`\n\t\tawait this.#addRouteToOpenApiDoc(cleanPath, route.method.toLowerCase(), def, {\n\t\t\toperationId,\n\t\t\tsummary: route.title ?? cleanPath,\n\t\t\tdescription: route.descriptions?.join('\\n\\n'),\n\t\t\ttags: tag ? [tag] : undefined,\n\t\t\tsecurity: route.security,\n\t\t})\n\t}\n\n\tasync #addRouteToOpenApiDoc(path: string, method: string, def: OpenApiSchemaDef, methodObj: OpenAPIV3_1.OperationObject) {\n\t\tif (def.response.response?.length) {\n\t\t\tmethodObj.responses ??= {}\n\t\t\tfor (const resp of def.response.response) {\n\t\t\t\tmethodObj.responses[resp.status] ??= { description: '', content: {} }\n\t\t\t\tconst res = methodObj.responses[resp.status] as OpenAPIV3_1.ResponseObject\n\t\t\t\tres.content![resp.contentType] = { schema: await convert(this.#visit(resp.schema)) }\n\t\t\t}\n\t\t}\n\n\t\tif (def.response.responseHeaders?.length) {\n\t\t\tmethodObj.responses ??= {}\n\t\t\tfor (const resp of def.response.responseHeaders) {\n\t\t\t\tmethodObj.responses[resp.status] ??= { description: '', content: {} }\n\t\t\t\tmethodObj.responses[resp.status] as OpenAPIV3_1.ResponseObject\n\t\t\t\tconst res = methodObj.responses[resp.status] as OpenAPIV3_1.ResponseObject\n\t\t\t\tres.headers = { schema: (await convert(this.#visit(resp.schema))) as any }\n\t\t\t}\n\t\t}\n\n\t\tif (def.request.body)\n\t\t\tmethodObj.requestBody = {\n\t\t\t\trequired: true,\n\t\t\t\tcontent: {\n\t\t\t\t\t'application/json': { schema: await convert(this.#visit(def.request.body)) },\n\t\t\t\t},\n\t\t\t}\n\n\t\tconst parameters: OpenAPIV3_1.ParameterObject[] = []\n\n\t\tconst addParams = async (location: 'query' | 'path' | 'header', schema: JsonSchema | undefined) => {\n\t\t\tif (!schema) return\n\t\t\tconst flat = this.#flattenForParameters(schema)\n\t\t\tfor (const schema of flat) {\n\t\t\t\tif (!schema.properties) continue\n\t\t\t\tfor (const [name, value] of Object.entries(schema.properties))\n\t\t\t\t\tparameters.push({\n\t\t\t\t\t\tname,\n\t\t\t\t\t\tin: location,\n\t\t\t\t\t\tschema: await convert(this.#visit(value)),\n\t\t\t\t\t\trequired: (schema.required || []).includes(name),\n\t\t\t\t\t})\n\t\t\t}\n\t\t}\n\n\t\tawait Promise.all([\n\t\t\taddParams('query', def.request.query),\n\t\t\taddParams('path', def.request.params),\n\t\t\taddParams('header', def.request.headers),\n\t\t])\n\t\tif (parameters.length) methodObj.parameters = parameters\n\n\t\tconst base = this.#baseOpenapiDoc\n\t\tif (!base.paths) base.paths = {}\n\t\tif (!base.paths[path]) base.paths[path] = {}\n\t\tbase.paths[path]![method] = methodObj\n\t}\n\n\trouter() {\n\t\tconst jsonPath = '/openapi.json'\n\t\tconst router = new Router({ path: this.config.openapi.docsPath ?? '/', hide: true })\n\t\trouter.get('/index.html')((req) => req.res({ body: this.#html(`.${jsonPath}`), contentType: 'text/html' }))\n\t\trouter.get(jsonPath)((req) => req.res({ body: this.#baseOpenapiDoc }))\n\t\treturn router\n\t}\n\n\t#flattenForParameters(node: JsonSchema): JsonSchema[] {\n\t\tconst { allOf, oneOf, anyOf, ...schema } = node\n\t\tif (allOf) return allOf.flatMap((n) => this.#flattenForParameters(n))\n\t\treturn [schema]\n\t}\n\n\t#visit(node: JsonSchema) {\n\t\tif (!node || typeof node !== 'object') return node\n\t\tif (typeof node.$refId === 'string') {\n\t\t\tconst { $refId: id, ...rest } = node\n\t\t\tconst res = this.#visit(rest)\n\t\t\tif (this.#baseOpenapiDoc.components?.schemas) {\n\t\t\t\tthis.#baseOpenapiDoc.components.schemas[id] = res\n\t\t\t\treturn { $ref: `#/components/schemas/${id}` }\n\t\t\t} else return res\n\t\t}\n\n\t\tif (Array.isArray(node)) return node.map((n) => this.#visit(n)) as any\n\t\treturn Object.fromEntries(Object.entries(node).map(([key, value]) => [key, this.#visit(value as any)]))\n\t}\n\n\t#buildTag(groups: NonNullable<Route<any>['groups']>) {\n\t\tif (!groups.length) return undefined\n\t\tconst parsed = groups.map((g) => (typeof g === 'string' ? { name: g } : g))\n\t\tconst name = parsed.map((g) => g.name).join(' > ')\n\t\tconst displayName = parsed.at(-1)?.name ?? ''\n\t\tconst description = parsed\n\t\t\t.map((g) => g.description?.trim() ?? '')\n\t\t\t.filter(Boolean)\n\t\t\t.join('\\n\\n\\n\\n')\n\n\t\tif (!this.#registeredTags[name]) {\n\t\t\tthis.#registeredTags[name] = true\n\t\t\tthis.#baseOpenapiDoc.tags!.push({ name, 'x-displayName': displayName, description })\n\n\t\t\tconst tagGroups = parsed.slice(0, -1)\n\t\t\tconst groupName = tagGroups.map((g) => g.name).join(' > ') || 'default'\n\t\t\tif (!this.#registeredTagGroups[groupName]) {\n\t\t\t\tconst group = { name: groupName, tags: [] }\n\t\t\t\tthis.#baseOpenapiDoc['x-tagGroups'].push(group)\n\t\t\t\tthis.#registeredTagGroups[groupName] = group\n\t\t\t}\n\t\t\tthis.#registeredTagGroups[groupName].tags = [...new Set([...this.#registeredTagGroups[groupName].tags, name])]\n\t\t}\n\n\t\treturn name\n\t}\n\n\t#html(jsonPath: string) {\n\t\tconst instance = Instance.get()\n\t\tconst title = capitalize(`${instance.settings.app.name} ${instance.id}`)\n\t\treturn `\n<!doctype html>\n<html>\n  <head>\n    <title>${title}</title>\n    <meta charset=\"utf-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n\t<style>\n      .darklight-reference {\n        display: none;\n      }\n    </style>\n  </head>\n  <body>\n    <script id=\"api-reference\" data-url=\"${jsonPath}\"></script>\n    <script>\n      const configuration = { theme: 'purple' };\n      document.getElementById('api-reference').dataset.configuration = JSON.stringify(configuration);\n    </script>\n    <script src=\"https://cdn.jsdelivr.net/npm/@scalar/api-reference@1.38.0\"></script>\n  </body>\n</html>\n`\n\t}\n}\n"],"mappings":"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","names":["convert","capitalize","Instance","Router","OpenApi","config","instance","app","#baseOpenapiDoc","url","#registeredTags","#registeredTagGroups","path","cleaned","route","def","tag","#buildTag","cleanPath","operationId","#addRouteToOpenApiDoc","method","methodObj","resp","res","#visit","parameters","addParams","location","schema","flat","#flattenForParameters","name","value","base","jsonPath","router","req","#html","node","allOf","oneOf","anyOf","n","id","rest","key","groups","parsed","g","displayName","description","groupName","group"]}