@scalar/oas-utils
Version:
Open API spec and Yaml handling utilities
375 lines (374 loc) • 13.7 kB
JavaScript
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