feat(OAS): sanitize circular reference for Redocly (#3198)

### What

Add OAS build step to patch known circular references that prevent Redocly from rendering the API documentation.

### Why

We've encountered crashing and loading issues with Redocly when the OAS contained circular references. We have been working around the limitation by omitting some known problematic $ref in our source OAS. We wish to move away from this strategy in order to always explicitly include $ref in our OAS.

### How

We are introducing a custom Redocly CLI plugin that will replace `$ref` by `type: object` base on a configurable set of instructions. These instructions can be modified in `docs-util/redocly/config.yaml`

We are adding a `redocly bundle` step in the current OAS build process in order to sanitize problematic circular references.

We updated the redocly-cli package version in order to ensure that plugins are supported.

### Test

We will use [Cart.payment](https://github.com/medusajs/medusa/blob/fd5c18515931aca7cb5d29530f5dd03ebaf7e07e/packages/medusa/src/models/cart.ts#L72-L74) to ensure that the new process is properly sanitizing.

* Run `yarn openapi:generate`
* Open `docs/api/store/components/schemas/Cart.yaml`
  * Expect the `payment` property to have been sanitized to `type: object`
* Run `yarn redocly preview-docs docs/api/store/openapi.yaml --config=docs-util/redocly/config.yaml`
* Visit http://127.0.0.1:8080/#tag/Cart/operation/GetCartsCart
  * In the response, expect cart.payment to not list the properties of the Payment schema.
This commit is contained in:
Patrick
2023-02-08 13:01:03 +00:00
committed by GitHub
parent b9bda3bf4e
commit 53532df8d5
7 changed files with 511 additions and 32 deletions
+97
View File
@@ -0,0 +1,97 @@
plugins:
- "./plugins/plugin.js"
# Allows to replace a $ref with `type: object` in order to avoid infinite loops
# when Redocly attempts to render circular references.
decorators:
plugin/circular-patch:
verbose: true
schemas:
Address:
- Customer
Cart:
- Customer
- Order
- Payment
ClaimItem:
- ClaimOrder
ClaimOrder:
- Fulfillment
- Order
- Return
Customer:
- Order
DraftOrder:
- Cart
- Order
Fulfillment:
- ClaimOrder
- Order
- Swap
GiftCard:
- Order
GiftCardTransaction:
- GiftCard
- Order
LineItem:
- Cart
- ClaimOrder
- Order
- OrderEdit
- Swap
Order:
- Cart
- ClaimOrder
- Customer
- DraftOrder
- Fulfillment
- OrderEdit
- Payment
- Refund
- Return
- Swap
OrderEdit:
- Order
Payment:
- Cart
- Order
- Swap
Refund:
- Order
- Payment
Return:
- ClaimOrder
- Order
- Swap
ShippingMethod:
- Cart
- ClaimOrder
- Order
- Payment
- Return
- Swap
Swap:
- Cart
- Fulfillment
- Order
- Payment
- Return
# Similar config to /www/docs/docusaurus.config.js > redocusaurus
# Allows to emulate rendering of API public documentation when using `yarn redocly preview-docs openapi.yaml`
theme.openapi:
theme:
colors:
primary:
dart: "#242526"
sidebar:
width: "250px"
disableSearch: true
expandResponses: "200,204"
generatedPayloadSamplesMaxDepth: 4
hideDownloadButton: true
hideRequestPayloadSample: true
nativeScrollbars: true
requiredPropsFirst: true
showObjectSchemaExamples: true
sortTagsAlphabetically: true
@@ -0,0 +1,87 @@
module.exports = CircularPatch
/**
* Since ref() is triggered upon reaching a $ref OAS line (leaf),
* we want to inverse `schemas` instructions in order to optimize iterators.
*/
function preparePatches(schemas) {
const patches = []
const patchesObj = {}
for (const schemaToPatch in schemas) {
for (const schemaName of schemas[schemaToPatch]) {
if (!patchesObj[schemaName]) {
patchesObj[schemaName] = {
schemaName,
schemaPointer: `#/components/schemas/${schemaName}`,
schemaToPatchPointers: [],
}
}
const schemaToPatchPointer = `#/components/schemas/${schemaToPatch}`
if (
!patchesObj[schemaName].schemaToPatchPointers.includes(
schemaToPatchPointer
)
) {
patchesObj[schemaName].schemaToPatchPointers.push(schemaToPatchPointer)
}
}
}
for (const key in patchesObj) {
patches.push(patchesObj[key])
}
return patches
}
function applyPatch(node, schemaName) {
delete node["$ref"]
node.type = "object"
if (!node.description && schemaName) {
node.description = `${schemaName} object.`
}
}
function CircularPatch({ schemas = {}, verbose = false }) {
const logs = []
const patches = preparePatches(schemas)
const refPathPrefix = "#/components/schemas/"
const refPathPrefixLength = refPathPrefix.length
const refPathPrefixRegex = /#\/components\/schemas\/\w+/
return {
ref(ref, ctx, resolved) {
if (
ctx.type.name.toLowerCase() !== "schema" ||
!resolved?.location?.pointer
) {
return
}
for (const patch of patches) {
if (resolved.location.pointer !== patch.schemaPointer) {
continue
}
const ctxSchemaPointer =
ctx.location.pointer.match(refPathPrefixRegex)[0]
if (!patch.schemaToPatchPointers.includes(ctxSchemaPointer)) {
continue
}
applyPatch(ref)
if (verbose) {
logs.push(
`${ctxSchemaPointer.substring(refPathPrefixLength)} patch $ref to ${
patch.schemaName
}`
)
}
}
},
Root: {
leave() {
if (verbose) {
logs.sort()
for (const log of logs) {
console.debug(log)
}
}
},
},
}
}
+14
View File
@@ -0,0 +1,14 @@
const CircularPatch = require("./decorators/circular-patch")
const id = "plugin"
const decorators = {
oas3: {
"circular-patch": CircularPatch,
},
}
module.exports = {
id,
decorators,
}