UNPKG

oas

Version:

Comprehensive tooling for working with OpenAPI definitions

835 lines (828 loc) 30.3 kB
import { PARAMETER_ORDERING, getExtension } from "./chunk-L2OVXZK3.js"; import { isOAS31, isRef, isSchema } from "./chunk-DPTPURCR.js"; // src/lib/clone-object.ts function cloneObject(obj) { if (typeof obj === "undefined") { return void 0; } return JSON.parse(JSON.stringify(obj)); } // src/lib/helpers.ts function hasSchemaType(schema, discriminator) { if (Array.isArray(schema.type)) { return schema.type.includes(discriminator); } return schema.type === discriminator; } function isObject(val) { return typeof val === "object" && val !== null && !Array.isArray(val); } function isPrimitive(val) { return typeof val === "string" || typeof val === "number" || typeof val === "boolean"; } // src/lib/matches-mimetype.ts function matchesMediaType(types2, mediaType) { return types2.some((type) => { return mediaType.indexOf(type) > -1; }); } var matches_mimetype_default = { formUrlEncoded: (mimeType) => { return matchesMediaType(["application/x-www-form-urlencoded"], mimeType); }, json: (contentType) => { return matchesMediaType( ["application/json", "application/x-json", "text/json", "text/x-json", "+json"], contentType ); }, multipart: (contentType) => { return matchesMediaType( ["multipart/mixed", "multipart/related", "multipart/form-data", "multipart/alternative"], contentType ); }, wildcard: (contentType) => { return contentType === "*/*"; }, xml: (contentType) => { return matchesMediaType( [ "application/xml", "application/xml-external-parsed-entity", "application/xml-dtd", "text/xml", "text/xml-external-parsed-entity", "+xml" ], contentType ); } }; // src/lib/openapi-to-json-schema.ts import mergeJSONSchemaAllOf from "json-schema-merge-allof"; import jsonpointer from "jsonpointer"; import removeUndefinedObjects from "remove-undefined-objects"; var UNSUPPORTED_SCHEMA_PROPS = [ "example", // OpenAPI supports `example` but we're mapping it to `examples` in this library. "externalDocs", "xml" ]; function encodePointer(str) { return str.replace("~", "~0").replace("/", "~1"); } function getSchemaVersionString(schema, api) { if (!isOAS31(api)) { return "http://json-schema.org/draft-04/schema#"; } if (schema.$schema) { return schema.$schema; } if (api.jsonSchemaDialect) { return api.jsonSchemaDialect; } return "https://json-schema.org/draft/2020-12/schema#"; } function isPolymorphicSchema(schema) { return "allOf" in schema || "anyOf" in schema || "oneOf" in schema; } function isRequestBodySchema(schema) { return "content" in schema; } function searchForValueByPropAndPointer(property, pointer, schemas = []) { if (!schemas.length || !pointer.length) { return void 0; } const locSplit = pointer.split("/").filter(Boolean).reverse(); const pointers = []; let point = ""; for (let i = 0; i < locSplit.length; i += 1) { point = `/${locSplit[i]}${point}`; pointers.push(point); } let foundValue; const rev = [...schemas].reverse(); for (let i = 0; i < pointers.length; i += 1) { for (let ii = 0; ii < rev.length; ii += 1) { let schema = rev[ii]; if (property === "example") { if ("example" in schema) { schema = schema.example; } else { if (!Array.isArray(schema.examples) || !schema.examples.length) { continue; } schema = [...schema.examples].shift(); } } else { schema = schema.default; } try { foundValue = jsonpointer.get(schema, pointers[i]); } catch (err) { } if (foundValue !== void 0) { break; } } if (foundValue !== void 0) { break; } } return foundValue; } function toJSONSchema(data, opts = {}) { let schema = data === true ? {} : { ...data }; const schemaAdditionalProperties = isSchema(schema) ? schema.additionalProperties : null; const { addEnumsToDescriptions, currentLocation, globalDefaults, hideReadOnlyProperties, hideWriteOnlyProperties, isPolymorphicAllOfChild, prevDefaultSchemas, prevExampleSchemas, refLogger, transformer } = { addEnumsToDescriptions: false, currentLocation: "", globalDefaults: {}, hideReadOnlyProperties: false, hideWriteOnlyProperties: false, isPolymorphicAllOfChild: false, prevDefaultSchemas: [], prevExampleSchemas: [], refLogger: () => true, transformer: (s) => s, ...opts }; if (isRef(schema)) { refLogger(schema.$ref, "ref"); return transformer({ $ref: schema.$ref }); } if (isSchema(schema, isPolymorphicAllOfChild)) { if ("allOf" in schema && Array.isArray(schema.allOf)) { try { schema = mergeJSONSchemaAllOf(schema, { ignoreAdditionalProperties: true, resolvers: { // `merge-json-schema-allof` by default takes the first `description` when you're // merging an `allOf` but because generally when you're merging two schemas together // with an `allOf` you want data in the subsequent schemas to be applied to the first // and `description` should be a part of that. description: (obj) => { return obj.slice(-1)[0]; }, // `merge-json-schema-allof` doesn't support merging enum arrays but since that's a // safe and simple operation as enums always contain primitives we can handle it // ourselves with a custom resolver. enum: (obj) => { let arr = []; obj.forEach((e) => { arr = arr.concat(e); }); return arr; }, // for any unknown keywords (e.g., `example`, `format`, `x-readme-ref-name`), // we fallback to using the title resolver (which uses the first value found). // https://github.com/mokkabonna/json-schema-merge-allof/blob/ea2e48ee34415022de5a50c236eb4793a943ad11/src/index.js#L292 // https://github.com/mokkabonna/json-schema-merge-allof/blob/ea2e48ee34415022de5a50c236eb4793a943ad11/README.md?plain=1#L147 defaultResolver: mergeJSONSchemaAllOf.options.resolvers.title } }); } catch (e) { const { ...schemaWithoutAllOf } = schema; schema = schemaWithoutAllOf; delete schema.allOf; } if (isRef(schema)) { refLogger(schema.$ref, "ref"); return transformer({ $ref: schema.$ref }); } } ["anyOf", "oneOf"].forEach((polyType) => { if (polyType in schema && Array.isArray(schema[polyType])) { schema[polyType].forEach((item, idx) => { const polyOptions = { addEnumsToDescriptions, currentLocation: `${currentLocation}/${idx}`, globalDefaults, hideReadOnlyProperties, hideWriteOnlyProperties, isPolymorphicAllOfChild: false, prevDefaultSchemas, prevExampleSchemas, refLogger, transformer }; if ("properties" in schema) { schema[polyType][idx] = toJSONSchema( { required: schema.required, allOf: [item, { properties: schema.properties }] }, polyOptions ); } else if ("items" in schema) { schema[polyType][idx] = toJSONSchema( { allOf: [item, { items: schema.items }] }, polyOptions ); } else { schema[polyType][idx] = toJSONSchema(item, polyOptions); } if (isObject(schema[polyType][idx]) && "required" in schema[polyType][idx] && typeof schema[polyType][idx].required === "boolean") { delete schema[polyType][idx].required; } }); } }); if ("discriminator" in schema) { if ("mapping" in schema.discriminator && typeof schema.discriminator.mapping === "object") { const mapping = schema.discriminator.mapping; Object.keys(mapping).forEach((k) => { refLogger(mapping[k], "discriminator"); }); } } } if (!("type" in schema) && !isPolymorphicSchema(schema) && !isRequestBodySchema(schema)) { if ("properties" in schema) { schema.type = "object"; } else if ("items" in schema) { schema.type = "array"; } else { } } if ("type" in schema) { if ("nullable" in schema) { if (schema.nullable) { if (Array.isArray(schema.type)) { schema.type.push("null"); } else if (schema.type !== null && schema.type !== "null") { schema.type = [schema.type, "null"]; } } delete schema.nullable; } if (schema.type === null) { schema.type = "null"; } else if (Array.isArray(schema.type)) { if (schema.type.includes(null)) { schema.type[schema.type.indexOf(null)] = "null"; } schema.type = Array.from(new Set(schema.type)); if (schema.type.length === 1) { schema.type = schema.type.shift(); } else if (schema.type.includes("array") || schema.type.includes("boolean") || schema.type.includes("object")) { const isNullable = schema.type.includes("null"); if (schema.type.length === 2 && isNullable) { } else { const nonPrimitives = []; Object.entries({ // https://json-schema.org/understanding-json-schema/reference/array.html array: [ "additionalItems", "contains", "items", "maxContains", "maxItems", "minContains", "minItems", "prefixItems", "uniqueItems" ], // https://json-schema.org/understanding-json-schema/reference/boolean.html boolean: [ // Booleans don't have any boolean-specific properties. ], // https://json-schema.org/understanding-json-schema/reference/object.html object: [ "additionalProperties", "maxProperties", "minProperties", "nullable", "patternProperties", "properties", "propertyNames", "required" ] }).forEach(([typeKey, keywords]) => { if (!schema.type.includes(typeKey)) { return; } const reducedSchema = removeUndefinedObjects({ type: isNullable ? [typeKey, "null"] : typeKey, allowEmptyValue: schema.allowEmptyValue ?? void 0, deprecated: schema.deprecated ?? void 0, description: schema.description ?? void 0, readOnly: schema.readOnly ?? void 0, title: schema.title ?? void 0, writeOnly: schema.writeOnly ?? void 0 }); keywords.forEach((t) => { if (t in schema) { reducedSchema[t] = schema[t]; delete schema[t]; } }); nonPrimitives.push(reducedSchema); }); schema.type = schema.type.filter((t) => t !== "array" && t !== "boolean" && t !== "object"); if (schema.type.length === 1) { schema.type = schema.type.shift(); } if (schema.type.length > 1) { schema = { oneOf: [schema, ...nonPrimitives] }; } else { schema = { oneOf: nonPrimitives }; } } } } } if (isSchema(schema, isPolymorphicAllOfChild)) { if ("default" in schema && isObject(schema.default)) { prevDefaultSchemas.push({ default: schema.default }); } if ("example" in schema) { if (isPrimitive(schema.example)) { schema.examples = [schema.example]; } else if (Array.isArray(schema.example)) { schema.examples = schema.example.filter((example) => isPrimitive(example)); if (!schema.examples.length) { delete schema.examples; } } else { prevExampleSchemas.push({ example: schema.example }); } delete schema.example; } else if ("examples" in schema) { let reshapedExamples = false; if (typeof schema.examples === "object" && !Array.isArray(schema.examples)) { const examples = []; Object.keys(schema.examples).forEach((name) => { const example = schema.examples[name]; if ("$ref" in example) { refLogger(example.$ref, "ref"); } else if ("value" in example) { if (isPrimitive(example.value)) { examples.push(example.value); reshapedExamples = true; } else if (Array.isArray(example.value) && isPrimitive(example.value[0])) { examples.push(example.value[0]); reshapedExamples = true; } else { prevExampleSchemas.push({ example: example.value }); } } }); if (examples.length) { reshapedExamples = true; schema.examples = examples; } } else if (Array.isArray(schema.examples) && isPrimitive(schema.examples[0])) { reshapedExamples = true; } if (!reshapedExamples) { delete schema.examples; } } if (!hasSchemaType(schema, "array") && !hasSchemaType(schema, "object") && !schema.examples) { const foundExample = searchForValueByPropAndPointer("example", currentLocation, prevExampleSchemas); if (foundExample) { if (isPrimitive(foundExample) || Array.isArray(foundExample) && isPrimitive(foundExample[0])) { schema.examples = [foundExample]; } } } if (hasSchemaType(schema, "array")) { if ("items" in schema) { if (!Array.isArray(schema.items) && Object.keys(schema.items).length === 1 && isRef(schema.items)) { refLogger(schema.items.$ref, "ref"); } else if (schema.items !== true) { schema.items = toJSONSchema(schema.items, { addEnumsToDescriptions, currentLocation: `${currentLocation}/0`, globalDefaults, hideReadOnlyProperties, hideWriteOnlyProperties, prevExampleSchemas, refLogger, transformer }); if (isObject(schema.items) && "required" in schema.items && !Array.isArray(schema.items.required)) { delete schema.items.required; } } } else if ("properties" in schema || "additionalProperties" in schema) { schema.type = "object"; } else { schema.items = {}; } } else if (hasSchemaType(schema, "object")) { if ("properties" in schema) { Object.keys(schema.properties).forEach((prop) => { if (Array.isArray(schema.properties[prop]) || typeof schema.properties[prop] === "object" && schema.properties[prop] !== null) { const newPropSchema = toJSONSchema(schema.properties[prop], { addEnumsToDescriptions, currentLocation: `${currentLocation}/${encodePointer(prop)}`, globalDefaults, hideReadOnlyProperties, hideWriteOnlyProperties, prevDefaultSchemas, prevExampleSchemas, refLogger, transformer }); let propShouldBeUpdated = true; if ((hideReadOnlyProperties || hideWriteOnlyProperties) && !Object.keys(newPropSchema).length) { if (Object.keys(schema.properties[prop]).length > 0) { delete schema.properties[prop]; propShouldBeUpdated = false; } } if (propShouldBeUpdated) { schema.properties[prop] = newPropSchema; if (isObject(newPropSchema) && "required" in newPropSchema && typeof newPropSchema.required === "boolean" && newPropSchema.required === true) { if ("required" in schema && Array.isArray(schema.required)) { schema.required.push(prop); } else { schema.required = [prop]; } delete schema.properties[prop].required; } } } }); if (hideReadOnlyProperties || hideWriteOnlyProperties) { if (!Object.keys(schema.properties).length) { return transformer({}); } } } if (typeof schemaAdditionalProperties === "object" && schemaAdditionalProperties !== null) { if (!("type" in schemaAdditionalProperties) && !("$ref" in schemaAdditionalProperties) && // We know it will be a schema object because it's dereferenced !isPolymorphicSchema(schemaAdditionalProperties)) { schema.additionalProperties = true; } else { schema.additionalProperties = toJSONSchema(schemaAdditionalProperties, { addEnumsToDescriptions, currentLocation, globalDefaults, hideReadOnlyProperties, hideWriteOnlyProperties, prevDefaultSchemas, prevExampleSchemas, refLogger, transformer }); } } if (!isPolymorphicSchema(schema) && !("properties" in schema) && !("additionalProperties" in schema)) { schema.additionalProperties = true; } } } if (isSchema(schema, isPolymorphicAllOfChild) && globalDefaults && Object.keys(globalDefaults).length > 0 && currentLocation) { try { const userJwtDefault = jsonpointer.get(globalDefaults, currentLocation); if (userJwtDefault) { schema.default = userJwtDefault; } } catch (err) { } } if ("default" in schema && typeof schema.default !== "undefined") { if (hasSchemaType(schema, "object")) { delete schema.default; } else if ("allowEmptyValue" in schema && schema.allowEmptyValue && schema.default === "" || schema.default !== "") { } else { delete schema.default; } } else if (prevDefaultSchemas.length) { const foundDefault = searchForValueByPropAndPointer("default", currentLocation, prevDefaultSchemas); if (isPrimitive(foundDefault) || foundDefault === null || Array.isArray(foundDefault) && hasSchemaType(schema, "array")) { schema.default = foundDefault; } } if (isSchema(schema, isPolymorphicAllOfChild) && "enum" in schema && Array.isArray(schema.enum)) { schema.enum = Array.from(new Set(schema.enum)); if (addEnumsToDescriptions) { const enums = schema.enum.filter(Boolean).map((str) => `\`${str}\``).join(" "); if (enums.length) { if ("description" in schema) { schema.description += ` ${enums}`; } else { schema.description = enums; } } } } if ("anyOf" in schema || "oneOf" in schema) { if ("properties" in schema) { delete schema.properties; } if ("items" in schema) { delete schema.items; } } for (let i = 0; i < UNSUPPORTED_SCHEMA_PROPS.length; i += 1) { delete schema[UNSUPPORTED_SCHEMA_PROPS[i]]; } if (hideReadOnlyProperties && "readOnly" in schema && schema.readOnly === true) { return {}; } else if (hideWriteOnlyProperties && "writeOnly" in schema && schema.writeOnly === true) { return {}; } return transformer(schema); } // src/operation/lib/get-parameters-as-json-schema.ts var types = { path: "Path Params", query: "Query Params", body: "Body Params", cookie: "Cookie Params", formData: "Form Data", header: "Headers", metadata: "Metadata" // This a special type reserved for https://npm.im/api }; function getParametersAsJSONSchema(operation, api, opts) { let hasCircularRefs = false; let hasDiscriminatorMappingRefs = false; function refLogger(ref, type) { if (type === "ref") { hasCircularRefs = true; } else { hasDiscriminatorMappingRefs = true; } } function getDeprecated(schema, type) { if (opts.retainDeprecatedProperties) { return null; } if (!schema || !schema.properties) return null; const deprecatedBody = cloneObject(schema); const requiredParams = schema.required || []; const allDeprecatedProps = {}; Object.keys(deprecatedBody.properties).forEach((key) => { const deprecatedProp = deprecatedBody.properties[key]; if (deprecatedProp.deprecated && !requiredParams.includes(key) && !deprecatedProp.readOnly) { allDeprecatedProps[key] = deprecatedProp; } }); deprecatedBody.properties = allDeprecatedProps; const deprecatedSchema = toJSONSchema(deprecatedBody, { globalDefaults: opts.globalDefaults, hideReadOnlyProperties: opts.hideReadOnlyProperties, hideWriteOnlyProperties: opts.hideWriteOnlyProperties, prevExampleSchemas: [], refLogger, transformer: opts.transformer }); if (Object.keys(deprecatedSchema).length === 0 || Object.keys(deprecatedSchema.properties).length === 0) { return null; } Object.keys(schema.properties).forEach((key) => { if (schema.properties[key].deprecated && !requiredParams.includes(key)) { delete schema.properties[key]; } }); return { type, schema: isPrimitive(deprecatedSchema) ? deprecatedSchema : { ...deprecatedSchema, $schema: getSchemaVersionString(deprecatedSchema, api) } }; } function transformRequestBody() { const requestBody = operation.getRequestBody(); if (!requestBody || !Array.isArray(requestBody)) return null; const [mediaType, mediaTypeObject, description] = requestBody; const type = mediaType === "application/x-www-form-urlencoded" ? "formData" : "body"; if (!mediaTypeObject.schema || !Object.keys(mediaTypeObject.schema).length) { return null; } const prevExampleSchemas = []; if ("example" in mediaTypeObject) { prevExampleSchemas.push({ example: mediaTypeObject.example }); } else if ("examples" in mediaTypeObject) { prevExampleSchemas.push({ examples: Object.values(mediaTypeObject.examples).map((example) => example.value).filter((val) => val !== void 0) }); } const requestSchema = cloneObject(mediaTypeObject.schema); const cleanedSchema = toJSONSchema(requestSchema, { globalDefaults: opts.globalDefaults, hideReadOnlyProperties: opts.hideReadOnlyProperties, hideWriteOnlyProperties: opts.hideWriteOnlyProperties, prevExampleSchemas, refLogger, transformer: opts.transformer }); if (!Object.keys(cleanedSchema).length) { return null; } return { type, label: types[type], schema: isPrimitive(cleanedSchema) ? cleanedSchema : { ...cleanedSchema, $schema: getSchemaVersionString(cleanedSchema, api) }, deprecatedProps: getDeprecated(cleanedSchema, type), ...description ? { description } : {} }; } function transformComponents() { if (!("components" in api)) { return false; } const components2 = { ...Object.keys(api.components).map((componentType) => ({ [componentType]: {} })).reduce((prev, next) => Object.assign(prev, next), {}) }; Object.keys(api.components).forEach((componentType) => { if (typeof api.components[componentType] === "object" && !Array.isArray(api.components[componentType])) { Object.keys(api.components[componentType]).forEach((schemaName) => { const componentSchema = cloneObject(api.components[componentType][schemaName]); components2[componentType][schemaName] = toJSONSchema(componentSchema, { globalDefaults: opts.globalDefaults, hideReadOnlyProperties: opts.hideReadOnlyProperties, hideWriteOnlyProperties: opts.hideWriteOnlyProperties, refLogger, transformer: opts.transformer }); }); } }); Object.keys(components2).forEach((componentType) => { if (!Object.keys(components2[componentType]).length) { delete components2[componentType]; } }); return components2; } function transformParameters() { const operationParams = operation.getParameters(); const transformed = Object.keys(types).map((type) => { const required = []; const parameters = operationParams.filter((param) => param.in === type); if (parameters.length === 0) { return null; } const properties = parameters.reduce((prev, current) => { let schema2 = {}; if ("schema" in current) { const currentSchema = current.schema ? cloneObject(current.schema) : {}; if (current.example) { currentSchema.example = current.example; } else if (current.examples) { currentSchema.examples = current.examples; } if (current.deprecated) currentSchema.deprecated = current.deprecated; const interimSchema = toJSONSchema(currentSchema, { currentLocation: `/${current.name}`, globalDefaults: opts.globalDefaults, hideReadOnlyProperties: opts.hideReadOnlyProperties, hideWriteOnlyProperties: opts.hideWriteOnlyProperties, refLogger, transformer: opts.transformer }); schema2 = isPrimitive(interimSchema) ? interimSchema : { ...interimSchema, // Note: this applies a `$schema` version to each field in the larger schema // object. It's not really **correct** but it's what we have to do because // there's a chance that the end user has indicated the schemas are different. $schema: getSchemaVersionString(currentSchema, api) }; } else if ("content" in current && typeof current.content === "object") { const contentKeys = Object.keys(current.content); if (contentKeys.length) { let contentType; if (contentKeys.length === 1) { contentType = contentKeys[0]; } else { const jsonLikeContentTypes = contentKeys.filter((k) => matches_mimetype_default.json(k)); if (jsonLikeContentTypes.length) { contentType = jsonLikeContentTypes[0]; } else { contentType = contentKeys[0]; } } if (typeof current.content[contentType] === "object" && "schema" in current.content[contentType]) { const currentSchema = current.content[contentType].schema ? cloneObject(current.content[contentType].schema) : {}; if (current.example) { currentSchema.example = current.example; } else if (current.examples) { currentSchema.examples = current.examples; } if (current.deprecated) currentSchema.deprecated = current.deprecated; const interimSchema = toJSONSchema(currentSchema, { currentLocation: `/${current.name}`, globalDefaults: opts.globalDefaults, hideReadOnlyProperties: opts.hideReadOnlyProperties, hideWriteOnlyProperties: opts.hideWriteOnlyProperties, refLogger, transformer: opts.transformer }); schema2 = isPrimitive(interimSchema) ? interimSchema : { ...interimSchema, // Note: this applies a `$schema` version to each field in the larger schema // object. It's not really **correct** but it's what we have to do because // there's a chance that the end user has indicated the schemas are different. $schema: getSchemaVersionString(currentSchema, api) }; } } } if (current.description) { if (!isPrimitive(schema2)) { schema2.description = current.description; } } prev[current.name] = schema2; if (current.required) { required.push(current.name); } return prev; }, {}); const schema = { type: "object", properties, required }; return { type, label: types[type], schema, deprecatedProps: getDeprecated(schema, type) }; }).filter(Boolean); if (!opts.mergeIntoBodyAndMetadata) { return transformed; } else if (!transformed.length) { return []; } const deprecatedProps = transformed.map((r) => r.deprecatedProps?.schema || null).filter(Boolean); return [ { type: "metadata", label: types.metadata, schema: { allOf: transformed.map((r) => r.schema) }, deprecatedProps: deprecatedProps.length ? { type: "metadata", schema: { allOf: deprecatedProps } } : null } ]; } if (!operation.hasParameters() && !operation.hasRequestBody()) { return null; } const typeKeys = getExtension(PARAMETER_ORDERING, api, operation).map((k) => k.toLowerCase()); typeKeys[typeKeys.indexOf("form")] = "formData"; typeKeys.push("metadata"); const jsonSchema = [transformRequestBody()].concat(...transformParameters()).filter(Boolean); const shouldIncludeComponents = hasCircularRefs || hasDiscriminatorMappingRefs && opts.includeDiscriminatorMappingRefs; const components = shouldIncludeComponents ? transformComponents() : false; return jsonSchema.map((group) => { if (components && shouldIncludeComponents) { group.schema.components = components; } if (!group.deprecatedProps) delete group.deprecatedProps; return group; }).sort((a, b) => { return typeKeys.indexOf(a.type) - typeKeys.indexOf(b.type); }); } export { isObject, isPrimitive, matches_mimetype_default, cloneObject, getSchemaVersionString, toJSONSchema, types, getParametersAsJSONSchema }; //# sourceMappingURL=chunk-5KFARTQ3.js.map