docs-util: add JS SDK examples to generated OAS (#11935)

This commit is contained in:
Shahed Nasser
2025-03-25 11:11:48 +02:00
committed by GitHub
parent d53af655f0
commit 8a302130b3
8 changed files with 283 additions and 412 deletions

View File

@@ -1,14 +1,9 @@
import { OpenAPIV3 } from "openapi-types"
import { OasArea } from "../kinds/oas.js"
import { CodeSample } from "../../types/index.js"
import {
capitalize,
getFakeStrValue,
kebabToCamel,
wordsToCamel,
wordsToKebab,
} from "utils"
import { API_ROUTE_PARAM_REGEX } from "../../constants.js"
import { getFakeStrValue } from "utils"
import { getRouteExamplesOutputBasePath } from "../../utils/get-output-base-paths.js"
import { RouteExamples } from "types"
import { readFileSync } from "fs"
type CodeSampleData = Omit<CodeSample, "source">
@@ -18,166 +13,44 @@ type CodeSampleData = Omit<CodeSample, "source">
class OasExamplesGenerator {
static JSCLIENT_CODESAMPLE_DATA: CodeSampleData = {
lang: "JavaScript",
label: "JS Client",
label: "JS SDK",
}
static CURL_CODESAMPLE_DATA: CodeSampleData = {
lang: "Shell",
label: "cURL",
}
static MEDUSAREACT_CODESAMPLE_DATA: CodeSampleData = {
lang: "tsx",
label: "Medusa React",
private routeExamples: RouteExamples
constructor() {
// load route examples
this.routeExamples = JSON.parse(
readFileSync(getRouteExamplesOutputBasePath(), "utf8")
)
}
/**
* Generate JS client example for an OAS operation.
*
* @param param0 - The operation's details
* @returns The JS client example.
*/
generateJSClientExample({
area,
tag,
oasPath,
httpMethod,
isAdminAuthenticated,
isStoreAuthenticated,
parameters,
requestBody,
responseBody,
generateJsSdkExanmple({
method,
path,
}: {
/**
* The area of the operation.
*/
area: OasArea
/**
* The tag this operation belongs to.
*/
tag: string
/**
* The API route's path.
*/
oasPath: string
/**
* The http method of the operation.
*/
httpMethod: string
/**
* Whether the operation requires admin authentication.
*/
isAdminAuthenticated?: boolean
/**
* Whether the operation requires customer authentication.
*/
isStoreAuthenticated?: boolean
/**
* The path parameters that can be sent in the request, if any.
*/
parameters?: OpenAPIV3.ParameterObject[]
/**
* The request body's schema, if any.
*/
requestBody?: OpenAPIV3.SchemaObject
/**
* The response body's schema, if any.
*/
responseBody?: OpenAPIV3.SchemaObject
}) {
const exampleArr = [
`import Medusa from "@medusajs/medusa-js"`,
`const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })`,
]
method: string
path: string
}): string {
const normalizedMethod = method.toUpperCase()
// Try to match route by normalizing path parameters
// path parameters may have different names, so we normalize them
// to a generic `{param}` placeholder
const normalizedPath = path.replaceAll(/\{[^}]+\}/g, "{param}")
const targetRouteKey = `${normalizedMethod} ${normalizedPath}`
const matchingRouteKey = Object.keys(this.routeExamples).find((key) => {
const normalizedKey = key.replaceAll(/\{[^}]+\}/g, "{param}")
return normalizedKey === targetRouteKey
})
if (isAdminAuthenticated) {
exampleArr.push(`// must be previously logged in or use api token`)
} else if (isStoreAuthenticated) {
exampleArr.push(`// must be previously logged in.`)
if (!matchingRouteKey || !this.routeExamples[matchingRouteKey]["js-sdk"]) {
return ""
}
// infer JS method name
// reset regex manually
API_ROUTE_PARAM_REGEX.lastIndex = 0
const isForSingleEntity = API_ROUTE_PARAM_REGEX.test(oasPath)
let jsMethod = `{methodName}`
if (isForSingleEntity) {
const splitOasPath = oasPath
.replaceAll(API_ROUTE_PARAM_REGEX, "")
.replace(/\/(batch)*$/, "")
.split("/")
const isBulk = oasPath.endsWith("/batch")
const isOperationOnDifferentEntity =
wordsToKebab(tag) !== splitOasPath[splitOasPath.length - 1]
if (isBulk || isOperationOnDifferentEntity) {
const endingEntityName = capitalize(
isBulk &&
API_ROUTE_PARAM_REGEX.test(splitOasPath[splitOasPath.length - 1])
? wordsToCamel(tag)
: kebabToCamel(splitOasPath[splitOasPath.length - 1])
)
jsMethod =
httpMethod === "get"
? `list${endingEntityName}`
: httpMethod === "post"
? `add${endingEntityName}`
: `remove${endingEntityName}`
} else {
jsMethod =
httpMethod === "get"
? "retrieve"
: httpMethod === "post"
? "update"
: "delete"
}
} else {
jsMethod =
httpMethod === "get"
? "list"
: httpMethod === "post"
? "create"
: "delete"
}
// collect the path/request parameters to be passed to the request.
const parametersArr: string[] =
parameters?.map((parameter) => parameter.name) || []
const requestData = requestBody
? this.getSchemaRequiredData(requestBody)
: {}
// assemble the method-call line of format `medusa.{admin?}.{methodName}({...parameters,} {requestBodyDataObj})`
exampleArr.push(
`medusa${area === "admin" ? `.${area}` : ""}.${wordsToCamel(
tag
)}.${jsMethod}(${parametersArr.join(", ")}${
Object.keys(requestData).length
? `${parametersArr.length ? ", " : ""}${JSON.stringify(
requestData,
undefined,
2
)}`
: ""
})`
)
// assemble then lines with response data, if any
const responseData = responseBody
? this.getSchemaRequiredData(responseBody)
: {}
const responseRequiredItems = Object.keys(responseData)
const responseRequiredItemsStr = responseRequiredItems.length
? `{ ${responseRequiredItems.join(", ")} }`
: ""
exampleArr.push(
`.then((${responseRequiredItemsStr}) => {\n\t\t${
responseRequiredItemsStr.length
? `console.log(${responseRequiredItemsStr})`
: "// Success"
}\n})`
)
return exampleArr.join("\n")
return this.routeExamples[matchingRouteKey]["js-sdk"]
}
/**

View File

@@ -4,8 +4,7 @@ import getBasePath from "../../utils/get-base-path.js"
import RouteExamplesKindGenerator from "../kinds/route-examples.js"
import ts from "typescript"
import type { RouteExamples } from "types"
import getMonorepoRoot from "../../utils/get-monorepo-root.js"
import path from "path"
import { getRouteExamplesOutputBasePath } from "../../utils/get-output-base-paths.js"
class RouteExamplesGenerator extends AbstractGenerator {
protected routeExamplesKindGenerator?: RouteExamplesKindGenerator
@@ -91,10 +90,7 @@ class RouteExamplesGenerator extends AbstractGenerator {
* @param routeExamples - The route examples to write.
*/
writeJson(routeExamples: RouteExamples) {
const filePath = path.join(
getMonorepoRoot(),
"www/utils/generated/route-examples-output/route-examples.json"
)
const filePath = getRouteExamplesOutputBasePath()
const fileContent = JSON.stringify(routeExamples, null, 2)

View File

@@ -348,45 +348,33 @@ class OasKindGenerator extends FunctionKindGenerator {
tagName,
})
// retrieve code examples
// only generate cURL examples, and for the rest
// check if the --generate-examples option is enabled
oas["x-codeSamples"] = [
{
...OasExamplesGenerator.CURL_CODESAMPLE_DATA,
source: this.oasExamplesGenerator.generateCurlExample({
method: methodName,
path: normalizedOasPath,
isAdminAuthenticated,
isStoreAuthenticated,
requestSchema,
}),
},
]
const curlExample = this.oasExamplesGenerator.generateCurlExample({
method: methodName,
path: normalizedOasPath,
isAdminAuthenticated,
isStoreAuthenticated,
requestSchema,
})
const jsSdkExample = this.oasExamplesGenerator.generateJsSdkExanmple({
method: methodName,
path: normalizedOasPath,
})
if (this.options.generateExamples) {
oas["x-codeSamples"].push(
{
...OasExamplesGenerator.JSCLIENT_CODESAMPLE_DATA,
source: this.oasExamplesGenerator.generateJSClientExample({
oasPath,
httpMethod: methodName,
area: splitOasPath[0] as OasArea,
tag: tagName || "",
isAdminAuthenticated,
isStoreAuthenticated,
parameters: (oas.parameters as OpenAPIV3.ParameterObject[])?.filter(
(parameter) => parameter.in === "path"
),
requestBody: requestSchema,
responseBody: responseSchema,
}),
},
{
...OasExamplesGenerator.MEDUSAREACT_CODESAMPLE_DATA,
source: "EXAMPLE", // TODO figure out if we can generate examples for medusa react
}
)
// retrieve code examples
oas["x-codeSamples"] = []
if (curlExample) {
oas["x-codeSamples"].push({
...OasExamplesGenerator.CURL_CODESAMPLE_DATA,
source: curlExample,
})
}
if (jsSdkExample) {
oas["x-codeSamples"].push({
...OasExamplesGenerator.JSCLIENT_CODESAMPLE_DATA,
source: jsSdkExample,
})
}
// add security details if applicable
@@ -672,44 +660,6 @@ class OasKindGenerator extends FunctionKindGenerator {
}
}
// update examples if the --generate-examples option is enabled
if (this.options.generateExamples) {
const oldJsExampleIndex = oas["x-codeSamples"]
? oas["x-codeSamples"].findIndex(
(example) =>
example.label ==
OasExamplesGenerator.JSCLIENT_CODESAMPLE_DATA.label
)
: -1
if (oldJsExampleIndex === -1) {
// only generate a new example if it doesn't have an example
const newJsExample = this.oasExamplesGenerator.generateJSClientExample({
oasPath,
httpMethod: methodName,
area: splitOasPath[0] as OasArea,
tag: tagName || "",
isAdminAuthenticated,
isStoreAuthenticated,
parameters: (oas.parameters as OpenAPIV3.ParameterObject[])?.filter(
(parameter) => parameter.in === "path"
),
requestBody: updatedRequestSchema?.schema,
responseBody: updatedResponseSchema,
})
oas["x-codeSamples"] = [
...(oas["x-codeSamples"] || []),
{
...OasExamplesGenerator.JSCLIENT_CODESAMPLE_DATA,
source: newJsExample,
},
]
}
// TODO add for Medusa React once we figure out how to generate it
}
// check if cURL example should be updated.
const oldCurlExampleIndex = oas["x-codeSamples"]
? oas["x-codeSamples"].findIndex(
@@ -743,6 +693,38 @@ class OasKindGenerator extends FunctionKindGenerator {
)
}
// generate JS SDK example
const oldJsSdkExampleIndex = oas["x-codeSamples"]
? oas["x-codeSamples"].findIndex(
(example) =>
example.label ===
OasExamplesGenerator.JSCLIENT_CODESAMPLE_DATA.label
)
: -1
const jsSdkExample = this.oasExamplesGenerator.generateJsSdkExanmple({
method: methodName,
path: normalizedOasPath,
})
if (jsSdkExample) {
if (oldJsSdkExampleIndex === -1) {
oas["x-codeSamples"] = [
...(oas["x-codeSamples"] || []),
{
...OasExamplesGenerator.JSCLIENT_CODESAMPLE_DATA,
source: jsSdkExample,
},
]
} else {
oas["x-codeSamples"]![oldJsSdkExampleIndex] = {
...OasExamplesGenerator.JSCLIENT_CODESAMPLE_DATA,
source: jsSdkExample,
}
}
} else if (oldJsSdkExampleIndex !== -1) {
// remove the JS SDK example if it doesn't exist
oas["x-codeSamples"]!.splice(oldJsSdkExampleIndex, 1)
}
// push new tags to the tags property
if (tagName) {
const areaTags = this.tags.get(splitOasPath[0] as OasArea)

View File

@@ -1,7 +1,6 @@
import ts from "typescript"
import { SyntaxKind } from "typescript"
import DefaultKindGenerator, { GetDocBlockOptions } from "./default.js"
import { API_ROUTE_PARAM_REGEX } from "../../constants.js"
import type { RouteExamples } from "types"
const EXAMPLE_CODEBLOCK_REGEX = /```(ts|typescript)\s*([.\s\S]*?)\s*```/
@@ -105,11 +104,16 @@ class RouteExamplesKindGenerator extends DefaultKindGenerator<ts.MethodDeclarati
node.kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral ||
node.kind === ts.SyntaxKind.TemplateExpression
) {
const str = node
let str = node
.getText()
.replace(/^["'`]|["'`]$/g, "")
.replace(API_ROUTE_PARAM_REGEX, `{$1}`)
.replace(/\$\{(.+?)\}/g, `{$1}`)
.toLowerCase()
// remove possible query params in string
const queryIndex = str.indexOf("?")
if (queryIndex > -1) {
str = str.slice(0, queryIndex)
}
if (
str.startsWith("/store") ||
str.startsWith("/admin") ||