docs-util: fix workflows not picked for some routes + generate OAS (#13342)
* generated oas * fixes and improvements
This commit is contained in:
@@ -24,6 +24,7 @@
|
||||
"commander": "^11.1.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"eslint": "8.56.0",
|
||||
"fdir": "^6.5.0",
|
||||
"glob": "^11.0.2",
|
||||
"minimatch": "^9.0.3",
|
||||
"openai": "^4.29.1",
|
||||
|
||||
@@ -364,7 +364,7 @@ class OasSchemaHelper {
|
||||
.replace("DTO", "")
|
||||
.replace(this.schemaRefPrefix, "")
|
||||
.replace(
|
||||
/(?<!(AdminProduct|CreateProduct|UpdateProduct|StoreShippingOption|AdminShippingOption|CreateShippingOption|BaseProduct|StoreProduct))Type$/,
|
||||
/(?<!(AdminProduct|CreateProduct|UpdateProduct|StoreShippingOption|AdminShippingOption|CreateShippingOption|BaseProduct|StoreProduct|AdminCreateShippingOption|AdminUpdateShippingOption))Type$/,
|
||||
""
|
||||
)
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ class DefaultKindGenerator<T extends ts.Node = ts.Node> {
|
||||
* @returns {boolean} Whether this generator can be used with the specified node.
|
||||
*/
|
||||
isAllowed(node: ts.Node): node is T {
|
||||
return this.allowedKinds.includes(node.kind)
|
||||
return !this.isIgnored(node) && this.allowedKinds.includes(node.kind)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -638,6 +638,18 @@ class DefaultKindGenerator<T extends ts.Node = ts.Node> {
|
||||
featureFlagTag,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a node is ignored.
|
||||
*
|
||||
* @param node - The node to check.
|
||||
* @returns Whether the node is ignored.
|
||||
*/
|
||||
isIgnored(node: ts.Node): boolean {
|
||||
return ts
|
||||
.getJSDocTags(node)
|
||||
.some((tag) => tag.tagName.getText() === "ignore")
|
||||
}
|
||||
}
|
||||
|
||||
export default DefaultKindGenerator
|
||||
|
||||
@@ -203,6 +203,10 @@ class OasKindGenerator extends FunctionKindGenerator {
|
||||
return false
|
||||
}
|
||||
|
||||
if (this.isIgnored(functionNode)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const hasCorrectRequestType = this.REQUEST_TYPE_NAMES.some(
|
||||
(name) => functionNode.parameters[0].type?.getText().startsWith(name)
|
||||
)
|
||||
@@ -2653,9 +2657,16 @@ class OasKindGenerator extends FunctionKindGenerator {
|
||||
if (
|
||||
fnText.includes(`${workflowName}(`) ||
|
||||
fnText.includes(`${workflowName} (`) ||
|
||||
fnText.includes(`${workflowName}.`)
|
||||
fnText.includes(`${workflowName}.`) ||
|
||||
fnText.includes(`we.run(${workflowName}`) ||
|
||||
fnText.includes(`we.run (${workflowName}`) ||
|
||||
fnText.includes(`we.run(
|
||||
${workflowName}
|
||||
)`)
|
||||
) {
|
||||
workflow = workflowName
|
||||
// workaround for API routes that execute a workflow
|
||||
// by its ID. Not very smart but will do for now.
|
||||
workflow = workflowName.replace(/Id$/, "")
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import {
|
||||
existsSync,
|
||||
readFileSync,
|
||||
readdirSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
} from "fs"
|
||||
import { existsSync, readFileSync, rmSync, writeFileSync } from "fs"
|
||||
import { fdir } from "fdir"
|
||||
import { OpenAPIV3 } from "openapi-types"
|
||||
import path from "path"
|
||||
import ts from "typescript"
|
||||
@@ -78,175 +73,181 @@ export default async function () {
|
||||
console.log("Cleaning OAS files...")
|
||||
|
||||
// read files under the operations/{area} directory
|
||||
areas.forEach((area) => {
|
||||
const areaPath = path.join(oasOperationsPath, area)
|
||||
if (!existsSync(areaPath)) {
|
||||
return
|
||||
}
|
||||
|
||||
readdirSync(areaPath, {
|
||||
recursive: true,
|
||||
encoding: "utf-8",
|
||||
}).forEach((oasFile) => {
|
||||
const filePath = path.join(areaPath, oasFile)
|
||||
const { oas, oasPrefix } = parseOas(readFileSync(filePath, "utf-8")) || {}
|
||||
|
||||
if (!oas || !oasPrefix) {
|
||||
await Promise.all(
|
||||
areas.map(async (area) => {
|
||||
const areaPath = path.join(oasOperationsPath, area)
|
||||
if (!existsSync(areaPath)) {
|
||||
return
|
||||
}
|
||||
|
||||
// decode oasPrefix
|
||||
const matchOasPrefix = OAS_PREFIX_REGEX.exec(oasPrefix)
|
||||
if (
|
||||
!matchOasPrefix?.groups?.method ||
|
||||
!matchOasPrefix.groups.path ||
|
||||
matchOasPrefix.groups.path.startsWith("/auth/")
|
||||
) {
|
||||
return
|
||||
}
|
||||
const splitPath = matchOasPrefix.groups.path.substring(1).split("/")
|
||||
const dirFiles = await new fdir()
|
||||
.withFullPaths()
|
||||
.crawl(areaPath)
|
||||
.withPromise()
|
||||
|
||||
// normalize path by replacing {paramName} with [paramName]
|
||||
const normalizedOasPrefix = splitPath
|
||||
.map((item) => item.replace(/^\{(.+)\}$/, "[$1]"))
|
||||
.join("/")
|
||||
const sourceFilePath = path.join(
|
||||
apiRoutesPath,
|
||||
normalizedOasPrefix,
|
||||
"route.ts"
|
||||
)
|
||||
dirFiles.forEach((oasFile) => {
|
||||
const { oas, oasPrefix } =
|
||||
parseOas(readFileSync(oasFile, "utf-8")) || {}
|
||||
|
||||
if (!oas["x-ignoreCleanup"]) {
|
||||
// check if a route exists for the path
|
||||
if (!existsSync(sourceFilePath)) {
|
||||
// remove OAS file
|
||||
rmSync(filePath, {
|
||||
force: true,
|
||||
})
|
||||
if (!oas || !oasPrefix) {
|
||||
return
|
||||
}
|
||||
|
||||
// check if method exists in the file
|
||||
let exists = false
|
||||
const program = ts.createProgram([sourceFilePath], {})
|
||||
// decode oasPrefix
|
||||
const matchOasPrefix = OAS_PREFIX_REGEX.exec(oasPrefix)
|
||||
if (
|
||||
!matchOasPrefix?.groups?.method ||
|
||||
!matchOasPrefix.groups.path ||
|
||||
matchOasPrefix.groups.path.startsWith("/auth/")
|
||||
) {
|
||||
return
|
||||
}
|
||||
const splitPath = matchOasPrefix.groups.path.substring(1).split("/")
|
||||
|
||||
const oasKindGenerator = new OasKindGenerator({
|
||||
checker: program.getTypeChecker(),
|
||||
generatorEventManager: new GeneratorEventManager(),
|
||||
additionalOptions: {},
|
||||
// normalize path by replacing {paramName} with [paramName]
|
||||
const normalizedOasPrefix = splitPath
|
||||
.map((item) => item.replace(/^\{(.+)\}$/, "[$1]"))
|
||||
.join("/")
|
||||
const sourceFilePath = path.join(
|
||||
apiRoutesPath,
|
||||
normalizedOasPrefix,
|
||||
"route.ts"
|
||||
)
|
||||
|
||||
if (!oas["x-ignoreCleanup"]) {
|
||||
// check if a route exists for the path
|
||||
if (!existsSync(sourceFilePath)) {
|
||||
// remove OAS file
|
||||
rmSync(oasFile, {
|
||||
force: true,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// check if method exists in the file
|
||||
let exists = false
|
||||
const program = ts.createProgram([sourceFilePath], {})
|
||||
|
||||
const oasKindGenerator = new OasKindGenerator({
|
||||
checker: program.getTypeChecker(),
|
||||
generatorEventManager: new GeneratorEventManager(),
|
||||
additionalOptions: {},
|
||||
})
|
||||
const sourceFile = program.getSourceFile(sourceFilePath)
|
||||
|
||||
if (!sourceFile) {
|
||||
// remove file
|
||||
rmSync(oasFile, {
|
||||
force: true,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const visitChildren = (node: ts.Node) => {
|
||||
if (
|
||||
!exists &&
|
||||
oasKindGenerator.isAllowed(node) &&
|
||||
oasKindGenerator.canDocumentNode(node) &&
|
||||
oasKindGenerator.getHTTPMethodName(node) ===
|
||||
matchOasPrefix.groups!.method
|
||||
) {
|
||||
exists = true
|
||||
} else if (!exists) {
|
||||
ts.forEachChild(node, visitChildren)
|
||||
}
|
||||
}
|
||||
|
||||
ts.forEachChild(sourceFile, visitChildren)
|
||||
|
||||
if (!exists) {
|
||||
// remove OAS file
|
||||
rmSync(oasFile, {
|
||||
force: true,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// collect tags
|
||||
oas.tags?.forEach((tag) => {
|
||||
const areaTags = tags.get(area as OasArea)
|
||||
areaTags?.add(tag)
|
||||
})
|
||||
const sourceFile = program.getSourceFile(sourceFilePath)
|
||||
|
||||
if (!sourceFile) {
|
||||
// remove file
|
||||
rmSync(filePath, {
|
||||
force: true,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const visitChildren = (node: ts.Node) => {
|
||||
if (
|
||||
!exists &&
|
||||
oasKindGenerator.isAllowed(node) &&
|
||||
oasKindGenerator.canDocumentNode(node) &&
|
||||
oasKindGenerator.getHTTPMethodName(node) ===
|
||||
matchOasPrefix.groups!.method
|
||||
) {
|
||||
exists = true
|
||||
} else if (!exists) {
|
||||
ts.forEachChild(node, visitChildren)
|
||||
}
|
||||
}
|
||||
|
||||
ts.forEachChild(sourceFile, visitChildren)
|
||||
|
||||
if (!exists) {
|
||||
// remove OAS file
|
||||
rmSync(filePath, {
|
||||
force: true,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// collect tags
|
||||
oas.tags?.forEach((tag) => {
|
||||
const areaTags = tags.get(area as OasArea)
|
||||
areaTags?.add(tag)
|
||||
})
|
||||
|
||||
// collect schemas
|
||||
oas.parameters?.forEach((parameter) => {
|
||||
if (oasSchemaHelper.isRefObject(parameter)) {
|
||||
referencedSchemas.add(
|
||||
oasSchemaHelper.normalizeSchemaName(parameter.$ref)
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (!parameter.schema) {
|
||||
return
|
||||
}
|
||||
|
||||
if (oasSchemaHelper.isRefObject(parameter.schema)) {
|
||||
referencedSchemas.add(
|
||||
oasSchemaHelper.normalizeSchemaName(parameter.schema.$ref)
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
testAndFindReferenceSchema(parameter.schema)
|
||||
})
|
||||
|
||||
if (oas.requestBody) {
|
||||
if (oasSchemaHelper.isRefObject(oas.requestBody)) {
|
||||
referencedSchemas.add(
|
||||
oasSchemaHelper.normalizeSchemaName(oas.requestBody.$ref)
|
||||
)
|
||||
} else {
|
||||
const requestBodySchema =
|
||||
oas.requestBody.content[Object.keys(oas.requestBody.content)[0]]
|
||||
.schema
|
||||
if (requestBodySchema) {
|
||||
testAndFindReferenceSchema(requestBodySchema)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (oas.responses) {
|
||||
const successResponseKey = Object.keys(oas.responses)[0]
|
||||
if (!Object.keys(DEFAULT_OAS_RESPONSES).includes(successResponseKey)) {
|
||||
const responseObj = oas.responses[successResponseKey]
|
||||
if (oasSchemaHelper.isRefObject(responseObj)) {
|
||||
// collect schemas
|
||||
oas.parameters?.forEach((parameter) => {
|
||||
if (oasSchemaHelper.isRefObject(parameter)) {
|
||||
referencedSchemas.add(
|
||||
oasSchemaHelper.normalizeSchemaName(responseObj.$ref)
|
||||
oasSchemaHelper.normalizeSchemaName(parameter.$ref)
|
||||
)
|
||||
} else if (responseObj.content) {
|
||||
const responseBodySchema =
|
||||
responseObj.content[Object.keys(responseObj.content)[0]].schema
|
||||
if (responseBodySchema) {
|
||||
testAndFindReferenceSchema(responseBodySchema)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (!parameter.schema) {
|
||||
return
|
||||
}
|
||||
|
||||
if (oasSchemaHelper.isRefObject(parameter.schema)) {
|
||||
referencedSchemas.add(
|
||||
oasSchemaHelper.normalizeSchemaName(parameter.schema.$ref)
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
testAndFindReferenceSchema(parameter.schema)
|
||||
})
|
||||
|
||||
if (oas.requestBody) {
|
||||
if (oasSchemaHelper.isRefObject(oas.requestBody)) {
|
||||
referencedSchemas.add(
|
||||
oasSchemaHelper.normalizeSchemaName(oas.requestBody.$ref)
|
||||
)
|
||||
} else {
|
||||
const requestBodySchema =
|
||||
oas.requestBody.content[Object.keys(oas.requestBody.content)[0]]
|
||||
.schema
|
||||
if (requestBodySchema) {
|
||||
testAndFindReferenceSchema(requestBodySchema)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (oas.responses) {
|
||||
const successResponseKey = Object.keys(oas.responses)[0]
|
||||
if (
|
||||
!Object.keys(DEFAULT_OAS_RESPONSES).includes(successResponseKey)
|
||||
) {
|
||||
const responseObj = oas.responses[successResponseKey]
|
||||
if (oasSchemaHelper.isRefObject(responseObj)) {
|
||||
referencedSchemas.add(
|
||||
oasSchemaHelper.normalizeSchemaName(responseObj.$ref)
|
||||
)
|
||||
} else if (responseObj.content) {
|
||||
const responseBodySchema =
|
||||
responseObj.content[Object.keys(responseObj.content)[0]].schema
|
||||
if (responseBodySchema) {
|
||||
testAndFindReferenceSchema(responseBodySchema)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
console.log("Clean tags...")
|
||||
|
||||
// check if any tags should be removed
|
||||
const oasBasePath = path.join(oasOutputBasePath, "base")
|
||||
readdirSync(oasBasePath, {
|
||||
recursive: true,
|
||||
encoding: "utf-8",
|
||||
}).forEach((baseYaml) => {
|
||||
const baseYamlPath = path.join(oasBasePath, baseYaml)
|
||||
const baseFiles = await new fdir()
|
||||
.withFullPaths()
|
||||
.crawl(oasBasePath)
|
||||
.withPromise()
|
||||
baseFiles.forEach((baseYaml) => {
|
||||
const parsedBaseYaml = parse(
|
||||
readFileSync(baseYamlPath, "utf-8")
|
||||
readFileSync(baseYaml, "utf-8")
|
||||
) as OpenApiDocument
|
||||
|
||||
const area = path.basename(baseYaml).split(".")[0] as OasArea
|
||||
@@ -266,7 +267,7 @@ export default async function () {
|
||||
return tagA.name.localeCompare(tagB.name)
|
||||
})
|
||||
// write to the file
|
||||
writeFileSync(baseYamlPath, stringify(parsedBaseYaml))
|
||||
writeFileSync(baseYaml, stringify(parsedBaseYaml))
|
||||
}
|
||||
|
||||
// collect referenced schemas
|
||||
@@ -284,18 +285,18 @@ export default async function () {
|
||||
// check if any schemas should be removed
|
||||
// a schema is removed if no other schemas/operations reference it
|
||||
const oasSchemasPath = path.join(oasOutputBasePath, "schemas")
|
||||
readdirSync(oasSchemasPath, {
|
||||
recursive: true,
|
||||
encoding: "utf-8",
|
||||
}).forEach((schemaYaml) => {
|
||||
const schemaPath = path.join(oasSchemasPath, schemaYaml)
|
||||
const oasSchemaFiles = await new fdir()
|
||||
.withFullPaths()
|
||||
.crawl(oasSchemasPath)
|
||||
.withPromise()
|
||||
oasSchemaFiles.forEach((schemaYaml) => {
|
||||
const parsedSchema = oasSchemaHelper.parseSchema(
|
||||
readFileSync(schemaPath, "utf-8")
|
||||
readFileSync(schemaYaml, "utf-8")
|
||||
)
|
||||
|
||||
if (!parsedSchema) {
|
||||
// remove file
|
||||
rmSync(schemaPath, {
|
||||
rmSync(schemaYaml, {
|
||||
force: true,
|
||||
})
|
||||
return
|
||||
@@ -311,19 +312,19 @@ export default async function () {
|
||||
})
|
||||
|
||||
// clean up schemas
|
||||
allSchemas.forEach((schemaName) => {
|
||||
if (
|
||||
referencedSchemas.has(schemaName) ||
|
||||
ignoreSchemas.includes(schemaName)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
// schema isn't referenced anywhere, so remove it
|
||||
rmSync(path.join(oasSchemasPath, `${schemaName}.ts`), {
|
||||
force: true,
|
||||
Array.from(allSchemas)
|
||||
.filter((schemaName) => {
|
||||
return (
|
||||
!referencedSchemas.has(schemaName) &&
|
||||
!ignoreSchemas.includes(schemaName)
|
||||
)
|
||||
})
|
||||
.forEach((schemaName) => {
|
||||
// schema isn't referenced anywhere, so remove it
|
||||
rmSync(path.join(oasSchemasPath, `${schemaName}.ts`), {
|
||||
force: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
console.log("Finished clean up")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user