docs-util: fixes for circular references, PAK in examples, namespaces (#9091)

- Fix circular references in generated OAS comments by cloning objects
- Add publishable API key in the header of store cURL examples + change the admin authentication header to use bearer tokens.
- Add plugin to generate namespaces from the `@customNamespace` tag in TSDocs, this was available before but was removed.
- Other fixes related to re-using schemas, overwritten descriptions, and smaller fixes
This commit is contained in:
Shahed Nasser
2024-09-16 20:55:05 +03:00
committed by GitHub
parent 05c88268d3
commit cb79a5dbff
10 changed files with 448 additions and 42 deletions

View File

@@ -216,11 +216,15 @@ class OasExamplesGenerator {
]
if (isAdminAuthenticated) {
exampleArr.push(`-H 'x-medusa-access-token: {api_token}'`)
exampleArr.push(`-H 'Authorization: Bearer {access_token}'`)
} else if (isStoreAuthenticated) {
exampleArr.push(`-H 'Authorization: Bearer {access_token}'`)
}
if (path.startsWith("/store")) {
exampleArr.push(`-H 'x-publishable-api-key: {your_publishable_api_key}'`)
}
if (requestSchema) {
const requestData = this.getSchemaRequiredData(requestSchema)

View File

@@ -113,7 +113,9 @@ class OasSchemaHelper {
})
}
this.schemas.set(schema["x-schemaName"], schema)
if (this.canAddSchema(schema)) {
this.schemas.set(schema["x-schemaName"], schema)
}
return {
$ref: this.constructSchemaReference(schema["x-schemaName"]),
@@ -181,6 +183,36 @@ class OasSchemaHelper {
return clonedSchema
}
isSchemaEmpty(schema: OpenApiSchema): boolean {
switch (schema.type) {
case "object":
return (
schema.properties === undefined ||
Object.keys(schema.properties).length === 0
)
case "array":
return (
!this.isRefObject(schema.items) && this.isSchemaEmpty(schema.items)
)
default:
return false
}
}
canAddSchema(schema: OpenApiSchema): boolean {
if (!schema["x-schemaName"]) {
return false
}
const existingSchema = this.schemas.get(schema["x-schemaName"])
if (!existingSchema) {
return true
}
return this.isSchemaEmpty(existingSchema) && !this.isSchemaEmpty(schema)
}
/**
* Retrieve the expected file name of the schema.
*
@@ -212,7 +244,7 @@ class OasSchemaHelper {
// check if it already exists in the schemas map
if (this.schemas.has(schemaName)) {
return {
schema: this.schemas.get(schemaName)!,
schema: JSON.parse(JSON.stringify(this.schemas.get(schemaName)!)),
schemaPrefix: `@schema ${schemaName}`,
}
}
@@ -272,7 +304,7 @@ class OasSchemaHelper {
return name
.replace("DTO", "")
.replace(this.schemaRefPrefix, "")
.replace(/(?<!AdminProduct)Type$/, "")
.replace(/(?<!(AdminProduct|CreateProduct))Type$/, "")
}
/**

View File

@@ -79,7 +79,7 @@ class SchemaFactory {
schema = Object.assign(schema, {
...additionalData,
// keep the description
description: schema.description || additionalData.description
description: schema.description || additionalData.description,
})
}

View File

@@ -463,6 +463,7 @@ class OasKindGenerator extends FunctionKindGenerator {
node,
tagName,
methodName,
forUpdate: true,
})
// update query parameters
@@ -511,6 +512,7 @@ class OasKindGenerator extends FunctionKindGenerator {
const newResponseSchema = this.getResponseSchema({
node,
tagName,
forUpdate: true,
})
let updatedResponseSchema: OpenApiSchema | undefined
@@ -968,6 +970,7 @@ class OasKindGenerator extends FunctionKindGenerator {
node,
tagName,
methodName,
forUpdate = false,
}: {
/**
* The node to retrieve its request parameters.
@@ -981,6 +984,10 @@ class OasKindGenerator extends FunctionKindGenerator {
* The tag's name.
*/
tagName?: string
/**
* Whether the request parameters are retrieved for update purposes only.
*/
forUpdate?: boolean
}): {
/**
* The query parameters.
@@ -1041,6 +1048,7 @@ class OasKindGenerator extends FunctionKindGenerator {
title: propertyName,
descriptionOptions,
context: "request",
saveSchema: !forUpdate,
}),
})
)
@@ -1063,6 +1071,7 @@ class OasKindGenerator extends FunctionKindGenerator {
},
zodObjectTypeName: zodObjectTypeName,
context: "request",
saveSchema: !forUpdate,
})
// If function is a GET function, add the type parameter to the
@@ -1135,6 +1144,7 @@ class OasKindGenerator extends FunctionKindGenerator {
getResponseSchema({
node,
tagName,
forUpdate = false,
}: {
/**
* The node to retrieve its response schema.
@@ -1144,6 +1154,10 @@ class OasKindGenerator extends FunctionKindGenerator {
* The tag's name.
*/
tagName?: string
/**
* Whether the response schema is retrieved for update only.
*/
forUpdate?: boolean
}): OpenApiSchema | undefined {
let responseSchema: OpenApiSchema | undefined
@@ -1170,6 +1184,7 @@ class OasKindGenerator extends FunctionKindGenerator {
itemType: responseTypeArguments[0],
}),
context: "response",
saveSchema: !forUpdate,
})
}
}
@@ -1191,6 +1206,7 @@ class OasKindGenerator extends FunctionKindGenerator {
allowedChildren,
disallowedChildren,
zodObjectTypeName,
saveSchema = true,
...rest
}: {
/**
@@ -1229,6 +1245,10 @@ class OasKindGenerator extends FunctionKindGenerator {
* Whether the type is in a request / response
*/
context?: "request" | "response"
/**
* Whether to save object schemas. Useful when only getting schemas to update.
*/
saveSchema?: boolean
}): OpenApiSchema {
if (level > this.MAX_LEVEL) {
return {}
@@ -1354,6 +1374,7 @@ class OasKindGenerator extends FunctionKindGenerator {
parentName: title || descriptionOptions?.parentName,
}
: undefined,
saveSchema,
...rest,
}),
}
@@ -1384,6 +1405,7 @@ class OasKindGenerator extends FunctionKindGenerator {
level,
title,
descriptionOptions,
saveSchema,
...rest,
})
)
@@ -1407,6 +1429,7 @@ class OasKindGenerator extends FunctionKindGenerator {
level,
title,
descriptionOptions,
saveSchema,
...rest,
})
})
@@ -1437,6 +1460,7 @@ class OasKindGenerator extends FunctionKindGenerator {
level,
descriptionOptions,
allowedChildren: pickedProperties,
saveSchema,
...rest,
})
case typeAsString.startsWith("Omit"):
@@ -1458,6 +1482,7 @@ class OasKindGenerator extends FunctionKindGenerator {
level,
descriptionOptions,
disallowedChildren: omitProperties,
saveSchema,
...rest,
})
case typeAsString.startsWith("Partial"):
@@ -1475,6 +1500,7 @@ class OasKindGenerator extends FunctionKindGenerator {
descriptionOptions,
disallowedChildren,
allowedChildren,
saveSchema,
...rest,
})
@@ -1485,13 +1511,29 @@ class OasKindGenerator extends FunctionKindGenerator {
case itemType.isClassOrInterface() ||
itemType.isTypeParameter() ||
(itemType as ts.Type).flags === ts.TypeFlags.Object:
const properties: Record<string, OpenApiSchema> = {}
const properties: Record<
string,
OpenApiSchema | OpenAPIV3.ReferenceObject
> = {}
const requiredProperties: string[] = []
const baseType = itemType.getBaseTypes()?.[0]
const isDeleteResponse =
baseType?.aliasSymbol?.getEscapedName() === "DeleteResponse"
const objSchema: OpenApiSchema = {
type: "object",
description,
"x-schemaName":
itemType.isClassOrInterface() ||
itemType.isTypeParameter() ||
(isZodObject(itemType) && zodObjectTypeName)
? this.oasSchemaHelper.normalizeSchemaName(typeAsString)
: undefined,
// this is changed later
required: undefined,
}
if (level + 1 <= this.MAX_LEVEL) {
itemType.getProperties().forEach((property) => {
if (
@@ -1504,6 +1546,41 @@ class OasKindGenerator extends FunctionKindGenerator {
requiredProperties.push(property.name)
}
const propertyType = this.checker.getTypeOfSymbol(property)
// if property's type is same as parent's property,
// create a reference to the parent
const arrHasParentType =
this.checker.isArrayType(propertyType) &&
this.areTypesEqual(
itemType,
this.checker.getTypeArguments(
propertyType as ts.TypeReference
)[0]
)
const isParentType = this.areTypesEqual(itemType, propertyType)
if (isParentType && objSchema["x-schemaName"]) {
properties[property.name] = {
$ref: this.oasSchemaHelper.constructSchemaReference(
objSchema["x-schemaName"]
),
}
return
} else if (arrHasParentType && objSchema["x-schemaName"]) {
properties[property.name] = {
type: "array",
description,
items: {
$ref: this.oasSchemaHelper.constructSchemaReference(
objSchema["x-schemaName"]
),
},
} as OpenAPIV3.ArraySchemaObject
return
}
properties[property.name] = this.typeToSchema({
itemType: propertyType,
level: level + 1,
@@ -1513,38 +1590,34 @@ class OasKindGenerator extends FunctionKindGenerator {
typeStr: property.name,
parentName: title || descriptionOptions?.parentName,
},
saveSchema,
...rest,
})
if (isDeleteResponse && property.name === "object") {
if (
isDeleteResponse &&
property.name === "object" &&
!this.oasSchemaHelper.isRefObject(properties[property.name])
) {
const schemaProperty = properties[property.name] as OpenApiSchema
// try to retrieve default from `DeleteResponse`'s type argument
const deleteTypeArg = baseType.aliasTypeArguments?.[0]
properties[property.name].default =
schemaProperty.default =
deleteTypeArg && "value" in deleteTypeArg
? (deleteTypeArg.value as string)
: properties[property.name].default
: schemaProperty.default
}
})
}
const objSchema: OpenApiSchema = {
type: "object",
description,
"x-schemaName":
itemType.isClassOrInterface() ||
itemType.isTypeParameter() ||
(isZodObject(itemType) && zodObjectTypeName)
? this.oasSchemaHelper.normalizeSchemaName(typeAsString)
: undefined,
required:
requiredProperties.length > 0 ? requiredProperties : undefined,
}
if (Object.values(properties).length) {
objSchema.properties = properties
}
if (objSchema["x-schemaName"]) {
objSchema.required =
requiredProperties.length > 0 ? requiredProperties : undefined
if (saveSchema && objSchema["x-schemaName"]) {
// add object to schemas to be created
// if necessary
this.oasSchemaHelper.namedSchemaToReference(objSchema)
@@ -1947,8 +2020,6 @@ class OasKindGenerator extends FunctionKindGenerator {
}) || oldSchemaObj.items
}
// update schema
if (
oldSchemaObj!.description !== newSchemaObj?.description &&
oldSchemaObj!.description === SUMMARY_PLACEHOLDER
@@ -2184,6 +2255,10 @@ class OasKindGenerator extends FunctionKindGenerator {
return true
})
}
private areTypesEqual(type1: ts.Type, type2: ts.Type): boolean {
return "id" in type1 && "id" in type2 && type1.id === type2.id
}
}
export default OasKindGenerator

View File

@@ -17,9 +17,9 @@ const customOptions: Record<string, Partial<TypeDocOptions>> = {
name: "core-flows",
plugin: ["typedoc-plugin-workflows"],
enableWorkflowsPlugins: true,
enableNamespaceGenerator: true,
enablePathNamespaceGenerator: true,
// @ts-expect-error there's a typing issue in typedoc
generateNamespaces: getCoreFlowNamespaces(),
generatePathNamespaces: getCoreFlowNamespaces(),
}),
"auth-provider": getOptions({
entryPointPath: "packages/core/utils/src/auth/abstract-auth-provider.ts",
@@ -35,6 +35,7 @@ const customOptions: Record<string, Partial<TypeDocOptions>> = {
],
tsConfigName: "utils.json",
name: "dml",
generateCustomNamespaces: true,
}),
file: getOptions({
entryPointPath: "packages/core/utils/src/file/abstract-file-provider.ts",

View File

@@ -37,7 +37,7 @@ The following options are useful for linting:
### Generate Namespace Plugin
If the `generateNamespaces` option is enabled, Namespaces are created from reflections having the `@customNamespace` tag. It also attaches categories (using the `@category` tag) of the same reflection to its generated parent namespace.
If the `generatePathNamespaces` option is enabled, Namespaces are created from reflections having the `@customNamespace` tag. It also attaches categories (using the `@category` tag) of the same reflection to its generated parent namespace.
It also accepts the following options:

View File

@@ -0,0 +1,279 @@
import {
Application,
Comment,
CommentDisplayPart,
CommentTag,
Context,
Converter,
DeclarationReflection,
ParameterType,
Reflection,
ReflectionCategory,
ReflectionKind,
} from "typedoc"
type PluginOptions = {
generatePathNamespaces: boolean
parentNamespace: string
namePrefix: string
}
export class GenerateCustomNamespacePlugin {
private options?: PluginOptions
private app: Application
private parentNamespace?: DeclarationReflection
private currentNamespaceHeirarchy: DeclarationReflection[]
private currentContext?: Context
private scannedComments = false
constructor(app: Application) {
this.app = app
this.currentNamespaceHeirarchy = []
this.declareOptions()
this.app.converter.on(
Converter.EVENT_RESOLVE,
this.handleCreateDeclarationEvent.bind(this)
)
this.app.converter.on(
Converter.EVENT_CREATE_DECLARATION,
this.scanComments.bind(this)
)
}
declareOptions() {
this.app.options.addDeclaration({
name: "generateCustomNamespaces",
type: ParameterType.Boolean,
defaultValue: false,
help: "Whether to enable conversion of categories to namespaces.",
})
this.app.options.addDeclaration({
name: "customParentNamespace",
type: ParameterType.String,
defaultValue: "",
help: "Optionally specify a parent namespace to place all generated namespaces in.",
})
this.app.options.addDeclaration({
name: "customNamespaceNamePrefix",
type: ParameterType.String,
defaultValue: "",
help: "Optionally specify a name prefix for all namespaces.",
})
}
readOptions() {
if (this.options) {
return
}
this.options = {
generatePathNamespaces: this.app.options.getValue(
"generateCustomNamespaces"
),
parentNamespace: this.app.options.getValue("customParentNamespace"),
namePrefix: this.app.options.getValue("customNamespaceNamePrefix"),
}
}
loadNamespace(namespaceName: string): DeclarationReflection {
const formattedName = this.formatName(namespaceName)
return this.currentContext?.project
.getReflectionsByKind(ReflectionKind.Namespace)
.find(
(m) =>
m.name === formattedName &&
(!this.currentNamespaceHeirarchy.length ||
m.parent?.id ===
this.currentNamespaceHeirarchy[
this.currentNamespaceHeirarchy.length - 1
].id)
) as DeclarationReflection
}
createNamespace(namespaceName: string): DeclarationReflection | undefined {
if (!this.currentContext) {
return
}
const formattedName = this.formatName(namespaceName)
const namespace = this.currentContext?.createDeclarationReflection(
ReflectionKind.Namespace,
void 0,
void 0,
formattedName
)
namespace.children = []
return namespace
}
formatName(namespaceName: string): string {
return `${this.options?.namePrefix}${namespaceName}`
}
generateNamespaceFromTag({
tag,
summary,
}: {
tag: CommentTag
reflection?: DeclarationReflection
summary?: CommentDisplayPart[]
}) {
const categoryHeirarchy = tag.content[0].text.split(".")
categoryHeirarchy.forEach((cat, index) => {
// check whether a namespace exists with the category name.
let namespace = this.loadNamespace(cat)
if (!namespace) {
// add a namespace for this category
namespace = this.createNamespace(cat) || namespace
namespace.comment = new Comment()
if (this.currentNamespaceHeirarchy.length) {
namespace.comment.modifierTags.add("@namespaceMember")
}
if (summary && index === categoryHeirarchy.length - 1) {
namespace.comment.summary = summary
}
}
this.currentContext =
this.currentContext?.withScope(namespace) || this.currentContext
this.currentNamespaceHeirarchy.push(namespace)
})
}
/**
* create categories in the last namespace if the
* reflection has a category
*/
attachCategories(
reflection: DeclarationReflection,
comments: Comment | undefined
) {
if (!this.currentNamespaceHeirarchy.length) {
return
}
const parentNamespace =
this.currentNamespaceHeirarchy[this.currentNamespaceHeirarchy.length - 1]
comments?.blockTags
.filter((tag) => tag.tag === "@category")
.forEach((tag) => {
const categoryName = tag.content[0].text
if (!parentNamespace.categories) {
parentNamespace.categories = []
}
let category = parentNamespace.categories.find(
(category) => category.title === categoryName
)
if (!category) {
category = new ReflectionCategory(categoryName)
parentNamespace.categories.push(category)
}
category.children.push(reflection)
})
}
handleCreateDeclarationEvent(context: Context, reflection: Reflection) {
if (!(reflection instanceof DeclarationReflection)) {
return
}
this.readOptions()
if (this.options?.parentNamespace && !this.parentNamespace) {
this.parentNamespace =
this.loadNamespace(this.options.parentNamespace) ||
this.createNamespace(this.options.parentNamespace)
}
this.currentNamespaceHeirarchy = []
if (this.parentNamespace) {
this.currentNamespaceHeirarchy.push(this.parentNamespace)
}
this.currentContext = context
const comments = this.getReflectionComments(reflection)
comments?.blockTags
.filter((tag) => tag.tag === "@customNamespace")
.forEach((tag) => {
this.generateNamespaceFromTag({
tag,
})
if (
reflection.parent instanceof DeclarationReflection ||
reflection.parent?.isProject()
) {
reflection.parent.children = reflection.parent.children?.filter(
(child) => child.id !== reflection.id
)
}
this.currentContext?.addChild(reflection)
})
comments?.removeTags("@customNamespace")
this.attachCategories(reflection, comments)
this.currentContext = undefined
this.currentNamespaceHeirarchy = []
}
/**
* Scan all source files for `@customNamespace` tag to generate namespaces
* This is mainly helpful to pull summaries of the namespaces.
*/
scanComments(context: Context) {
if (this.scannedComments) {
return
}
this.currentContext = context
const fileNames = context.program.getRootFileNames()
fileNames.forEach((fileName) => {
const sourceFile = context.program.getSourceFile(fileName)
if (!sourceFile) {
return
}
const comments = context.getFileComment(sourceFile)
comments?.blockTags
.filter((tag) => tag.tag === "@customNamespace")
.forEach((tag) => {
this.generateNamespaceFromTag({ tag, summary: comments.summary })
if (this.currentNamespaceHeirarchy.length) {
// add comments of the file to the last created namespace
this.currentNamespaceHeirarchy[
this.currentNamespaceHeirarchy.length - 1
].comment = comments
this.currentNamespaceHeirarchy[
this.currentNamespaceHeirarchy.length - 1
].comment!.blockTags = this.currentNamespaceHeirarchy[
this.currentNamespaceHeirarchy.length - 1
].comment!.blockTags.filter((tag) => tag.tag !== "@customNamespace")
}
// reset values
this.currentNamespaceHeirarchy = []
this.currentContext = context
})
})
this.scannedComments = true
}
getReflectionComments(
reflection: DeclarationReflection
): Comment | undefined {
if (reflection.comment) {
return reflection.comment
}
// try to retrieve comment from signature
if (!reflection.signatures?.length) {
return
}
return reflection.signatures.find((signature) => signature.comment)?.comment
}
// for debugging
printCurrentHeirarchy() {
return this.currentNamespaceHeirarchy.map((heirarchy) => heirarchy.name)
}
}

View File

@@ -12,13 +12,13 @@ import { NamespaceGenerateDetails } from "types"
export function load(app: Application) {
app.options.addDeclaration({
name: "enableNamespaceGenerator",
name: "enablePathNamespaceGenerator",
type: ParameterType.Boolean,
defaultValue: false,
help: "Whether to enable the namespace generator plugin.",
})
app.options.addDeclaration({
name: "generateNamespaces",
name: "generatePathNamespaces",
type: ParameterType.Mixed,
defaultValue: [],
help: "The namespaces to generate.",
@@ -27,15 +27,15 @@ export function load(app: Application) {
const generatedNamespaces: Map<string, DeclarationReflection> = new Map()
app.converter.on(Converter.EVENT_BEGIN, (context) => {
if (!app.options.getValue("enableNamespaceGenerator")) {
if (!app.options.getValue("enablePathNamespaceGenerator")) {
return
}
const namespaces = app.options.getValue(
"generateNamespaces"
"generatePathNamespaces"
) as unknown as NamespaceGenerateDetails[]
const generateNamespaces = (ns: NamespaceGenerateDetails[]) => {
const generatePathNamespaces = (ns: NamespaceGenerateDetails[]) => {
const createdNamespaces: DeclarationReflection[] = []
ns.forEach((namespace) => {
const genNamespace = createNamespace(context, namespace)
@@ -43,7 +43,7 @@ export function load(app: Application) {
generatedNamespaces.set(namespace.pathPattern, genNamespace)
if (namespace.children) {
generateNamespaces(namespace.children).forEach((child) =>
generatePathNamespaces(namespace.children).forEach((child) =>
genNamespace.addChild(child)
)
}
@@ -54,13 +54,13 @@ export function load(app: Application) {
return createdNamespaces
}
generateNamespaces(namespaces)
generatePathNamespaces(namespaces)
})
app.converter.on(
Converter.EVENT_CREATE_DECLARATION,
(context, reflection) => {
if (!app.options.getValue("enableNamespaceGenerator")) {
if (!app.options.getValue("enablePathNamespaceGenerator")) {
return
}
@@ -72,7 +72,7 @@ export function load(app: Application) {
}
const namespaces = app.options.getValue(
"generateNamespaces"
"generatePathNamespaces"
) as unknown as NamespaceGenerateDetails[]
const findNamespace = (

View File

@@ -7,11 +7,12 @@ import { load as eslintExamplePlugin } from "./eslint-example"
import { load as signatureModifierPlugin } from "./signature-modifier"
import { MermaidDiagramGenerator } from "./mermaid-diagram-generator"
import { load as parentIgnorePlugin } from "./parent-ignore"
import { load as generateNamespacePlugin } from "./generate-namespace"
import { load as generateNamespacePlugin } from "./generate-path-namespaces"
import { DmlRelationsResolver } from "./dml-relations-resolver"
import { load as dmlTypesNormalizer } from "./dml-types-normalizer"
import { MermaidDiagramDMLGenerator } from "./mermaid-diagram-dml-generator"
import { load as dmlJsonParser } from "./dml-json-parser"
import { GenerateCustomNamespacePlugin } from "./generate-custom-namespaces"
export function load(app: Application) {
resolveReferencesPluginLoad(app)
@@ -28,4 +29,5 @@ export function load(app: Application) {
new MermaidDiagramGenerator(app)
new DmlRelationsResolver(app)
new MermaidDiagramDMLGenerator(app)
new GenerateCustomNamespacePlugin(app)
}

View File

@@ -251,14 +251,27 @@ export declare module "typedoc" {
*/
enableWorkflowsPlugins: boolean
/**
* Whether to enable the namespace generator plugin.
* Whether to enable the namespace generator plugin for paths.
* @defaultValue false
*/
enableNamespaceGenerator: boolean
enablePathNamespaceGenerator: boolean
/**
* The namespaces to generate.
* The namespaces to generate for paths.
*/
generateNamespaces: NamespaceGenerateDetails[]
generatePathNamespaces: NamespaceGenerateDetails[]
/**
* Whether to enable the namespace generator plugin for `@customNamespaces` usage.
* @defaultValue false
*/
generateCustomNamespaces: boolean
/**
* Optionally specify a parent namespace to place all generated custom namespaces in.
*/
customParentNamespace: string
/**
* Optionally specify a name prefix for all custom namespaces.
*/
customNamespaceNamePrefix: string
}
}