feat(oas-cli): combine admin + store + custom OAS (#3411)

This commit is contained in:
Patrick authored and GitHub committed 2023-03-10 10:37:59 +01:00
1 parent ef4a36794e
commit 966aea65c2
16 files changed
+816 -37

No files matched your search

+6
View File
@@ -0,0 +1,6 @@
---
"@medusajs/medusa-oas-cli": minor
"@medusajs/medusa": patch
---
feat(oas-cli): combine admin + store + custom OAS
+2 -1
View File
@@ -13,7 +13,8 @@
"access": "public" "access": "public"
}, },
"files": [ "files": [
"dist" "dist",
"oas"
], ],
"author": "Sebastian Rindom", "author": "Sebastian Rindom",
"license": "MIT", "license": "MIT",
+14 -4
View File
@@ -25,15 +25,16 @@ yarn medusa-oas <command>
This command will scan the `@medusajs/medusa` package in order to extract JSDoc OAS into a json file. This command will scan the `@medusajs/medusa` package in order to extract JSDoc OAS into a json file.
By default, the command will output the two files `admin.oas.json` and `store.oas.json` in the same directory that the The command will output one of three the files `admin.oas.json`, `store.oas.json` or `combined.oas.json` in the same
command was run. directory that the command was run.
Invalid OAS with throw an error and will prevent the files from being outputted. Invalid OAS with throw an error and will prevent the files from being outputted.
#### `--type <string>` #### `--type <string>`
Specify which API OAS to create. Accepts `all`, `admin`, `store`. Specify which API OAS to create. Accepts `admin`, `store`, `combined`.
Defaults to `all`.
The `combined` option will merge both the admin and the store APIs into a single OAS file.
```bash ```bash
yarn medusa-oas oas --type admin yarn medusa-oas oas --type admin
@@ -57,6 +58,15 @@ It accepts multiple entries.
yarn medusa-oas oas --paths ~/medusa-server/src yarn medusa-oas oas --paths ~/medusa-server/src
``` ```
#### `--base <path>`
Allows overwriting the content the API's base.yaml OAS that is fed to swagger-inline.
Paths, tags, and components will be merged together. Other OAS properties will be overwritten.
```bash
yarn medusa-oas oas --base ~/medusa-server/oas/custom.oas.base.yaml
```
#### `--dry-run` #### `--dry-run`
Will package the OAS but will not output file. Useful for validating OAS. Will package the OAS but will not output file. Useful for validating OAS.
@@ -0,0 +1,14 @@
module.exports = {
globals: {
"ts-jest": {
tsConfig: "tsconfig.json",
isolatedModules: false,
},
},
transform: {
"^.+\\.[jt]s?$": "ts-jest",
},
testEnvironment: `node`,
moduleFileExtensions: [`js`, `ts`],
testTimeout: 30000,
}
@@ -0,0 +1,5 @@
openapi: 3.0.0
info:
version: 1.0.0
title: Medusa API
paths: { }
+9 -4
View File
@@ -7,7 +7,8 @@
"medusa-oas": "./dist/index.js" "medusa-oas": "./dist/index.js"
}, },
"files": [ "files": [
"dist" "dist",
"oas"
], ],
"repository": { "repository": {
"type": "git", "type": "git",
@@ -20,15 +21,19 @@
"author": "Medusa", "author": "Medusa",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"jest": "^25.5.4",
"js-yaml": "^4.1.0",
"ts-jest": "^25.5.1",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"typescript": "4.9.5" "typescript": "4.9.5",
"uuid": "^9.0.0"
}, },
"scripts": { "scripts": {
"prepare": "cross-env NODE_ENV=production yarn run build", "prepare": "cross-env NODE_ENV=production yarn run build",
"build": "tsc --build", "build": "tsc --build",
"cli": "ts-node src/index.ts", "cli": "ts-node src/index.ts",
"test": "jest --passWithNoTests", "test": "jest src",
"test:unit": "jest --passWithNoTests" "test:unit": "jest src"
}, },
"dependencies": { "dependencies": {
"@medusajs/medusa": "*", "@medusajs/medusa": "*",
@@ -0,0 +1,465 @@
import os from "os"
import fs from "fs/promises"
import execa from "execa"
import path from "path"
import { v4 as uid } from "uuid"
import { OpenAPIObject, SchemaObject } from "openapi3-ts"
import * as yaml from "js-yaml"
import { OperationObject } from "openapi3-ts/src/model/OpenApi"
const medusaPackagePath = path.dirname(
require.resolve("@medusajs/medusa/package.json")
)
const basePath = path.resolve(__dirname, `../../`)
const getTmpDirectory = async () => {
/**
* RUNNER_TEMP: GitHub action, the path to a temporary directory on the runner.
*/
const tmpDir = process.env["RUNNER_TEMP"] ?? os.tmpdir()
return await fs.mkdtemp(tmpDir)
}
const runCLI = async (command: string, options: string[] = []) => {
const params = ["run", "cli", command, ...options]
try {
const { all: logs } = await execa("yarn", params, {
cwd: basePath,
all: true,
})
} catch (err) {
console.error(err)
throw err
}
}
const isFile = async (filePath): Promise<boolean> => {
return (await fs.lstat(path.resolve(filePath))).isFile()
}
const readJsonFile = async (filePath): Promise<unknown> => {
const jsonString = await fs.readFile(filePath, "utf8")
return JSON.parse(jsonString)
}
const readYamlFile = async (filePath): Promise<unknown> => {
const yamlString = await fs.readFile(filePath, "utf8")
return yaml.load(yamlString)
}
const listOperations = (oas: OpenAPIObject): OperationObject[] => {
const operations: OperationObject[] = []
for (const url in oas.paths) {
if (oas.paths.hasOwnProperty(url)) {
const path = oas.paths[url]
for (const method in path) {
if (path.hasOwnProperty(method)) {
switch (method) {
case "get":
case "put":
case "post":
case "delete":
case "options":
case "head":
case "patch":
operations.push(path[method])
break
}
}
}
}
}
return operations
}
describe("command oas", () => {
let tmpDir: string
beforeAll(async () => {
tmpDir = await getTmpDirectory()
})
describe("--type admin", () => {
let oas: OpenAPIObject
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 readJsonFile(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()
})
it("generates oas using admin.oas.base.yaml", async () => {
const yamlFilePath = path.resolve(
medusaPackagePath,
"oas",
"admin.oas.base.yaml"
)
const oasBase = (await readYamlFile(yamlFilePath)) as OpenAPIObject
expect(oas.info.title).toEqual(oasBase.info.title)
})
})
describe("--type store", () => {
let oas: OpenAPIObject
beforeAll(async () => {
const outDir = path.resolve(tmpDir, uid())
await runCLI("oas", ["--type", "store", "--out-dir", outDir])
const generatedFilePath = path.resolve(outDir, "store.oas.json")
oas = (await readJsonFile(generatedFilePath)) as OpenAPIObject
})
it("generates oas with store routes only", async () => {
const routes = Object.keys(oas.paths)
expect(routes.includes("/admin/products")).toBeFalsy()
expect(routes.includes("/store/products")).toBeTruthy()
})
it("generates oas using store.oas.base.yaml", async () => {
const yamlFilePath = path.resolve(
medusaPackagePath,
"oas",
"store.oas.base.yaml"
)
const oasBase = (await readYamlFile(yamlFilePath)) as OpenAPIObject
expect(oas.info.title).toEqual(oasBase.info.title)
})
})
describe("--type combined", () => {
let oas: OpenAPIObject
beforeAll(async () => {
const outDir = path.resolve(tmpDir, uid())
await runCLI("oas", ["--type", "combined", "--out-dir", outDir])
const generatedFilePath = path.resolve(outDir, "combined.oas.json")
oas = (await readJsonFile(generatedFilePath)) as OpenAPIObject
})
it("generates oas with admin and store routes", async () => {
const routes = Object.keys(oas.paths)
expect(routes.includes("/admin/products")).toBeTruthy()
expect(routes.includes("/store/products")).toBeTruthy()
})
it("generates oas using default.oas.base.yaml", async () => {
const yamlFilePath = path.resolve(
basePath,
"oas",
"default.oas.base.yaml"
)
const oasBase = (await readYamlFile(yamlFilePath)) as OpenAPIObject
expect(oas.info.title).toEqual(oasBase.info.title)
})
it("prefixes tags with api type", async () => {
const found = (oas.tags ?? []).filter((tag) => {
return !(tag.name.startsWith("Admin") || tag.name.startsWith("Store"))
})
expect(found).toEqual([])
})
it("prefixes route's tags with api type", async () => {
const tags: string[] = listOperations(oas)
.map((operation) => {
return operation.tags ?? []
})
.flat()
const found = tags.filter((tag) => {
return !(tag.startsWith("Admin") || tag.startsWith("Store"))
})
expect(found).toEqual([])
})
it("prefixes route's operationId with api type", async () => {
const operationIds: string[] = listOperations(oas)
.map((operation) => operation.operationId)
.filter((operationId): operationId is string => !!operationId)
const found = operationIds.filter((tag) => {
return !(tag.startsWith("Admin") || tag.startsWith("Store"))
})
expect(found).toEqual([])
})
it("combines components.schemas from admin and store", async () => {
const schemas = Object.keys(oas.components?.schemas ?? {})
expect(schemas.includes("AdminProductsListRes")).toBeTruthy()
expect(schemas.includes("StoreProductsListRes")).toBeTruthy()
})
})
/**
* to optimize test suite time, we only test --paths with the store api
*/
describe("--paths", () => {
let oas: OpenAPIObject
beforeAll(async () => {
const fileContent = `
/** @oas [get] /foobar/tests
* operationId: GetFoobarTests
*/
/** @oas [get] /store/regions
* operationId: OverwrittenOperation
*/
/**
* @schema FoobarTestSchema
* type: object
* properties:
* foo:
* type: string
*/
/**
* @schema StoreRegionsListRes
* type: object
* properties:
* foo:
* type: string
*/
`
const additionalPath = path.resolve(tmpDir, uid())
const filePath = path.resolve(additionalPath, "foobar.ts")
await fs.mkdir(additionalPath, { recursive: true })
await fs.writeFile(filePath, fileContent, "utf8")
const outDir = path.resolve(tmpDir, uid())
await runCLI("oas", [
"--type",
"store",
"--out-dir",
outDir,
"--paths",
additionalPath,
])
const generatedFilePath = path.resolve(outDir, "store.oas.json")
oas = (await readJsonFile(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()
})
})
/**
* to optimize test suite time, we only test --base with the store api
*/
describe("--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 readJsonFile(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()
})
})
})
+76 -19
View File
@@ -4,6 +4,11 @@ import swaggerInline from "swagger-inline"
import OpenAPIParser from "@readme/openapi-parser" import OpenAPIParser from "@readme/openapi-parser"
import { OpenAPIObject } from "openapi3-ts" import { OpenAPIObject } from "openapi3-ts"
import { Command, Option, OptionValues } from "commander" import { Command, Option, OptionValues } from "commander"
import { combineOAS } from "./utils/combine-oas"
import {
mergeBaseIntoOAS,
mergePathsAndSchemasIntoOAS,
} from "./utils/merge-oas"
/** /**
* Constants * Constants
@@ -12,8 +17,7 @@ import { Command, Option, OptionValues } from "commander"
const medusaPackagePath = path.dirname( const medusaPackagePath = path.dirname(
require.resolve("@medusajs/medusa/package.json") require.resolve("@medusajs/medusa/package.json")
) )
const basePath = path.resolve(__dirname, "../")
type ApiType = "store" | "admin"
/** /**
* CLI Command declaration * CLI Command declaration
@@ -23,9 +27,9 @@ export const commandDescription =
"Compile full OAS from swagger-inline compliant JSDoc." "Compile full OAS from swagger-inline compliant JSDoc."
export const commandOptions: Option[] = [ export const commandOptions: Option[] = [
new Option("-t, --type <type>", "API type to compile []") new Option("-t, --type <type>", "API type to compile.")
.choices(["all", "admin", "store"]) .choices(["admin", "store", "combined"])
.default("all"), .makeOptionMandatory(),
new Option( new Option(
"-o, --out-dir <outDir>", "-o, --out-dir <outDir>",
"Destination directory to output generated OAS files." "Destination directory to output generated OAS files."
@@ -35,6 +39,10 @@ export const commandOptions: Option[] = [
"-p, --paths <paths...>", "-p, --paths <paths...>",
"Additional paths to crawl for OAS JSDoc." "Additional paths to crawl for OAS JSDoc."
), ),
new Option(
"-b, --base <base>",
"Custom base OAS file to use for swagger-inline."
),
new Option("-F, --force", "Ignore OAS validation and output OAS files."), new Option("-F, --force", "Ignore OAS validation and output OAS files."),
] ]
@@ -58,8 +66,7 @@ export async function execute(cliParams: OptionValues) {
const dryRun = !!cliParams.dryRun const dryRun = !!cliParams.dryRun
const force = !!cliParams.force const force = !!cliParams.force
const apiTypesToExport = const apiType: ApiType = cliParams.type
cliParams.type === "all" ? ["store", "admin"] : [cliParams.type]
const outDir = path.resolve(cliParams.outDir) const outDir = path.resolve(cliParams.outDir)
@@ -72,6 +79,13 @@ export async function execute(cliParams: OptionValues) {
} }
} }
const baseFile = cliParams.base ? path.resolve(cliParams.base) : undefined
if (baseFile) {
if (!(await isFile(cliParams.base))) {
throw new Error(`--base must be a file - ${baseFile}`)
}
}
/** /**
* Command execution * Command execution
*/ */
@@ -79,13 +93,30 @@ export async function execute(cliParams: OptionValues) {
await mkdir(outDir, { recursive: true }) await mkdir(outDir, { recursive: true })
} }
for (const apiType of apiTypesToExport) { let oas: OpenAPIObject
console.log(`🟣 Generating OAS - ${apiType}`) console.log(`🟣 Generating OAS - ${apiType}`)
const oas = await getOASFromCodebase(apiType as ApiType, additionalPaths)
await validateOAS(oas, apiType as ApiType, force) if (apiType === "combined") {
if (!dryRun) { const adminOAS = await getOASFromCodebase("admin")
await exportOASToJSON(oas, apiType as ApiType, outDir) const storeOAS = await getOASFromCodebase("store")
oas = await combineOAS(adminOAS, storeOAS)
} else {
oas = await getOASFromCodebase(apiType)
} }
if (additionalPaths.length || baseFile) {
const customOAS = await getOASFromPaths(additionalPaths, baseFile)
if (baseFile) {
mergeBaseIntoOAS(oas, customOAS)
}
if (additionalPaths.length) {
mergePathsAndSchemasIntoOAS(oas, customOAS)
}
}
await validateOAS(oas, apiType, force)
if (!dryRun) {
await exportOASToJSON(oas, apiType, outDir)
} }
} }
@@ -94,7 +125,7 @@ export async function execute(cliParams: OptionValues) {
*/ */
async function getOASFromCodebase( async function getOASFromCodebase(
apiType: ApiType, apiType: ApiType,
additionalPaths: string[] = [] customBaseFile?: string
): Promise<OpenAPIObject> { ): Promise<OpenAPIObject> {
const gen = await swaggerInline( const gen = await swaggerInline(
[ [
@@ -102,18 +133,30 @@ async function getOASFromCodebase(
path.resolve(medusaPackagePath, "dist", "types"), path.resolve(medusaPackagePath, "dist", "types"),
path.resolve(medusaPackagePath, "dist", "api/middlewares"), path.resolve(medusaPackagePath, "dist", "api/middlewares"),
path.resolve(medusaPackagePath, "dist", `api/routes/${apiType}`), path.resolve(medusaPackagePath, "dist", `api/routes/${apiType}`),
...additionalPaths,
], ],
{ {
base: path.resolve( base:
medusaPackagePath, customBaseFile ??
"oas", path.resolve(medusaPackagePath, "oas", `${apiType}.oas.base.yaml`),
`${apiType}-spec3-base.yaml`
),
format: ".json", format: ".json",
} }
) )
return await OpenAPIParser.parse(JSON.parse(gen))
}
async function getOASFromPaths(
additionalPaths: string[] = [],
customBaseFile?: string
): Promise<OpenAPIObject> {
console.log(`🔵 Gathering custom OAS`)
const gen = await swaggerInline(additionalPaths, {
base:
customBaseFile ?? path.resolve(basePath, "oas", "default.oas.base.yaml"),
format: ".json",
logger: (log) => {
console.log(log)
},
})
return await OpenAPIParser.parse(JSON.parse(gen)) return await OpenAPIParser.parse(JSON.parse(gen))
} }
@@ -145,5 +188,19 @@ async function exportOASToJSON(
} }
async function isDirectory(dirPath: string): Promise<boolean> { async function isDirectory(dirPath: string): Promise<boolean> {
try {
return (await lstat(path.resolve(dirPath))).isDirectory() return (await lstat(path.resolve(dirPath))).isDirectory()
} catch (err) {
console.log(err)
return false
}
}
async function isFile(filePath: string): Promise<boolean> {
try {
return (await lstat(path.resolve(filePath))).isFile()
} catch (err) {
console.log(err)
return false
}
} }
+1
View File
@@ -0,0 +1 @@
type ApiType = "store" | "admin" | "combined"
@@ -0,0 +1,118 @@
import { OpenAPIObject } from "openapi3-ts"
import { upperFirst } from "lodash"
export async function combineOAS(
adminOAS: OpenAPIObject,
storeOAS: OpenAPIObject
): Promise<OpenAPIObject> {
prepareOASForCombine(adminOAS, "admin")
prepareOASForCombine(storeOAS, "store")
const combinedOAS: OpenAPIObject = {
openapi: "3.0.0",
info: { title: "Medusa API", version: "1.0.0" },
servers: [],
paths: {},
tags: [],
components: {
callbacks: {},
examples: {},
headers: {},
links: {},
parameters: {},
requestBodies: {},
responses: {},
schemas: {},
securitySchemes: {},
},
}
for (const oas of [adminOAS, storeOAS]) {
/**
* Combine paths
*/
Object.assign(combinedOAS.paths, oas.paths)
/**
* Combine tags
*/
if (oas.tags) {
combinedOAS.tags = [...combinedOAS.tags!, ...oas.tags]
}
/**
* Combine components
*/
if (oas.components) {
for (const componentGroup of [
"callbacks",
"examples",
"headers",
"links",
"parameters",
"requestBodies",
"responses",
"schemas",
"securitySchemes",
]) {
if (Object.keys(oas.components).includes(componentGroup)) {
Object.assign(
combinedOAS.components![componentGroup],
oas.components[componentGroup]
)
}
}
}
}
return combinedOAS
}
function prepareOASForCombine(
oas: OpenAPIObject,
apiType: ApiType
): OpenAPIObject {
console.log(
`🔵 Prefixing ${apiType} tags and operationId with ${upperFirst(apiType)}`
)
for (const pathKey in oas.paths) {
for (const operationKey in oas.paths[pathKey]) {
/**
* Prefix tags declared on routes
* e.g.: Admin Customer, Store Customer
*/
if (oas.paths[pathKey][operationKey].tags) {
oas.paths[pathKey][operationKey].tags = oas.paths[pathKey][
operationKey
].tags.map((tag) => getPrefixedTagName(tag, apiType))
}
/**
* Prefix operationId
* e.g.: AdminGetCustomers, StoreGetCustomers
*/
if (oas.paths[pathKey][operationKey].operationId) {
oas.paths[pathKey][operationKey].operationId = getPrefixedOperationId(
oas.paths[pathKey][operationKey].operationId,
apiType
)
}
}
}
/**
* Prefix tags globally
* e.g.: Admin Customer, Store Customer
*/
if (oas.tags) {
for (const tag of oas.tags) {
tag.name = getPrefixedTagName(tag.name, apiType)
}
}
return oas
}
function getPrefixedTagName(tagName: string, apiType: ApiType): string {
return `${upperFirst(apiType)} ${tagName}`
}
function getPrefixedOperationId(operationId: string, apiType: ApiType): string {
return `${upperFirst(apiType)}${operationId}`
}
@@ -0,0 +1,93 @@
import { OpenAPIObject, TagObject } from "openapi3-ts"
export function mergeBaseIntoOAS(
targetOAS: OpenAPIObject,
sourceOAS: OpenAPIObject
): void {
/**
* replace strategy for OpenAPIObject properties
*/
targetOAS.openapi = sourceOAS.openapi ?? targetOAS.openapi
targetOAS.info = sourceOAS.info ?? targetOAS.info
targetOAS.servers = sourceOAS.servers ?? targetOAS.servers
targetOAS.security = sourceOAS.security ?? targetOAS.security
targetOAS.externalDocs = sourceOAS.externalDocs ?? targetOAS.externalDocs
targetOAS.webhooks = sourceOAS.webhooks ?? targetOAS.webhooks
/**
* merge + concat strategy for tags
*/
const targetTags = targetOAS.tags ?? []
const sourceTags = sourceOAS.tags ?? []
const combinedTags: TagObject[] = []
const sourceIndexes: number[] = []
for (const targetTag of targetTags) {
for (const [sourceTagIndex, sourceTag] of sourceTags.entries()) {
if (targetTag.name === sourceTag.name) {
combinedTags.push(sourceTag)
sourceIndexes.push(sourceTagIndex)
continue
}
combinedTags.push(targetTag)
}
}
for (const [sourceTagIndex, sourceTag] of sourceTags.entries()) {
if (!sourceIndexes.includes(sourceTagIndex)) {
combinedTags.push(sourceTag)
}
}
targetOAS.tags = combinedTags
/**
* merge strategy for paths
*/
targetOAS.paths = Object.assign(targetOAS.paths ?? {}, sourceOAS.paths ?? {})
/**
* merge strategy for components
*/
if (!sourceOAS.components) {
return
}
if (!targetOAS.components) {
targetOAS.components = {}
}
for (const componentGroup of [
"callbacks",
"examples",
"headers",
"links",
"parameters",
"requestBodies",
"responses",
"schemas",
"securitySchemes",
]) {
if (Object.keys(sourceOAS.components).includes(componentGroup)) {
targetOAS.components[componentGroup] = Object.assign(
targetOAS.components[componentGroup] ?? {},
sourceOAS.components[componentGroup]
)
}
}
}
export function mergePathsAndSchemasIntoOAS(
targetOAS: OpenAPIObject,
sourceOAS: OpenAPIObject
): void {
/**
* merge paths
*/
Object.assign(targetOAS.paths, sourceOAS.paths)
/**
* merge components.schemas
*/
if (sourceOAS.components?.schemas) {
if (!targetOAS.components) {
targetOAS.components = {}
}
if (!targetOAS.components.schemas) {
targetOAS.components.schemas = {}
}
Object.assign(targetOAS.components.schemas, sourceOAS.components.schemas)
}
}
+5 -2
View File
@@ -24,8 +24,11 @@
"src" "src"
], ],
"exclude": [ "exclude": [
"node_modules", "dist",
"**/tests/*" "src/**/__tests__",
"src/**/__mocks__",
"src/**/__fixtures__",
"node_modules"
], ],
"ts-node": { "ts-node": {
"transpileOnly": true "transpileOnly": true
@@ -19,9 +19,8 @@ const redoclyConfigPath = path.resolve(
const run = async () => { const run = async () => {
const outputPath = isDryRun ? await getTmpDirectory() : docsApiPath const outputPath = isDryRun ? await getTmpDirectory() : docsApiPath
await generateOASSources(outputPath)
for (const apiType of ["store", "admin"]) { for (const apiType of ["store", "admin"]) {
await generateOASSource(outputPath, apiType)
const inputJsonFile = path.resolve(outputPath, `${apiType}.oas.json`) const inputJsonFile = path.resolve(outputPath, `${apiType}.oas.json`)
const outputYamlFile = path.resolve(outputPath, `${apiType}.oas.yaml`) const outputYamlFile = path.resolve(outputPath, `${apiType}.oas.yaml`)
@@ -34,11 +33,8 @@ const run = async () => {
} }
} }
const generateOASSources = async (outDir, isDryRun) => { const generateOASSource = async (outDir, apiType) => {
const params = ["oas", `--out-dir=${outDir}`] const params = ["oas", `--type=${apiType}`, `--out-dir=${outDir}`]
if (isDryRun) {
params.push("--dry-run")
}
const { all: logs } = await execa("medusa-oas", params, { const { all: logs } = await execa("medusa-oas", params, {
cwd: basePath, cwd: basePath,
all: true, all: true,
@@ -78,6 +74,7 @@ const circularReferenceCheck = async (srcFile) => {
`🔴 Unhandled circular references - ${fileName} - Please patch in docs-util/redocly/config.yaml` `🔴 Unhandled circular references - ${fileName} - Please patch in docs-util/redocly/config.yaml`
) )
} }
console.log(`🟢 All circular references handled`)
} }
const generateReference = async (srcFile, apiType) => { const generateReference = async (srcFile, apiType) => {
+4
View File
@@ -5913,11 +5913,15 @@ __metadata:
"@readme/openapi-parser": ^2.4.0 "@readme/openapi-parser": ^2.4.0
"@types/lodash": ^4.14.191 "@types/lodash": ^4.14.191
commander: ^10.0.0 commander: ^10.0.0
jest: ^25.5.4
js-yaml: ^4.1.0
lodash: ^4.17.21 lodash: ^4.17.21
openapi3-ts: ^3.1.2 openapi3-ts: ^3.1.2
swagger-inline: ^6.1.0 swagger-inline: ^6.1.0
ts-jest: ^25.5.1
ts-node: ^10.9.1 ts-node: ^10.9.1
typescript: 4.9.5 typescript: 4.9.5
uuid: ^9.0.0
bin: bin:
medusa-oas: ./dist/index.js medusa-oas: ./dist/index.js
languageName: unknown languageName: unknown