fix(medusa-oas-cli): fix tool not working in Medusa backends (#6812)
* fix(medusa-oas-cli): fix tool not working in custom projects * fix changeset message --------- Co-authored-by: Riqwan Thamir <rmthamir@gmail.com>
This commit is contained in:
7
.changeset/serious-panthers-allow.md
Normal file
7
.changeset/serious-panthers-allow.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@medusajs/client-types": patch
|
||||
"@medusajs/medusa-oas-cli": patch
|
||||
"@medusajs/oas-github-ci": patch
|
||||
---
|
||||
|
||||
fix(medusa-oas-cli): fix tool not working in Medusa backends
|
||||
@@ -16,7 +16,7 @@ async function run() {
|
||||
}
|
||||
|
||||
const generateOASSources = async (outDir: string) => {
|
||||
const params = ["oas", `--out-dir=${outDir}`, "--type=combined"]
|
||||
const params = ["oas", `--out-dir=${outDir}`, "--type=combined", "--local"]
|
||||
const { all: logs } = await execa("medusa-oas", params, {
|
||||
cwd: basePath,
|
||||
all: true,
|
||||
|
||||
@@ -8,9 +8,6 @@ import { readYaml } from "../utils/yaml-utils"
|
||||
import { readJson } from "../utils/json-utils"
|
||||
import execa from "execa"
|
||||
|
||||
const medusaPackagePath = path.dirname(
|
||||
require.resolve("@medusajs/medusa/package.json")
|
||||
)
|
||||
/**
|
||||
* OAS output directory
|
||||
*
|
||||
@@ -77,7 +74,7 @@ describe("command oas", () => {
|
||||
*/
|
||||
beforeAll(async () => {
|
||||
const outDir = path.resolve(tmpDir, uid())
|
||||
await runCLI("oas", ["--type", "admin", "--out-dir", outDir])
|
||||
await runCLI("oas", ["--type", "admin", "--out-dir", outDir, "--local"])
|
||||
const generatedFilePath = path.resolve(outDir, "admin.oas.json")
|
||||
oas = (await readJson(generatedFilePath)) as OpenAPIObject
|
||||
})
|
||||
@@ -104,7 +101,7 @@ describe("command oas", () => {
|
||||
|
||||
beforeAll(async () => {
|
||||
const outDir = path.resolve(tmpDir, uid())
|
||||
await runCLI("oas", ["--type", "store", "--out-dir", outDir])
|
||||
await runCLI("oas", ["--type", "store", "--out-dir", outDir, "--local"])
|
||||
const generatedFilePath = path.resolve(outDir, "store.oas.json")
|
||||
oas = (await readJson(generatedFilePath)) as OpenAPIObject
|
||||
})
|
||||
@@ -131,7 +128,7 @@ describe("command oas", () => {
|
||||
|
||||
beforeAll(async () => {
|
||||
const outDir = path.resolve(tmpDir, uid())
|
||||
await runCLI("oas", ["--type", "combined", "--out-dir", outDir])
|
||||
await runCLI("oas", ["--type", "combined", "--out-dir", outDir, "--local"])
|
||||
const generatedFilePath = path.resolve(outDir, "combined.oas.json")
|
||||
oas = (await readJson(generatedFilePath)) as OpenAPIObject
|
||||
})
|
||||
@@ -230,6 +227,7 @@ describe("command oas", () => {
|
||||
outDir,
|
||||
"--paths",
|
||||
additionalPath,
|
||||
"--local"
|
||||
])
|
||||
const generatedFilePath = path.resolve(outDir, "store.oas.json")
|
||||
oas = (await readJson(generatedFilePath)) as OpenAPIObject
|
||||
@@ -365,6 +363,7 @@ components:
|
||||
outDir,
|
||||
"--base",
|
||||
filePath,
|
||||
"--local"
|
||||
])
|
||||
const generatedFilePath = path.resolve(outDir, "store.oas.json")
|
||||
oas = (await readJson(generatedFilePath)) as OpenAPIObject
|
||||
@@ -455,4 +454,216 @@ components:
|
||||
).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe("public OAS", () => {
|
||||
let oas: OpenAPIObject
|
||||
/**
|
||||
* In a CI context, beforeAll might exceed the configured jest timeout.
|
||||
* Until we upgrade our jest version, the timeout error will be swallowed
|
||||
* and the test will fail in unexpected ways.
|
||||
*/
|
||||
beforeAll(async () => {
|
||||
const outDir = path.resolve(tmpDir, uid())
|
||||
await runCLI("oas", ["--type", "admin", "--out-dir", outDir])
|
||||
const generatedFilePath = path.resolve(outDir, "admin.oas.json")
|
||||
oas = (await readJson(generatedFilePath)) as OpenAPIObject
|
||||
})
|
||||
|
||||
it("generates oas with admin routes only", async () => {
|
||||
const routes = Object.keys(oas.paths)
|
||||
expect(routes.includes("/admin/products")).toBeTruthy()
|
||||
expect(routes.includes("/store/products")).toBeFalsy()
|
||||
})
|
||||
})
|
||||
|
||||
describe("public OAS with base", () => {
|
||||
let oas: OpenAPIObject
|
||||
beforeAll(async () => {
|
||||
const fileContent = `
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
version: 1.0.1
|
||||
title: Custom API
|
||||
servers:
|
||||
- url: https://foobar.com
|
||||
security:
|
||||
- api_key: []
|
||||
externalDocs:
|
||||
url: https://docs.com
|
||||
webhooks:
|
||||
"foo-hook":
|
||||
get:
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
tags:
|
||||
- name: Products
|
||||
description: Overwritten tag
|
||||
- name: FoobarTag
|
||||
description: Foobar tag description
|
||||
paths:
|
||||
"/foobar/tests":
|
||||
get:
|
||||
operationId: GetFoobarTests
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
"/store/regions":
|
||||
get:
|
||||
operationId: OverwrittenOperation
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
components:
|
||||
schemas:
|
||||
FoobarTestSchema:
|
||||
type: object
|
||||
properties:
|
||||
foo:
|
||||
type: string
|
||||
StoreRegionsListRes:
|
||||
type: object
|
||||
properties:
|
||||
foo:
|
||||
type: string
|
||||
callbacks:
|
||||
fooCallback:
|
||||
get:
|
||||
description: foo callback
|
||||
examples:
|
||||
fooExample:
|
||||
description: foo example
|
||||
headers:
|
||||
fooHeader:
|
||||
description: foo header
|
||||
links:
|
||||
fooLink:
|
||||
description: foo link
|
||||
operationRef: GetFoobarTests
|
||||
parameters:
|
||||
fooParameter:
|
||||
description: foo parameter
|
||||
name: foobar
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBodies:
|
||||
fooRequestBody:
|
||||
description: foo requestBody
|
||||
content:
|
||||
"application/octet-stream": { }
|
||||
responses:
|
||||
fooResponse:
|
||||
description: foo response
|
||||
securitySchemes:
|
||||
fooSecurity:
|
||||
description: foo security
|
||||
type: apiKey
|
||||
name: foo-api-key
|
||||
in: header
|
||||
`
|
||||
const targetDir = path.resolve(tmpDir, uid())
|
||||
const filePath = path.resolve(targetDir, "custom.oas.base.yaml")
|
||||
await fs.mkdir(targetDir, { recursive: true })
|
||||
await fs.writeFile(filePath, fileContent, "utf8")
|
||||
|
||||
const outDir = path.resolve(tmpDir, uid())
|
||||
await runCLI("oas", [
|
||||
"--type",
|
||||
"store",
|
||||
"--out-dir",
|
||||
outDir,
|
||||
"--base",
|
||||
filePath,
|
||||
])
|
||||
const generatedFilePath = path.resolve(outDir, "store.oas.json")
|
||||
oas = (await readJson(generatedFilePath)) as OpenAPIObject
|
||||
})
|
||||
|
||||
it("should add new path to existing paths", async () => {
|
||||
const routes = Object.keys(oas.paths)
|
||||
expect(routes.includes("/store/products")).toBeTruthy()
|
||||
expect(routes.includes("/foobar/tests")).toBeTruthy()
|
||||
})
|
||||
|
||||
it("should overwrite existing path", async () => {
|
||||
expect(oas.paths["/store/regions"]["get"].operationId).toBe(
|
||||
"OverwrittenOperation"
|
||||
)
|
||||
})
|
||||
|
||||
it("should add new schema to existing schemas", async () => {
|
||||
const schemas = Object.keys(oas.components?.schemas ?? {})
|
||||
expect(schemas.includes("StoreProductsListRes")).toBeTruthy()
|
||||
expect(schemas.includes("FoobarTestSchema")).toBeTruthy()
|
||||
})
|
||||
|
||||
it("should overwrite existing schema", async () => {
|
||||
const schema = oas.components?.schemas?.StoreRegionsListRes as
|
||||
| SchemaObject
|
||||
| undefined
|
||||
expect(schema?.properties?.foo).toBeDefined()
|
||||
})
|
||||
|
||||
it("should replace base properties", async () => {
|
||||
expect(oas.openapi).toBe("3.1.0")
|
||||
expect(oas.info).toEqual({ version: "1.0.1", title: "Custom API" })
|
||||
expect(oas.servers).toEqual([{ url: "https://foobar.com" }])
|
||||
expect(oas.security).toEqual([{ api_key: [] }])
|
||||
expect(oas.externalDocs).toEqual({ url: "https://docs.com" })
|
||||
expect(oas.webhooks).toEqual({
|
||||
"foo-hook": { get: { responses: { "200": { description: "OK" } } } },
|
||||
})
|
||||
})
|
||||
|
||||
it("should add new tag", async () => {
|
||||
expect(oas.tags).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: "FoobarTag",
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it("should overwrite existing tag", async () => {
|
||||
expect(oas.tags).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: "Products",
|
||||
description: "Overwritten tag",
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it("should add new components", async () => {
|
||||
const components = oas.components ?? {}
|
||||
expect(
|
||||
Object.keys(components.callbacks ?? {}).includes("fooCallback")
|
||||
).toBeTruthy()
|
||||
expect(
|
||||
Object.keys(components.examples ?? {}).includes("fooExample")
|
||||
).toBeTruthy()
|
||||
expect(
|
||||
Object.keys(components.headers ?? {}).includes("fooHeader")
|
||||
).toBeTruthy()
|
||||
expect(
|
||||
Object.keys(components.links ?? {}).includes("fooLink")
|
||||
).toBeTruthy()
|
||||
expect(
|
||||
Object.keys(components.parameters ?? {}).includes("fooParameter")
|
||||
).toBeTruthy()
|
||||
expect(
|
||||
Object.keys(components.requestBodies ?? {}).includes("fooRequestBody")
|
||||
).toBeTruthy()
|
||||
expect(
|
||||
Object.keys(components.responses ?? {}).includes("fooResponse")
|
||||
).toBeTruthy()
|
||||
expect(
|
||||
Object.keys(components.securitySchemes ?? {}).includes("fooSecurity")
|
||||
).toBeTruthy()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -26,16 +26,6 @@ const medusaTypesPath = path.dirname(
|
||||
const medusaUtilsPath = path.dirname(
|
||||
require.resolve("@medusajs/utils/package.json")
|
||||
)
|
||||
/**
|
||||
* OAS output directory
|
||||
*
|
||||
* @privateRemark
|
||||
* This should be the only directory OAS is loaded from for Medusa V2.
|
||||
* For now, we only use it if the --v2 flag it passed to the CLI tool.
|
||||
*/
|
||||
const oasOutputPath = path.resolve(
|
||||
__dirname, "..", "..", "..", "..", "docs-util", "oas-output"
|
||||
)
|
||||
const basePath = path.resolve(__dirname, "../")
|
||||
|
||||
/**
|
||||
@@ -66,6 +56,10 @@ export const commandOptions: Option[] = [
|
||||
new Option(
|
||||
"--v2",
|
||||
"Generate OAS files for V2 endpoints. This loads OAS from docs-util/oas-output/operations directory"
|
||||
),
|
||||
new Option(
|
||||
"--local",
|
||||
"Generate OAS from local files rather than public OAS. This is useful for generating references in the Medusa monorepo."
|
||||
)
|
||||
]
|
||||
|
||||
@@ -90,6 +84,7 @@ export async function execute(cliParams: OptionValues) {
|
||||
const dryRun = !!cliParams.dryRun
|
||||
const force = !!cliParams.force
|
||||
const v2 = !!cliParams.v2
|
||||
const local = !!cliParams.local
|
||||
|
||||
const apiType: ApiType = cliParams.type
|
||||
|
||||
@@ -122,11 +117,11 @@ export async function execute(cliParams: OptionValues) {
|
||||
console.log(`🟣 Generating OAS - ${apiType}`)
|
||||
|
||||
if (apiType === "combined") {
|
||||
const adminOAS = await getOASFromCodebase("admin", undefined, v2)
|
||||
const storeOAS = await getOASFromCodebase("store", undefined, v2)
|
||||
const adminOAS = !local ? await getPublicOas("admin", v2) : await getOASFromCodebase("admin", v2)
|
||||
const storeOAS = !local ? await getPublicOas("store", v2) : await getOASFromCodebase("store", v2)
|
||||
oas = await combineOAS(adminOAS, storeOAS)
|
||||
} else {
|
||||
oas = await getOASFromCodebase(apiType, undefined, v2)
|
||||
oas = !local ? await getPublicOas(apiType, v2) : await getOASFromCodebase(apiType, v2)
|
||||
}
|
||||
|
||||
if (additionalPaths.length || baseFile) {
|
||||
@@ -152,9 +147,18 @@ export async function execute(cliParams: OptionValues) {
|
||||
*/
|
||||
async function getOASFromCodebase(
|
||||
apiType: ApiType,
|
||||
customBaseFile?: string,
|
||||
v2?: boolean
|
||||
): Promise<OpenAPIObject> {
|
||||
/**
|
||||
* OAS output directory
|
||||
*
|
||||
* @privateRemark
|
||||
* This should be the only directory OAS is loaded from for Medusa V2.
|
||||
* For now, we only use it if the --v2 flag it passed to the CLI tool.
|
||||
*/
|
||||
const oasOutputPath = path.resolve(
|
||||
__dirname, "..", "..", "..", "..", "docs-util", "oas-output"
|
||||
)
|
||||
const gen = await swaggerInline(
|
||||
v2 ? [
|
||||
path.resolve(oasOutputPath, "operations", apiType),
|
||||
@@ -171,15 +175,21 @@ async function getOASFromCodebase(
|
||||
path.resolve(medusaPackagePath, "dist", `api/routes/${apiType}`),
|
||||
],
|
||||
{
|
||||
base:
|
||||
customBaseFile ??
|
||||
path.resolve(oasOutputPath, v2 ? "base-v2" : "base", `${apiType}.oas.base.yaml`),
|
||||
base: path.resolve(oasOutputPath, v2 ? "base-v2" : "base", `${apiType}.oas.base.yaml`),
|
||||
format: ".json",
|
||||
}
|
||||
)
|
||||
return (await OpenAPIParser.parse(JSON.parse(gen))) as OpenAPIObject
|
||||
}
|
||||
|
||||
async function getPublicOas(
|
||||
apiType: ApiType,
|
||||
v2?: boolean
|
||||
) {
|
||||
const url = `https://docs.medusajs.com/api/download/${apiType}?version=${v2 ? "2" : "1"}`
|
||||
return await OpenAPIParser.parse(url) as OpenAPIObject
|
||||
}
|
||||
|
||||
async function getOASFromPaths(
|
||||
additionalPaths: string[] = [],
|
||||
customBaseFile?: string
|
||||
|
||||
@@ -24,7 +24,7 @@ const run = async () => {
|
||||
}
|
||||
|
||||
const generateOASSource = async (outDir, apiType) => {
|
||||
const commandParams = ["oas", `--type=${apiType}`, `--out-dir=${outDir}`]
|
||||
const commandParams = ["oas", `--type=${apiType}`, `--out-dir=${outDir}`, "--local"]
|
||||
if (v2) {
|
||||
commandParams.push(`--v2`)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { existsSync, readFileSync } from "fs"
|
||||
import { NextResponse } from "next/server"
|
||||
import path from "path"
|
||||
import { Version } from "../../../../types/openapi"
|
||||
|
||||
type DownloadParams = {
|
||||
params: {
|
||||
@@ -9,8 +10,16 @@ type DownloadParams = {
|
||||
}
|
||||
|
||||
export function GET(request: Request, { params }: DownloadParams) {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const { area } = params
|
||||
const filePath = path.join(process.cwd(), `specs/${area}/openapi.full.yaml`)
|
||||
const version =
|
||||
process.env.NEXT_PUBLIC_VERSIONING === "true"
|
||||
? (searchParams.get("version") as Version) || "1"
|
||||
: "1"
|
||||
const filePath = path.join(
|
||||
process.cwd(),
|
||||
`${version === "1" ? "specs" : "specs-v2"}/${area}/openapi.full.yaml`
|
||||
)
|
||||
|
||||
if (!existsSync(filePath)) {
|
||||
return new NextResponse(null, {
|
||||
|
||||
Reference in New Issue
Block a user