UNPKG

@budibase/server

Version:
419 lines (377 loc) • 12.3 kB
import { GetQueriesOptions, ImportInfo } from "./base" import { BodyType, Query, QueryParameter, RestTemplateQueryMetadata, } from "@budibase/types" import { QueryVerbToHttpMethod } from "../../../../../constants" import { OpenAPIV2 } from "openapi-types" import { OpenAPISource } from "./base/openapi" import { URL } from "url" import { GeneratedRequestBody, buildRequestBodyFromFormDataParameters, buildSerializableRequestBody, generateRequestBodyFromExample, generateRequestBodyFromSchema, buildKeyValueRequestBody, type FormDataParameter, } from "./utils/requestBody" const parameterNotRef = ( param: OpenAPIV2.Parameter | OpenAPIV2.ReferenceObject ): param is OpenAPIV2.Parameter => { // all refs are deferenced by parser library return true } const isOpenAPI2 = (document: any): document is OpenAPIV2.Document => { if (document.swagger === "2.0") { return true } else { return false } } const methods: string[] = Object.values(OpenAPIV2.HttpMethods) const isOperation = ( key: string, pathItem: any ): pathItem is OpenAPIV2.OperationObject => { return methods.includes(key) } const isParameter = ( key: string, pathItem: any ): pathItem is OpenAPIV2.Parameter => { return !isOperation(key, pathItem) } /** * OpenAPI Version 2.0 - aka "Swagger" * https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md */ export class OpenAPI2 extends OpenAPISource { document!: OpenAPIV2.Document private securityHeaders: Map<string, string> = new Map() private getDocsUrl = ( operation: OpenAPIV2.OperationObject ): string | undefined => { return ( operation.externalDocs?.url || this.document.externalDocs?.url || this.document.info?.termsOfService || this.document.info?.contact?.url ) } private setSecurityHeaders = () => { this.securityHeaders = new Map() const securityDefinitions = this.document.securityDefinitions if (!securityDefinitions) { return } for (const scheme of Object.values(securityDefinitions)) { const headerName = this.getSecuritySchemeHeader(scheme) if (headerName) { this.securityHeaders.set(headerName.toLowerCase(), headerName) } } } private getSecuritySchemeHeader( scheme?: OpenAPIV2.SecuritySchemeObject ): string | undefined { if (!scheme) { return undefined } if (scheme.type === "apiKey" && scheme.in === "header" && scheme.name) { return scheme.name } return undefined } private isSecurityHeader(name?: string): boolean { if (!name) { return false } return this.securityHeaders.has(name.toLowerCase()) } private buildRestTemplateMetadata = ( operation: OpenAPIV2.OperationObject, path: string, requestBody: GeneratedRequestBody | undefined, parameters: QueryParameter[], bodyType?: BodyType ): RestTemplateQueryMetadata => { const metadata: RestTemplateQueryMetadata = { operationId: operation.operationId, docsUrl: this.getDocsUrl(operation), description: operation.summary || operation.description, originalPath: path, } let parsedBody = buildSerializableRequestBody(requestBody?.body) if ( parsedBody !== undefined && bodyType && (bodyType === BodyType.FORM_DATA || bodyType === BodyType.ENCODED) ) { parsedBody = buildKeyValueRequestBody(parsedBody) } if (parsedBody !== undefined) { metadata.originalRequestBody = parsedBody } const defaultBindings = this.buildDefaultBindings( parameters, requestBody?.bindings ) if (defaultBindings) { metadata.defaultBindings = defaultBindings } return metadata } isSupported = async (data: string): Promise<boolean> => { try { const document: any = await this.parseData(data) if (isOpenAPI2(document)) { this.document = document this.setSecurityHeaders() return true } else { return false } } catch (err) { return false } } getUrl = (): URL | undefined => { const scheme = this.document.schemes?.includes("https") ? "https" : "http" const basePath = this.document.basePath || "" const host = this.document.host if (!host) { return undefined } const normalizedBasePath = basePath ? basePath.startsWith("/") ? basePath : `/${basePath}` : "" try { return new URL(`${scheme}://${host}${normalizedBasePath}`) } catch (_err) { return undefined } } private getEndpoints = async (): Promise<ImportInfo["endpoints"]> => { const queries = await this.getQueries("") const endpoints: ImportInfo["endpoints"] = [] for (const query of queries) { const metadata = query.restTemplateMetadata if (!metadata) { continue } const path = metadata.originalPath || query.fields.path || "" const method = QueryVerbToHttpMethod[query.queryVerb] if (!this.isSupportedMethod(method)) { continue } endpoints.push({ id: this.buildEndpointId(method || "", path), name: query.name, method: method?.toUpperCase() || "", path, description: metadata.description, queryVerb: query.queryVerb, operationId: metadata.operationId, docsUrl: metadata.docsUrl, originalPath: metadata.originalPath, originalRequestBody: metadata.originalRequestBody, defaultBindings: metadata.defaultBindings, bodyType: query.fields.bodyType, headers: query.fields.headers && Object.keys(query.fields.headers).length > 0 ? query.fields.headers : undefined, queryString: query.fields.queryString || undefined, }) } return endpoints } getInfo = async (): Promise<ImportInfo> => { const name = this.document.info.title || "Swagger Import" const rawUrl = this.getUrl()?.href const url = rawUrl ? this.convertPathVariables(rawUrl) : undefined const docsUrl = this.document.externalDocs?.url || this.document.info?.termsOfService || this.document.info?.contact?.url return { name, url, docsUrl, endpoints: await this.getEndpoints(), securityHeaders: this.getSecurityHeaders(), } } getSecurityHeaders(): string[] { return Array.from(new Set(this.securityHeaders.values())) } getImportSource(): string { return "openapi2.0" } getQueries = async ( datasourceId: string, options?: GetQueriesOptions ): Promise<Query[]> => { const url = this.getUrl() const queries = [] const filterIds = options?.filterIds for (let [path, pathItem] of Object.entries(this.document.paths)) { // parameters that apply to every operation in the path let pathParams: OpenAPIV2.Parameter[] = [] for (let [key, opOrParams] of Object.entries(pathItem)) { if (isParameter(key, opOrParams)) { const pathParameters = opOrParams as OpenAPIV2.Parameter[] pathParams.push(...pathParameters) continue } // can not be a parameter, must be an operation const operation = opOrParams as OpenAPIV2.OperationObject const methodName = key if (!this.isSupportedMethod(methodName)) { continue } const endpointId = this.buildEndpointId(methodName, path) if (filterIds && !filterIds.has(endpointId)) { continue } const name = operation.operationId || path let queryString = "" const headers: Record<string, unknown> = {} const setHeader = (headerName: string, value: unknown) => { if (!headerName) { return } const normalized = headerName.toLowerCase() const existingKey = Object.keys(headers).find( existing => existing.toLowerCase() === normalized ) headers[existingKey || headerName] = value } let primaryMimeType: string | undefined let requestBody: GeneratedRequestBody | undefined = undefined const parameters: QueryParameter[] = [] if (operation.consumes) { primaryMimeType = operation.consumes[0] setHeader("Content-Type", primaryMimeType) } const formDataParams: FormDataParameter[] = [] // combine the path parameters with the operation parameters const operationParams = operation.parameters || [] const allParams = [...pathParams, ...operationParams] for (let param of allParams) { if (parameterNotRef(param)) { let skipParameterBinding = false switch (param.in) { case "query": { let prefix = "" if (queryString) { prefix = "&" } queryString = `${queryString}${prefix}${param.name}={{${param.name}}}` break } case "header": if (this.isSecurityHeader(param.name)) { skipParameterBinding = true break } setHeader(param.name, `{{${param.name}}}`) break case "path": // do nothing: param is already in the path break case "formData": formDataParams.push({ ...(param as OpenAPIV2.ParameterObject), in: "formData", } as FormDataParameter) break case "body": { let bodyParam: OpenAPIV2.InBodyParameterObject = param as OpenAPIV2.InBodyParameterObject const schema = bodyParam.schema as OpenAPIV2.SchemaObject if (schema) { if (schema.example !== undefined) { requestBody = generateRequestBodyFromExample( schema.example, bodyParam.name || "body" ) } else { const generated = generateRequestBodyFromSchema( schema, bodyParam.name || "body" ) if (generated !== undefined) { requestBody = generated } } } break } } // add the parameter if it can be bound in our config if ( !skipParameterBinding && ["query", "header", "path", "formData"].includes(param.in) ) { const defaultValue = param.default !== undefined ? String(param.default) : "" parameters.push({ name: param.name, default: defaultValue, }) } } } if (!requestBody && formDataParams.length > 0) { const formDataBody = buildRequestBodyFromFormDataParameters(formDataParams) if (formDataBody) { requestBody = formDataBody if (!primaryMimeType) { primaryMimeType = formDataParams.some( param => param.type === "file" ) ? "multipart/form-data" : "application/x-www-form-urlencoded" } if (primaryMimeType) { setHeader("Content-Type", primaryMimeType) } } } const bodyType = primaryMimeType ? this.bodyTypeFromMimeType(primaryMimeType) : undefined const restTemplateMetadata = this.buildRestTemplateMetadata( operation, path, requestBody, parameters, bodyType ) const query = this.constructQuery( datasourceId, name, methodName, path, url, queryString, headers, parameters, requestBody?.body, requestBody?.bindings ?? {}, bodyType, restTemplateMetadata ) queries.push(query) } } return queries } }