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 bd5ffbb75e..35424d3bf8 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 @@ -8,7 +8,8 @@ import getOasOutputBasePath from "../../utils/get-oas-output-base-path.js" import { parse } from "yaml" import formatOas from "../../utils/format-oas.js" import pluralize from "pluralize" -import { wordsToPascal } from "utils" +import { capitalize, wordsToPascal } from "utils" +import { OasArea } from "../kinds/oas.js" export type ParsedSchema = { schema: OpenApiSchema @@ -200,7 +201,10 @@ class OasSchemaHelper { * @returns The normalized name. */ normalizeSchemaName(name: string): string { - return name.replace("DTO", "").replace(this.schemaRefPrefix, "") + return name + .replace("DTO", "") + .replace(this.schemaRefPrefix, "") + .replace(/Type$/, "") } /** @@ -256,10 +260,11 @@ class OasSchemaHelper { * associated with a tag. * * @param tagName - The name of the tag. - * @returns The possible name of the associated schema. + * @returns The possible names of the associated schema. */ - tagNameToSchemaName(tagName: string): string { - return wordsToPascal(pluralize.singular(tagName)) + tagNameToSchemaName(tagName: string, area: OasArea): string[] { + const mainSchemaName = wordsToPascal(pluralize.singular(tagName)) + return [mainSchemaName, `${capitalize(area)}Create${mainSchemaName}`] } } 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 a05523afb7..cc6fdf20e9 100644 --- a/www/utils/packages/docblock-generator/src/classes/kinds/oas.ts +++ b/www/utils/packages/docblock-generator/src/classes/kinds/oas.ts @@ -19,11 +19,13 @@ import OasExamplesGenerator from "../examples/oas.js" import pluralize from "pluralize" import getOasOutputBasePath from "../../utils/get-oas-output-base-path.js" import parseOas, { ExistingOas } from "../../utils/parse-oas.js" -import OasSchemaHelper from "../helpers/oas-schema.js" +import OasSchemaHelper, { ParsedSchema } from "../helpers/oas-schema.js" import formatOas from "../../utils/format-oas.js" import { DEFAULT_OAS_RESPONSES } from "../../constants.js" import { capitalize, kebabToTitle, wordsToKebab } from "utils" import SchemaFactory from "../helpers/schema-factory.js" +import isZodObject from "../../utils/is-zod-object.js" +import getCorrectZodTypeName from "../../utils/get-correct-zod-type-name.js" export const API_ROUTE_PARAM_REGEX = /\[(.+?)\]/g const RES_STATUS_REGEX = /^res[\s\S]*\.status\((\d+)\)/ @@ -1094,11 +1096,16 @@ class OasKindGenerator extends FunctionKindGenerator { const requestTypeArguments = this.checker.getTypeArguments(requestType) if (requestTypeArguments.length === 1) { + const zodObjectTypeName = getCorrectZodTypeName({ + typeReferenceNode: node.parameters[0].type, + itemType: requestTypeArguments[0], + }) requestSchema = this.typeToSchema({ itemType: requestTypeArguments[0], descriptionOptions: { parentName: tagName, }, + zodObjectTypeName: zodObjectTypeName, }) } } @@ -1167,6 +1174,10 @@ class OasKindGenerator extends FunctionKindGenerator { descriptionOptions: { parentName: tagName, }, + zodObjectTypeName: getCorrectZodTypeName({ + typeReferenceNode: node.parameters[1].type, + itemType: responseTypeArguments[0], + }), }) } } @@ -1187,6 +1198,7 @@ class OasKindGenerator extends FunctionKindGenerator { descriptionOptions, allowedChildren, disallowedChildren, + zodObjectTypeName, }: { /** * The TypeScript type. @@ -1214,6 +1226,12 @@ class OasKindGenerator extends FunctionKindGenerator { * only children not included in this array are added to the schema. */ disallowedChildren?: string[] + /** + * By default, the type name is generated from itemType, which + * doesn't work for types created by Zod. This allows to correct the + * generated type name. + */ + zodObjectTypeName?: string }): OpenApiSchema { if (level > this.MAX_LEVEL) { return {} @@ -1227,7 +1245,8 @@ class OasKindGenerator extends FunctionKindGenerator { : title ? this.getSchemaDescription({ typeStr: title, nodeType: itemType }) : this.defaultSummary - const typeAsString = this.checker.typeToString(itemType) + const typeAsString = + zodObjectTypeName || this.checker.typeToString(itemType) const schemaFromFactory = this.schemaFactory.tryGetSchema( itemType.symbol?.getName() || @@ -1433,6 +1452,7 @@ class OasKindGenerator extends FunctionKindGenerator { (itemType as ts.Type).flags === ts.TypeFlags.Object: const properties: Record = {} const requiredProperties: string[] = [] + if (level + 1 <= this.MAX_LEVEL) { itemType.getProperties().forEach((property) => { if ( @@ -1461,7 +1481,9 @@ class OasKindGenerator extends FunctionKindGenerator { type: "object", description, "x-schemaName": - itemType.isClassOrInterface() || itemType.isTypeParameter() + itemType.isClassOrInterface() || + itemType.isTypeParameter() || + (isZodObject(itemType) && zodObjectTypeName) ? this.oasSchemaHelper.normalizeSchemaName(typeAsString) : undefined, required: @@ -1871,32 +1893,43 @@ class OasKindGenerator extends FunctionKindGenerator { const areaYaml = parse( readFileSync(areaYamlPath, "utf-8") ) as OpenApiDocument - let addedTags = false + let modifiedTags = false areaYaml.tags = [...(areaYaml.tags || [])] 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 associatedSchema = schema?.schema?.["x-schemaName"] + ? { + $ref: this.oasSchemaHelper.constructSchemaReference( + schema.schema["x-schemaName"] + ), + } + : undefined if (!existingTag) { - // try to retrieve associated schema - const schema = this.oasSchemaHelper.getSchemaByName( - this.oasSchemaHelper.tagNameToSchemaName(tag) - ) areaYaml.tags!.push({ name: tag, - "x-associatedSchema": schema?.schema?.["x-schemaName"] - ? { - $ref: this.oasSchemaHelper.constructSchemaReference( - schema.schema["x-schemaName"] - ), - } - : undefined, + "x-associatedSchema": associatedSchema, }) - addedTags = true + modifiedTags = true + } else if ( + existingTag["x-associatedSchema"]?.$ref !== associatedSchema?.$ref + ) { + existingTag["x-associatedSchema"] = associatedSchema + modifiedTags = true } }) - if (addedTags) { + if (modifiedTags) { // sort alphabetically areaYaml.tags.sort((tagA, tagB) => { return tagA.name.localeCompare(tagB.name) diff --git a/www/utils/packages/docblock-generator/src/utils/get-correct-zod-type-name.ts b/www/utils/packages/docblock-generator/src/utils/get-correct-zod-type-name.ts new file mode 100644 index 0000000000..b8e25ed56f --- /dev/null +++ b/www/utils/packages/docblock-generator/src/utils/get-correct-zod-type-name.ts @@ -0,0 +1,23 @@ +// Due to some types using Zod in their declaration +// The type name isn't picked properly by typescript + +import ts from "typescript" +import isZodObject from "./is-zod-object.js" + +// this ensures that the correct type name is used. +export default function getCorrectZodTypeName({ + typeReferenceNode, + itemType, +}: { + typeReferenceNode: ts.TypeReferenceNode + itemType: ts.Type +}): string | undefined { + if (!isZodObject(itemType)) { + return + } + + return typeReferenceNode.typeArguments?.[0] && + "typeName" in typeReferenceNode.typeArguments[0] + ? (typeReferenceNode.typeArguments?.[0].typeName as ts.Identifier).getText() + : undefined +} diff --git a/www/utils/packages/docblock-generator/src/utils/is-zod-object.ts b/www/utils/packages/docblock-generator/src/utils/is-zod-object.ts new file mode 100644 index 0000000000..7329c91e86 --- /dev/null +++ b/www/utils/packages/docblock-generator/src/utils/is-zod-object.ts @@ -0,0 +1,15 @@ +import ts from "typescript" + +export default function isZodObject(itemType: ts.Type): boolean { + if (!itemType.symbol?.declarations?.length) { + return false + } + + const parent = itemType.symbol.declarations[0].parent + + if (!("typeName" in parent)) { + return false + } + + return (parent.typeName as ts.Identifier).getText().includes("ZodObject") +}