diff --git a/www/utils/packages/docblock-generator/src/classes/examples/oas.ts b/www/utils/packages/docblock-generator/src/classes/examples/oas.ts index 3bab6ccef4..0a7464e0f1 100644 --- a/www/utils/packages/docblock-generator/src/classes/examples/oas.ts +++ b/www/utils/packages/docblock-generator/src/classes/examples/oas.ts @@ -1,8 +1,9 @@ import { faker } from "@faker-js/faker" import { OpenAPIV3 } from "openapi-types" -import { API_ROUTE_PARAM_REGEX, OasArea } from "../kinds/oas.js" +import { OasArea } from "../kinds/oas.js" import { CodeSample } from "../../types/index.js" import { capitalize, kebabToCamel, wordsToCamel, wordsToKebab } from "utils" +import { API_ROUTE_PARAM_REGEX } from "../../constants.js" type CodeSampleData = Omit diff --git a/www/utils/packages/docblock-generator/src/classes/helpers/knowledge-base-factory.ts b/www/utils/packages/docblock-generator/src/classes/helpers/knowledge-base-factory.ts index 17ee414e2f..e3a98342fa 100644 --- a/www/utils/packages/docblock-generator/src/classes/helpers/knowledge-base-factory.ts +++ b/www/utils/packages/docblock-generator/src/classes/helpers/knowledge-base-factory.ts @@ -1,7 +1,18 @@ import ts from "typescript" -import { DOCBLOCK_DOUBLE_LINES, DOCBLOCK_NEW_LINE } from "../../constants.js" +import { + API_ROUTE_PARAM_REGEX, + DOCBLOCK_DOUBLE_LINES, + DOCBLOCK_NEW_LINE, + SUMMARY_PLACEHOLDER, +} from "../../constants.js" import pluralize from "pluralize" -import { camelToTitle, camelToWords, snakeToWords } from "utils" +import { + camelToTitle, + camelToWords, + kebabToTitle, + snakeToWords, + wordsToKebab, +} from "utils" import { normalizeName } from "../../utils/str-formatting.js" type TemplateOptions = { @@ -537,7 +548,7 @@ class KnowledgeBaseFactory { * * @returns {string | undefined} The matching knowledgebase template, if found. */ - tryToGetOasDescription({ + tryToGetOasSchemaDescription({ str, ...options }: RetrieveOptions): string | undefined { @@ -548,6 +559,125 @@ class KnowledgeBaseFactory { knowledgeBase: this.oasDescriptionKnowledgeBase, }) } + /** + * Retrieve the summary and description of the OAS. + * + * @param param0 - The OAS operation's details. + * @returns The summary and description. + */ + tryToGetOasMethodSummaryAndDescription({ + oasPath, + httpMethod, + tag, + }: { + /** + * The OAS path. + */ + oasPath: string + /** + * The HTTP method name. + */ + httpMethod: string + /** + * The OAS tag name. + */ + tag: string + }): { + /** + * The OAS's summary + */ + summary: string + /** + * The OAS's description. + */ + description: string + } { + // reset regex manually + API_ROUTE_PARAM_REGEX.lastIndex = 0 + const result = { + summary: SUMMARY_PLACEHOLDER, + description: SUMMARY_PLACEHOLDER, + } + // retrieve different variations of the tag to include in the summary/description + const lowerTag = tag.toLowerCase() + const singularLowerTag = pluralize.singular(lowerTag) + const singularTag = pluralize.singular(tag) + + // check if the OAS operation is performed on a single entity or + // general entities. If the operation has a path parameter, then it's + // considered for a single entity. + const isForSingleEntity = API_ROUTE_PARAM_REGEX.test(oasPath) + + if (isForSingleEntity) { + // Check whether the OAS operation is applied on a different entity. + // If the OAS path ends with /batch or a different entity + // name than the tag name, then it's performed on an entity other than the + // main entity (the one indicated by the tag), so the summary/description vary + // slightly. + const splitOasPath = oasPath + .replaceAll(API_ROUTE_PARAM_REGEX, "") + .replace(/\/(batch)*$/, "") + .split("/") + const isBulk = oasPath.endsWith("/batch") + const isOperationOnDifferentEntity = + wordsToKebab(tag) !== splitOasPath[splitOasPath.length - 1] + + if (isBulk || isOperationOnDifferentEntity) { + // if the operation is a bulk operation and it ends with a path parameter (after removing the `/batch` part) + // then the tag name is the targeted entity. Else, it's the last part of the OAS path (after removing the `/batch` part). + const endingEntityName = + isBulk && + API_ROUTE_PARAM_REGEX.test(splitOasPath[splitOasPath.length - 1]) + ? tag + : kebabToTitle(splitOasPath[splitOasPath.length - 1]) + // retrieve different formatted versions of the entity name for the summary/description + const pluralEndingEntityName = pluralize.plural(endingEntityName) + const lowerEndingEntityName = pluralEndingEntityName.toLowerCase() + const singularLowerEndingEntityName = + pluralize.singular(endingEntityName) + + // set the summary/description based on the HTTP method + if (httpMethod === "get") { + result.summary = `List ${pluralEndingEntityName}` + result.description = `Retrieve a list of ${lowerEndingEntityName} in a ${singularLowerTag}. The ${lowerEndingEntityName} can be filtered by fields like FILTER FIELDS. The ${lowerEndingEntityName} can also be paginated.` + } else if (httpMethod === "post") { + result.summary = `Add ${pluralEndingEntityName} to ${singularTag}` + result.description = `Add a list of ${lowerEndingEntityName} to a ${singularLowerTag}.` + } else { + result.summary = `Remove ${pluralEndingEntityName} from ${singularTag}` + result.description = `Remove a list of ${lowerEndingEntityName} from a ${singularLowerTag}. This doesn't delete the ${singularLowerEndingEntityName}, only the association between the ${singularLowerEndingEntityName} and the ${singularLowerTag}.` + } + } else { + // the OAS operation is applied on a single entity that is the main entity (denoted by the tag). + // retrieve the summary/description based on the HTTP method. + if (httpMethod === "get") { + result.summary = `Get a ${singularTag}` + result.description = `Retrieve a ${singularLowerTag} by its ID. You can expand the ${singularLowerTag}'s relations or select the fields that should be returned.` + } else if (httpMethod === "post") { + result.summary = `Update a ${singularTag}` + result.description = `Update a ${singularLowerTag}'s details.` + } else { + result.summary = `Delete a ${singularTag}` + result.description = `Delete a ${singularLowerTag}.` + } + } + } else { + // the OAS operation is applied on all entities of the tag in general. + // retrieve the summary/description based on the HTTP method. + if (httpMethod === "get") { + result.summary = `List ${tag}` + result.description = `Retrieve a list of ${lowerTag}. The ${lowerTag} can be filtered by fields such as \`id\`. The ${lowerTag} can also be sorted or paginated.` + } else if (httpMethod === "post") { + result.summary = `Create ${singularTag}` + result.description = `Create a ${singularLowerTag}.` + } else { + result.summary = `Delete ${tag}` + result.description = `Delete ${tag}` + } + } + + return result + } } export default KnowledgeBaseFactory diff --git a/www/utils/packages/docblock-generator/src/classes/helpers/oas-schema.ts b/www/utils/packages/docblock-generator/src/classes/helpers/oas-schema.ts index 9f263b8758..feadb4d4c9 100644 --- a/www/utils/packages/docblock-generator/src/classes/helpers/oas-schema.ts +++ b/www/utils/packages/docblock-generator/src/classes/helpers/oas-schema.ts @@ -187,11 +187,11 @@ class OasSchemaHelper { * @param name - The schema's name * @returns The schema's file name */ - getSchemaFileName(name: string): string { + getSchemaFileName(name: string, shouldNormalizeName = true): string { return join( this.baseOutputPath, "schemas", - `${this.normalizeSchemaName(name)}.ts` + `${shouldNormalizeName ? this.normalizeSchemaName(name) : name}.ts` ) } @@ -202,8 +202,13 @@ class OasSchemaHelper { * @param name - The schema's name. * @returns The parsed schema, if found. */ - getSchemaByName(name: string): ParsedSchema | undefined { - const schemaName = this.normalizeSchemaName(name) + getSchemaByName( + name: string, + shouldNormalizeName = true + ): ParsedSchema | undefined { + const schemaName = shouldNormalizeName + ? this.normalizeSchemaName(name) + : name // check if it already exists in the schemas map if (this.schemas.has(schemaName)) { return { @@ -211,7 +216,7 @@ class OasSchemaHelper { schemaPrefix: `@schema ${schemaName}`, } } - const schemaFile = this.getSchemaFileName(schemaName) + const schemaFile = this.getSchemaFileName(schemaName, shouldNormalizeName) const schemaFileContent = ts.sys.readFile(schemaFile) if (!schemaFileContent) { @@ -267,7 +272,7 @@ class OasSchemaHelper { return name .replace("DTO", "") .replace(this.schemaRefPrefix, "") - .replace(/Type$/, "") + .replace(/(? { public name = "default" protected allowedKinds: ts.SyntaxKind[] protected checker: ts.TypeChecker - protected defaultSummary = "{summary}" protected knowledgeBaseFactory: KnowledgeBaseFactory protected generatorEventManager: GeneratorEventManager protected options: Pick @@ -186,7 +186,7 @@ class DefaultKindGenerator { summary = this.getTypeDocBlock(nodeType, knowledgeBaseOptions) } - return summary.length > 0 ? summary : this.defaultSummary + return summary.length > 0 ? summary : SUMMARY_PLACEHOLDER } /** @@ -234,7 +234,7 @@ class DefaultKindGenerator { } // do some formatting if the encapsulating type is an array - return `The list of ${capitalize(typeArgumentDoc) || this.defaultSummary}` + return `The list of ${capitalize(typeArgumentDoc) || SUMMARY_PLACEHOLDER}` } return ( @@ -360,7 +360,7 @@ class DefaultKindGenerator { } if (options.addDefaultSummary) { - tags.add(this.defaultSummary) + tags.add(SUMMARY_PLACEHOLDER) } // check for private or protected modifiers diff --git a/www/utils/packages/docblock-generator/src/classes/kinds/function.ts b/www/utils/packages/docblock-generator/src/classes/kinds/function.ts index 29f80d242d..60aee9fe62 100644 --- a/www/utils/packages/docblock-generator/src/classes/kinds/function.ts +++ b/www/utils/packages/docblock-generator/src/classes/kinds/function.ts @@ -5,6 +5,7 @@ import { DOCBLOCK_END_LINE, DOCBLOCK_START, DOCBLOCK_DOUBLE_LINES, + SUMMARY_PLACEHOLDER, } from "../../constants.js" import getSymbol from "../../utils/get-symbol.js" import AiGenerator from "../helpers/ai-generator.js" @@ -327,7 +328,7 @@ class FunctionKindGenerator extends DefaultKindGenerator // add returns const possibleReturnSummary = !this.hasReturnData(returnTypeStr) - ? `Resolves when ${this.defaultSummary}` + ? `Resolves when ${SUMMARY_PLACEHOLDER}` : this.getNodeSummary({ node: actualNode, nodeType, diff --git a/www/utils/packages/docblock-generator/src/classes/kinds/oas.ts b/www/utils/packages/docblock-generator/src/classes/kinds/oas.ts index 3c98a4abaa..81f64d61f5 100644 --- a/www/utils/packages/docblock-generator/src/classes/kinds/oas.ts +++ b/www/utils/packages/docblock-generator/src/classes/kinds/oas.ts @@ -1,11 +1,10 @@ import { readFileSync, writeFileSync } from "fs" import { OpenAPIV3 } from "openapi-types" import { basename, join } from "path" -import pluralize from "pluralize" import ts, { SyntaxKind } from "typescript" -import { capitalize, kebabToTitle, wordsToKebab } from "utils" +import { capitalize, kebabToTitle } from "utils" import { parse, stringify } from "yaml" -import { DEFAULT_OAS_RESPONSES } from "../../constants.js" +import { DEFAULT_OAS_RESPONSES, SUMMARY_PLACEHOLDER } from "../../constants.js" import { OpenApiDocument, OpenApiOperation, @@ -18,7 +17,7 @@ import isZodObject from "../../utils/is-zod-object.js" import parseOas, { ExistingOas } from "../../utils/parse-oas.js" import OasExamplesGenerator from "../examples/oas.js" import { GeneratorEvent } from "../helpers/generator-event-manager.js" -import OasSchemaHelper, { ParsedSchema } from "../helpers/oas-schema.js" +import OasSchemaHelper from "../helpers/oas-schema.js" import SchemaFactory from "../helpers/schema-factory.js" import { GeneratorOptions, GetDocBlockOptions } from "./default.js" import FunctionKindGenerator, { @@ -26,8 +25,8 @@ import FunctionKindGenerator, { FunctionOrVariableNode, VariableNode, } from "./function.js" +import { API_ROUTE_PARAM_REGEX } from "../helpers/../../constants.js" -export const API_ROUTE_PARAM_REGEX = /\[(.+?)\]/g const RES_STATUS_REGEX = /^res[\s\S]*\.status\((\d+)\)/ type SchemaDescriptionOptions = { @@ -50,11 +49,6 @@ class OasKindGenerator extends FunctionKindGenerator { public name = "oas" protected allowedKinds: SyntaxKind[] = [ts.SyntaxKind.FunctionDeclaration] private MAX_LEVEL = 4 - // we can't use `{summary}` because it causes an MDX error - // when we finally render the summary. We can alternatively - // use `\{summary\}` but it wouldn't look pretty in the OAS, - // so doing this for now. - protected defaultSummary = "SUMMARY" /** * This map collects tags of all the generated OAS, then, once the generation process finishes, @@ -228,11 +222,12 @@ class OasKindGenerator extends FunctionKindGenerator { const { isAdminAuthenticated, isStoreAuthenticated, isAuthenticated } = this.getAuthenticationDetails(node, oasPath) const tagName = this.getTagName(splitOasPath) - const { summary, description } = this.getSummaryAndDescription({ - oasPath, - httpMethod: methodName, - tag: tagName || "", - }) + const { summary, description } = + this.knowledgeBaseFactory.tryToGetOasMethodSummaryAndDescription({ + oasPath, + httpMethod: methodName, + tag: tagName || "", + }) // construct oas const oas: OpenApiOperation = { @@ -405,15 +400,16 @@ class OasKindGenerator extends FunctionKindGenerator { // update summary and description either if they're empty or default summary const shouldUpdateSummary = - !oas.summary || oas.summary === this.defaultSummary + !oas.summary || oas.summary === SUMMARY_PLACEHOLDER const shouldUpdateDescription = - !oas.description || oas.description === this.defaultSummary + !oas.description || oas.description === SUMMARY_PLACEHOLDER if (shouldUpdateSummary || shouldUpdateDescription) { - const { summary, description } = this.getSummaryAndDescription({ - oasPath, - httpMethod: methodName, - tag: tagName || "", - }) + const { summary, description } = + this.knowledgeBaseFactory.tryToGetOasMethodSummaryAndDescription({ + oasPath, + httpMethod: methodName, + tag: tagName || "", + }) if (shouldUpdateSummary) { oas.summary = summary @@ -775,126 +771,6 @@ class OasKindGenerator extends FunctionKindGenerator { return str } - /** - * Retrieve the summary and description of the OAS. - * - * @param param0 - The OAS operation's details. - * @returns The summary and description. - */ - getSummaryAndDescription({ - oasPath, - httpMethod, - tag, - }: { - /** - * The OAS path. - */ - oasPath: string - /** - * The HTTP method name. - */ - httpMethod: string - /** - * The OAS tag name. - */ - tag: string - }): { - /** - * The OAS's summary - */ - summary: string - /** - * The OAS's description. - */ - description: string - } { - // reset regex manually - API_ROUTE_PARAM_REGEX.lastIndex = 0 - const result = { - summary: this.defaultSummary, - description: this.defaultSummary, - } - // retrieve different variations of the tag to include in the summary/description - const lowerTag = tag.toLowerCase() - const singularLowerTag = pluralize.singular(lowerTag) - const singularTag = pluralize.singular(tag) - - // check if the OAS operation is performed on a single entity or - // general entities. If the operation has a path parameter, then it's - // considered for a single entity. - const isForSingleEntity = API_ROUTE_PARAM_REGEX.test(oasPath) - - if (isForSingleEntity) { - // Check whether the OAS operation is applied on a different entity. - // If the OAS path ends with /batch or a different entity - // name than the tag name, then it's performed on an entity other than the - // main entity (the one indicated by the tag), so the summary/description vary - // slightly. - const splitOasPath = oasPath - .replaceAll(API_ROUTE_PARAM_REGEX, "") - .replace(/\/(batch)*$/, "") - .split("/") - const isBulk = oasPath.endsWith("/batch") - const isOperationOnDifferentEntity = - wordsToKebab(tag) !== splitOasPath[splitOasPath.length - 1] - - if (isBulk || isOperationOnDifferentEntity) { - // if the operation is a bulk operation and it ends with a path parameter (after removing the `/batch` part) - // then the tag name is the targeted entity. Else, it's the last part of the OAS path (after removing the `/batch` part). - const endingEntityName = - isBulk && - API_ROUTE_PARAM_REGEX.test(splitOasPath[splitOasPath.length - 1]) - ? tag - : kebabToTitle(splitOasPath[splitOasPath.length - 1]) - // retrieve different formatted versions of the entity name for the summary/description - const pluralEndingEntityName = pluralize.plural(endingEntityName) - const lowerEndingEntityName = pluralEndingEntityName.toLowerCase() - const singularLowerEndingEntityName = - pluralize.singular(endingEntityName) - - // set the summary/description based on the HTTP method - if (httpMethod === "get") { - result.summary = `List ${pluralEndingEntityName}` - result.description = `Retrieve a list of ${lowerEndingEntityName} in a ${singularLowerTag}. The ${lowerEndingEntityName} can be filtered by fields like FILTER FIELDS. The ${lowerEndingEntityName} can also be paginated.` - } else if (httpMethod === "post") { - result.summary = `Add ${pluralEndingEntityName} to ${singularTag}` - result.description = `Add a list of ${lowerEndingEntityName} to a ${singularLowerTag}.` - } else { - result.summary = `Remove ${pluralEndingEntityName} from ${singularTag}` - result.description = `Remove a list of ${lowerEndingEntityName} from a ${singularLowerTag}. This doesn't delete the ${singularLowerEndingEntityName}, only the association between the ${singularLowerEndingEntityName} and the ${singularLowerTag}.` - } - } else { - // the OAS operation is applied on a single entity that is the main entity (denoted by the tag). - // retrieve the summary/description based on the HTTP method. - if (httpMethod === "get") { - result.summary = `Get a ${singularTag}` - result.description = `Retrieve a ${singularLowerTag} by its ID. You can expand the ${singularLowerTag}'s relations or select the fields that should be returned.` - } else if (httpMethod === "post") { - result.summary = `Update a ${singularTag}` - result.description = `Update a ${singularLowerTag}'s details.` - } else { - result.summary = `Delete a ${singularTag}` - result.description = `Delete a ${singularLowerTag}.` - } - } - } else { - // the OAS operation is applied on all entities of the tag in general. - // retrieve the summary/description based on the HTTP method. - if (httpMethod === "get") { - result.summary = `List ${tag}` - result.description = `Retrieve a list of ${lowerTag}. The ${lowerTag} can be filtered by fields such as \`id\`. The ${lowerTag} can also be sorted or paginated.` - } else if (httpMethod === "post") { - result.summary = `Create ${singularTag}` - result.description = `Create a ${singularLowerTag}.` - } else { - result.summary = `Delete ${tag}` - result.description = `Delete ${tag}` - } - } - - return result - } - /** * Retrieve the security details of an OAS operation. * @@ -1284,7 +1160,7 @@ class OasKindGenerator extends FunctionKindGenerator { ) : title ? this.getSchemaDescription({ typeStr: title, nodeType: itemType }) - : this.defaultSummary + : SUMMARY_PLACEHOLDER const typeAsString = zodObjectTypeName || this.checker.typeToString(itemType) @@ -1493,6 +1369,10 @@ class OasKindGenerator extends FunctionKindGenerator { const properties: Record = {} const requiredProperties: string[] = [] + const baseType = itemType.getBaseTypes()?.[0] + const isDeleteResponse = + baseType?.aliasSymbol?.getEscapedName() === "DeleteResponse" + if (level + 1 <= this.MAX_LEVEL) { itemType.getProperties().forEach((property) => { if ( @@ -1514,6 +1394,15 @@ class OasKindGenerator extends FunctionKindGenerator { parentName: title || descriptionOptions?.parentName, }, }) + + if (isDeleteResponse && property.name === "object") { + // try to retrieve default from `DeleteResponse`'s type argument + const deleteTypeArg = baseType.aliasTypeArguments?.[0] + properties[property.name].default = + deleteTypeArg && "value" in deleteTypeArg + ? (deleteTypeArg.value as string) + : properties[property.name].default + } }) } @@ -1565,12 +1454,12 @@ class OasKindGenerator extends FunctionKindGenerator { // either retrieve the description from the knowledge base or use // the default summary return ( - this.knowledgeBaseFactory.tryToGetOasDescription({ + this.knowledgeBaseFactory.tryToGetOasSchemaDescription({ str: typeStr, templateOptions: { parentName, }, - }) || this.defaultSummary + }) || SUMMARY_PLACEHOLDER ) } @@ -1592,7 +1481,7 @@ class OasKindGenerator extends FunctionKindGenerator { description = this.getSymbolDocBlock(symbol) } - return description.length ? description : this.defaultSummary + return description.length ? description : SUMMARY_PLACEHOLDER } /** @@ -1745,7 +1634,7 @@ class OasKindGenerator extends FunctionKindGenerator { if ( updatedParameter.description !== parameter.description && - parameter.description === this.defaultSummary + parameter.description === SUMMARY_PLACEHOLDER ) { parameter.description = updatedParameter.description } @@ -1775,7 +1664,7 @@ class OasKindGenerator extends FunctionKindGenerator { if ( (updatedParameter.schema as OpenApiSchema).description !== (parameter.schema as OpenApiSchema).description && - (parameter.schema as OpenApiSchema).description === this.defaultSummary + (parameter.schema as OpenApiSchema).description === SUMMARY_PLACEHOLDER ) { ;(parameter.schema as OpenApiSchema).description = ( updatedParameter.schema as OpenApiSchema @@ -1856,10 +1745,10 @@ class OasKindGenerator extends FunctionKindGenerator { if ( oldSchemaObj!.description !== newSchemaObj?.description && - oldSchemaObj!.description === this.defaultSummary + oldSchemaObj!.description === SUMMARY_PLACEHOLDER ) { oldSchemaObj!.description = - newSchemaObj?.description || this.defaultSummary + newSchemaObj?.description || SUMMARY_PLACEHOLDER } oldSchemaObj!.required = newSchemaObj?.required @@ -1971,15 +1860,8 @@ class OasKindGenerator extends FunctionKindGenerator { this.tags.get(area)?.forEach((tag) => { const existingTag = areaYaml.tags!.find((baseTag) => baseTag.name === tag) - // try to retrieve associated schema - let schema: ParsedSchema | undefined - this.oasSchemaHelper.tagNameToSchemaName(tag, area).some((schemaName) => { - schema = this.oasSchemaHelper.getSchemaByName(schemaName) - - if (schema) { - return true - } - }) + const schemaName = this.oasSchemaHelper.tagNameToSchemaName(tag, area) + const schema = this.oasSchemaHelper.getSchemaByName(schemaName, false) const associatedSchema = schema?.schema?.["x-schemaName"] ? { $ref: this.oasSchemaHelper.constructSchemaReference( diff --git a/www/utils/packages/docblock-generator/src/constants.ts b/www/utils/packages/docblock-generator/src/constants.ts index 66d9cb1d25..9a4843da6c 100644 --- a/www/utils/packages/docblock-generator/src/constants.ts +++ b/www/utils/packages/docblock-generator/src/constants.ts @@ -27,3 +27,9 @@ export const DEFAULT_OAS_RESPONSES: { $ref: "#/components/responses/500_error", }, } +export const API_ROUTE_PARAM_REGEX = /\[(.+?)\]/g +// we can't use `{summary}` because it causes an MDX error +// when we finally render the summary. We can alternatively +// use `\{summary\}` but it wouldn't look pretty in the OAS, +// so doing this for now. +export const SUMMARY_PLACEHOLDER = "SUMMARY"