UNPKG

@scalar/oas-utils

Version:

Open API spec and Yaml handling utilities

375 lines (374 loc) 13.7 kB
import { isDefined } from "@scalar/helpers/array/is-defined"; import { isHttpMethod } from "@scalar/helpers/http/is-http-method"; import { combineUrlAndPath } from "@scalar/helpers/url/merge-urls"; import { keysOf } from "@scalar/object-utils/arrays"; import { dereference, load, upgrade } from "@scalar/openapi-parser"; import { securitySchemeSchema } from "@scalar/types/entities"; import { collectionSchema } from "../entities/spec/collection.js"; import { createExampleFromRequest } from "../entities/spec/request-examples.js"; import { requestSchema } from "../entities/spec/requests.js"; import { serverSchema } from "../entities/spec/server.js"; import { tagSchema } from "../entities/spec/spec-objects.js"; import { schemaModel } from "../helpers/schema-model.js"; const dereferenceDocument = async (document, { shouldLoad = true } = {}) => { if (document === null || typeof document === "string" && document.trim() === "") { console.warn("[@scalar/oas-utils] Empty OpenAPI document provided."); return { schema: {}, errors: [] }; } let filesystem = document; let loadErrors = []; if (shouldLoad) { const response = await load(document).catch((e) => ({ errors: [ { code: e.code, message: e.message } ], filesystem: [] })); filesystem = response.filesystem; loadErrors = response.errors ?? []; } const { specification } = upgrade(filesystem); const { schema, errors: derefErrors = [] } = await dereference(specification); return { schema, errors: [...loadErrors, ...derefErrors] }; }; const parseSchema = async (originalDocument, { shouldLoad = true, /** If a dereferenced document is provided, we will skip the dereferencing step. */ dereferencedDocument = void 0 } = {}) => { const { schema, errors } = dereferencedDocument ? { schema: dereferencedDocument, errors: [] } : ( // Otherwise, dereference the original document await dereferenceDocument(originalDocument ?? "", { shouldLoad }) ); if (!schema) { console.warn("[@scalar/oas-utils] OpenAPI Parser Warning: Schema is undefined"); } return { /** * Temporary fix for the parser returning an empty array * TODO: remove this once the parser is fixed */ schema: Array.isArray(schema) ? {} : schema, errors }; }; const getSelectedSecuritySchemeUids = (securityRequirements, preferredSecurityNames = [], securitySchemeMap) => { const names = securityRequirements[0] && !preferredSecurityNames.length ? [securityRequirements[0]] : preferredSecurityNames; const uids = names.map( (name) => Array.isArray(name) ? name.map((k) => securitySchemeMap[k]).filter(isDefined) : securitySchemeMap[name] ).filter(isDefined); return uids; }; const getSlugUid = (slug) => `slug-uid-${slug}`; async function importSpecToWorkspace(content, { /** If a dereferenced document is provided, we will skip the dereferencing step. */ dereferencedDocument, authentication, baseServerURL, documentUrl, servers: configuredServers, useCollectionSecurity = false, slug, shouldLoad, watchMode = false } = {}) { const { schema, errors } = await parseSchema(content, { shouldLoad, dereferencedDocument }); const importWarnings = [...errors.map((e) => e.message)]; if (!schema) { return { importWarnings, error: true, collection: void 0 }; } const start = performance.now(); const requests = []; const collectionServers = getServersFromOpenApiDocument(configuredServers || schema.servers, { baseServerURL, documentUrl }); const operationServers = []; if (!collectionServers.length) { const fallbackUrl = getFallbackUrl(); if (fallbackUrl) { collectionServers.push(serverSchema.parse({ url: fallbackUrl })); } } const tagNames = /* @__PURE__ */ new Set(); const security = schema.components?.securitySchemes ?? schema?.securityDefinitions ?? {}; if (authentication?.oAuth2 || authentication?.apiKey || authentication?.http) { console.warn( `DEPRECATION WARNING: It looks like you're using legacy authentication config. Please migrate to use the updated config. See https://github.com/scalar/scalar/blob/main/documentation/configuration.md#authentication-partial This will be removed in a future version.` ); } const securitySchemes = Object.entries(security).map?.(([nameKey, _scheme]) => { const payload = { ..._scheme, // Add the new auth config overrides, we keep the old code below for backwards compatibility ...authentication?.securitySchemes?.[nameKey] ?? {}, nameKey }; if (payload.type === "oauth2" && payload.flows) { const flowKeys = Object.keys(payload.flows); flowKeys.forEach((key) => { if (!payload.flows?.[key] || _scheme.type !== "oauth2") { return; } const authFlow = authentication?.securitySchemes?.[nameKey]?.flows?.[key] ?? {}; payload.flows[key] = { ..._scheme.flows?.[key] ?? {}, ...authFlow }; const flow = payload.flows[key]; flow.type = key; if (authentication?.oAuth2) { if (authentication.oAuth2.accessToken) { flow.token = authentication.oAuth2.accessToken; } if (authentication.oAuth2.clientId) { flow["x-scalar-client-id"] = authentication.oAuth2.clientId; } if (authentication.oAuth2.scopes) { flow.selectedScopes = authentication.oAuth2.scopes; } if (flow.type === "password") { flow.username = authentication.oAuth2.username; flow.password = authentication.oAuth2.password; } } if (Array.isArray(flow.scopes)) { flow.scopes = flow.scopes.reduce((prev, s) => ({ ...prev, [s]: "" }), {}); } if (flow["x-defaultClientId"]) { flow["x-scalar-client-id"] = flow["x-defaultClientId"]; } }); } else if (authentication) { if (payload.type === "apiKey" && authentication.apiKey?.token) { payload.value = authentication.apiKey.token; } else if (payload.type === "http") { if (payload.scheme === "basic" && authentication.http?.basic) { payload.username = authentication.http.basic.username ?? ""; payload.password = authentication.http.basic.password ?? ""; } else if (payload.scheme === "bearer" && authentication.http?.bearer?.token) { payload.token = authentication.http.bearer.token ?? ""; } } } const scheme = schemaModel(payload, securitySchemeSchema, false); if (!scheme) { importWarnings.push(`Security scheme ${nameKey} is invalid.`); } return scheme; }).filter((v) => !!v); const securitySchemeMap = {}; securitySchemes.forEach((s) => { securitySchemeMap[s.nameKey] = s.uid; }); keysOf(schema.paths ?? {}).forEach((pathString) => { const path = schema?.paths?.[pathString]; if (!path) { return; } const pathServers = serverSchema.array().parse(path.servers ?? []); for (const server of pathServers) { collectionServers.push(server); } const methods = Object.keys(path).filter(isHttpMethod); methods.forEach((method) => { const operation = path[method]; if (!operation) { return; } const operationLevelServers = serverSchema.array().parse(operation.servers ?? []); for (const server of operationLevelServers) { operationServers.push(server); } operation.tags?.forEach((t) => tagNames.add(t)); const { security: operationSecurity, ...operationWithoutSecurity } = operation; const securityRequirements2 = (operationSecurity ?? schema.security ?? []).map((s) => { const keys = Object.keys(s); return keys.length > 1 ? keys : keys[0]; }).filter(isDefined); const preferredSecurityNames2 = [authentication?.preferredSecurityScheme ?? []].flat().filter((name) => { if (Array.isArray(name)) { return securityRequirements2.some( (r) => Array.isArray(r) && r.length === name.length && r.every((v, i) => v === name[i]) ); } return securityRequirements2.includes(name); }); const selectedSecuritySchemeUids2 = securityRequirements2.length && !useCollectionSecurity ? getSelectedSecuritySchemeUids(securityRequirements2, preferredSecurityNames2, securitySchemeMap) : []; const requestPayload = { ...operationWithoutSecurity, method, path: pathString, security: operationSecurity, selectedServerUid: operationLevelServers?.[0]?.uid, selectedSecuritySchemeUids: selectedSecuritySchemeUids2, // Merge path and operation level parameters parameters: [...path?.parameters ?? [], ...operation.parameters ?? []], servers: [...pathServers, ...operationLevelServers].map((s) => s.uid) }; if (requestPayload.examples) { console.warn("[@scalar/api-client] operation.examples is not a valid openapi property"); delete requestPayload.examples; } const request = schemaModel(requestPayload, requestSchema, false); if (!request) { importWarnings.push(`${method} Request at ${path} is invalid.`); } else { requests.push(request); } }); }); const tags = schemaModel(schema?.tags ?? [], tagSchema.array(), false) ?? []; tags.forEach((t) => tagNames.delete(t.name)); tagNames.forEach((name) => name && tags.push(tagSchema.parse({ name }))); const tagMap = {}; tags.forEach((t) => { tagMap[t.name] = t; }); const collectionChildren = new Set(tags.map((t) => t.uid)); tags.forEach((t) => { t["x-scalar-children"]?.forEach((c) => { const nestedUid = tagMap[c.tagName]?.uid; if (nestedUid) { t.children.push(nestedUid); collectionChildren.delete(nestedUid); } }); }); requests.forEach((r) => { if (r.tags?.length) { r.tags.forEach((t) => { tagMap[t]?.children.push(r.uid); }); } else { collectionChildren.add(r.uid); } }); const examples = []; requests.forEach((request) => { const example = createExampleFromRequest(request, "Default Example"); examples.push(example); request.examples.push(example.uid); }); const securityRequirements = (schema.security ?? []).map((s) => { const keys = Object.keys(s); return keys.length > 1 ? keys : keys[0]; }).filter(isDefined); const preferredSecurityNames = [authentication?.preferredSecurityScheme ?? []].flat(); const selectedSecuritySchemeUids = (securityRequirements.length || preferredSecurityNames?.length) && useCollectionSecurity ? getSelectedSecuritySchemeUids(securityRequirements, preferredSecurityNames, securitySchemeMap) : []; const slugObj = slug?.length ? { uid: getSlugUid(slug) } : {}; const collection = collectionSchema.parse({ ...slugObj, ...schema, watchMode, documentUrl, useCollectionSecurity, requests: requests.map((r) => r.uid), servers: collectionServers.map((s) => s.uid), tags: tags.map((t) => t.uid), children: [...collectionChildren], security: schema.security ?? [{}], selectedServerUid: collectionServers?.[0]?.uid, selectedSecuritySchemeUids, components: { ...schema.components }, securitySchemes: securitySchemes.map((s) => s.uid) }); const end = performance.now(); console.log(`workspace: ${Math.round(end - start)} ms`); return { error: false, servers: [...collectionServers, ...operationServers], schema, requests, examples, collection, tags, securitySchemes }; } function getBaseUrlFromDocumentUrl(documentUrl) { try { const url = new URL(documentUrl); return `${url.protocol}//${url.hostname}${url.port ? `:${url.port}` : ""}`; } catch { return void 0; } } function getServersFromOpenApiDocument(servers, { baseServerURL, documentUrl } = {}) { if (!servers?.length && documentUrl) { const newServerUrl = getBaseUrlFromDocumentUrl(documentUrl); if (newServerUrl) { return [serverSchema.parse({ url: newServerUrl })]; } } if (!servers || !Array.isArray(servers)) { return []; } return servers.map((server) => { try { const parsedSchema = serverSchema.parse(server); if (parsedSchema?.url?.startsWith("/")) { if (baseServerURL) { parsedSchema.url = combineUrlAndPath(baseServerURL, parsedSchema.url); return parsedSchema; } if (documentUrl) { const baseUrl = getBaseUrlFromDocumentUrl(documentUrl); if (baseUrl) { parsedSchema.url = combineUrlAndPath(baseUrl, parsedSchema.url); } return parsedSchema; } const fallbackUrl = getFallbackUrl(); if (fallbackUrl) { parsedSchema.url = combineUrlAndPath(fallbackUrl, parsedSchema.url.replace(/^\//, "")); return parsedSchema; } } return parsedSchema; } catch (error) { console.warn("Oops, that's an invalid server configuration."); console.warn("Server:", server); console.warn("Error:", error); return void 0; } }).filter(isDefined); } function getFallbackUrl() { if (typeof window === "undefined") { return void 0; } if (typeof window?.location?.origin !== "string") { return void 0; } return window.location.origin; } export { getSelectedSecuritySchemeUids, getServersFromOpenApiDocument, getSlugUid, importSpecToWorkspace, parseSchema }; //# sourceMappingURL=import-spec.js.map