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:
Shahed Nasser
2024-03-26 09:33:06 +02:00
committed by GitHub
parent 509ddf9a56
commit e005987adf
6 changed files with 263 additions and 26 deletions

View 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

View File

@@ -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,

View File

@@ -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()
})
})
})

View File

@@ -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

View File

@@ -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`)
}

View File

@@ -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, {