docs-util: add JS SDK examples to generated OAS (#11935)
This commit is contained in:
@@ -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"]
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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") ||
|
||||
|
||||
Reference in New Issue
Block a user