From d127abbcda52889d15350e63de15e3d309e6c902 Mon Sep 17 00:00:00 2001 From: Shahed Nasser Date: Mon, 29 Jul 2024 11:11:39 +0300 Subject: [PATCH] docs-util: add docblock generator for models built with DML (#8296) * docs-util: add docblock generator for models built with DML * add missing turbo task --- .github/workflows/generate-docblocks.yml | 52 ++++ .../workflows/generate-preview-references.yml | 54 +++- www/utils/package.json | 3 +- .../packages/docblock-generator/package.json | 3 +- .../src/classes/generators/dml.ts | 111 +++++++ .../src/classes/generators/docblock.ts | 3 +- .../src/classes/generators/index.ts | 21 +- .../src/classes/generators/oas.ts | 3 +- .../classes/helpers/knowledge-base-factory.ts | 57 ++++ .../src/classes/helpers/oas-schema.ts | 2 +- .../src/classes/kinds/default.ts | 4 +- .../src/classes/kinds/dml.ts | 277 ++++++++++++++++++ .../src/classes/kinds/dto-property.ts | 48 ++- .../src/classes/kinds/oas.ts | 2 +- .../src/classes/kinds/registry.ts | 2 + .../src/commands/clean-dml.ts | 43 +++ .../src/commands/clean-oas.ts | 2 +- .../src/commands/run-git-changes.ts | 10 + .../src/commands/run-git-commit.ts | 10 + .../src/commands/run-release.ts | 10 + .../docblock-generator/src/commands/run.ts | 10 + .../packages/docblock-generator/src/index.ts | 10 +- .../docblock-generator/src/types/index.d.ts | 11 +- .../src/utils/get-base-path.ts | 15 + .../src/utils/get-monorepo-root.ts | 2 +- .../src/utils/get-oas-output-base-path.ts | 6 - .../src/utils/get-output-base-paths.ts | 16 + .../src/utils/to-json-formatted.ts | 9 + .../packages/utils/src/str-formatting.ts | 11 + www/utils/turbo.json | 5 + 30 files changed, 749 insertions(+), 63 deletions(-) create mode 100644 www/utils/packages/docblock-generator/src/classes/generators/dml.ts create mode 100644 www/utils/packages/docblock-generator/src/classes/kinds/dml.ts create mode 100644 www/utils/packages/docblock-generator/src/commands/clean-dml.ts create mode 100644 www/utils/packages/docblock-generator/src/utils/get-base-path.ts delete mode 100644 www/utils/packages/docblock-generator/src/utils/get-oas-output-base-path.ts create mode 100644 www/utils/packages/docblock-generator/src/utils/get-output-base-paths.ts create mode 100644 www/utils/packages/docblock-generator/src/utils/to-json-formatted.ts diff --git a/.github/workflows/generate-docblocks.yml b/.github/workflows/generate-docblocks.yml index fb4b74113d..d2ccb8ced6 100644 --- a/.github/workflows/generate-docblocks.yml +++ b/.github/workflows/generate-docblocks.yml @@ -116,3 +116,55 @@ jobs: branch: "chore/generate-oas" branch-suffix: "timestamp" add-paths: www/utils/generated/oas-output/** + generate-dml: + name: Generated DML JSON files PR + runs-on: ubuntu-latest + steps: + - name: Checkout Repo + uses: actions/checkout@v3 + + - name: Setup Node.js 20 + uses: actions/setup-node@v3 + with: + node-version: 20 + + - name: Install Dependencies + run: yarn + + - name: Install www/utils Dependencies + run: yarn + working-directory: www/utils + + - name: Build packages + run: yarn build + working-directory: www/utils + + - name: Check Commit + id: check-commit + run: "yarn check:release-commit ${{ github.sha }}" + working-directory: www/utils/packages/scripts + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GIT_OWNER: ${{ github.repository_owner }} + GIT_REPO: medusa + + - name: Run docblock generator + if: steps.check-commit.outputs.is_release_commit == 'true' + run: "yarn generate:dml" + working-directory: www/utils + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GIT_OWNER: ${{ github.repository_owner }} + GIT_REPO: medusa + + - name: Create Pull Request + if: steps.check-commit.outputs.is_release_commit == 'true' + uses: peter-evans/create-pull-request@v5 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + title: "chore(docs): Generated DML JSON files" + body: "This PR holds all generated DML JSON files for the upcoming release." + branch: "chore/generate-dml-json" + branch-suffix: "timestamp" + add-paths: www/utils/generated/dml-output/** diff --git a/.github/workflows/generate-preview-references.yml b/.github/workflows/generate-preview-references.yml index b25fc991b6..417a8e4ff4 100644 --- a/.github/workflows/generate-preview-references.yml +++ b/.github/workflows/generate-preview-references.yml @@ -64,4 +64,56 @@ jobs: www/apps/api-reference/specs www/utils/generated/oas-output branch: "docs/generate-api-ref" - branch-suffix: "timestamp" \ No newline at end of file + branch-suffix: "timestamp" + preview-dml: + name: Generate DML JSON files + runs-on: ubuntu-latest + steps: + - name: Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.11.0 + with: + access_token: ${{ github.token }} + + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Setup Node.js environment + uses: actions/setup-node@v3 + with: + node-version: 20 + cache: "yarn" + + - name: Install dependencies + uses: ./.github/actions/cache-deps + with: + extension: reference + + - name: Build Packages + run: yarn build + + - name: Install www/utils Dependencies + run: yarn + working-directory: www/utils + + - name: Build www/utils packages + run: yarn build + working-directory: www/utils + + - name: Run docblock generator + run: "yarn generate:dml" + working-directory: www/utils + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GIT_OWNER: ${{ github.repository_owner }} + GIT_REPO: medusa + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v4 + with: + title: "chore(docs): Generated DML JSON files" + body: "This PR holds all generated DML JSON files for the upcoming release." + branch: "chore/generate-dml-json" + branch-suffix: "timestamp" + add-paths: www/utils/generated/dml-output/** \ No newline at end of file diff --git a/www/utils/package.json b/www/utils/package.json index 3793839c86..00f65dcb1f 100644 --- a/www/utils/package.json +++ b/www/utils/package.json @@ -9,7 +9,8 @@ "watch": "turbo run watch", "lint": "turbo run lint", "generate:references": "turbo run generate:references", - "generate:oas": "turbo run generate:oas" + "generate:oas": "turbo run generate:oas", + "generate:dml": "turbo run generate:dml" }, "dependencies": { "@babel/core": "^7.23.0", diff --git a/www/utils/packages/docblock-generator/package.json b/www/utils/packages/docblock-generator/package.json index 3bb540b14b..72e76f8fe4 100644 --- a/www/utils/packages/docblock-generator/package.json +++ b/www/utils/packages/docblock-generator/package.json @@ -7,7 +7,8 @@ "build": "tsc", "watch": "tsc --watch", "prepublishOnly": "cross-env NODE_ENV=production tsc --build", - "generate:oas": "yarn start run ../../../../packages/medusa/src/api --type oas && yarn start clean:oas" + "generate:oas": "yarn start run ../../../../packages/medusa/src/api --type oas && yarn start clean:oas", + "generate:dml": "yarn start run ../../../../packages/modules --type dml && yarn start clean:dml" }, "publishConfig": { "access": "public" diff --git a/www/utils/packages/docblock-generator/src/classes/generators/dml.ts b/www/utils/packages/docblock-generator/src/classes/generators/dml.ts new file mode 100644 index 0000000000..df1d2d02e8 --- /dev/null +++ b/www/utils/packages/docblock-generator/src/classes/generators/dml.ts @@ -0,0 +1,111 @@ +import ts from "typescript" +import DmlKindGenerator from "../kinds/dml.js" +import AbstractGenerator from "./index.js" +import { GeneratorEvent } from "../helpers/generator-event-manager.js" +import { minimatch } from "minimatch" +import getBasePath from "../../utils/get-base-path.js" +import toJsonFormatted from "../../utils/to-json-formatted.js" +import { DmlFile } from "../../types/index.js" + +/** + * A class used to generate DML JSON files with descriptions of properties. + */ +class DmlGenerator extends AbstractGenerator { + protected dmlKindGenerator?: DmlKindGenerator + + async run() { + this.init() + + this.dmlKindGenerator = new DmlKindGenerator({ + checker: this.checker!, + generatorEventManager: this.generatorEventManager, + }) + + await Promise.all( + this.program!.getSourceFiles().map(async (file) => { + // Ignore .d.ts files + if (file.isDeclarationFile || !this.isFileIncluded(file.fileName)) { + return + } + const fileNodes: ts.Node[] = [file] + + console.log(`[DML] Generating for ${file.fileName}...`) + + // since typescript's compiler API doesn't support + // async processes, we have to retrieve the nodes first then + // traverse them separately. + const pushNodesToArr = (node: ts.Node) => { + fileNodes.push(node) + + ts.forEachChild(node, pushNodesToArr) + } + ts.forEachChild(file, pushNodesToArr) + + const documentChild = async (node: ts.Node) => { + if ( + this.dmlKindGenerator!.isAllowed(node) && + this.dmlKindGenerator!.canDocumentNode(node) + ) { + const dmlJson = await this.dmlKindGenerator!.getDocBlock(node) + + if (!this.options.dryRun) { + const filePath = + this.dmlKindGenerator!.getAssociatedFileName(node) + this.writeJson(filePath, dmlJson) + } + } + } + + await Promise.all( + fileNodes.map(async (node) => await documentChild(node)) + ) + + this.generatorEventManager.emit(GeneratorEvent.FINISHED_GENERATE_EVENT) + console.log(`[OAS] Finished generating OAS for ${file.fileName}.`) + }) + ) + } + + /** + * Checks whether the specified file path is included in the program + * and is an API file. + * + * @param fileName - The file path to check + * @returns Whether the OAS generator can run on this file. + */ + isFileIncluded(fileName: string): boolean { + return ( + super.isFileIncluded(fileName) && + minimatch(getBasePath(fileName), "packages/modules/**/models/**", { + matchBase: true, + }) + ) + } + + /** + * This method writes the DML JSON file. If the file already exists, it only updates + * the data model's object in the JSON file. + * + * @param filePath - The path of the file to write the DML JSON to. + * @param dataModelJson - The DML JSON. + */ + writeJson(filePath: string, dataModelJson: string) { + let moduleJson = ts.sys.readFile(filePath) + if (!moduleJson) { + moduleJson = dataModelJson + } else { + // parse the JSON and replace the data model's JSON + // with the new one + const parsedModuleJson = JSON.parse(moduleJson) as DmlFile + const parsedDataModelJson = JSON.parse(dataModelJson) as DmlFile + const dataModelName = Object.keys(parsedDataModelJson)[0] + parsedModuleJson[dataModelName] = parsedDataModelJson[dataModelName] + + moduleJson = toJsonFormatted(parsedModuleJson) + } + + ts.sys.writeFile(filePath, moduleJson) + } +} + +export default DmlGenerator diff --git a/www/utils/packages/docblock-generator/src/classes/generators/docblock.ts b/www/utils/packages/docblock-generator/src/classes/generators/docblock.ts index b29ded0768..9aa69710bb 100644 --- a/www/utils/packages/docblock-generator/src/classes/generators/docblock.ts +++ b/www/utils/packages/docblock-generator/src/classes/generators/docblock.ts @@ -4,6 +4,7 @@ import { GeneratorEvent } from "../helpers/generator-event-manager.js" import AbstractGenerator from "./index.js" import { minimatch } from "minimatch" import AiGenerator from "../helpers/ai-generator.js" +import getBasePath from "../../utils/get-base-path.js" /** * A class used to generate docblock for one or multiple file paths. @@ -164,7 +165,7 @@ class DocblockGenerator extends AbstractGenerator { isFileIncluded(fileName: string): boolean { return ( super.isFileIncluded(fileName) && - !minimatch(this.getBasePath(fileName), "packages/medusa/**/api**/**", { + !minimatch(getBasePath(fileName), "packages/medusa/**/api**/**", { matchBase: true, }) ) diff --git a/www/utils/packages/docblock-generator/src/classes/generators/index.ts b/www/utils/packages/docblock-generator/src/classes/generators/index.ts index 01c46bdc59..e7b635b767 100644 --- a/www/utils/packages/docblock-generator/src/classes/generators/index.ts +++ b/www/utils/packages/docblock-generator/src/classes/generators/index.ts @@ -5,6 +5,7 @@ import GeneratorEventManager from "../helpers/generator-event-manager.js" import { CommonCliOptions } from "../../types/index.js" import { existsSync, readdirSync, statSync } from "node:fs" import path from "node:path" +import getBasePath from "../../utils/get-base-path.js" export type Options = { paths: string[] @@ -76,28 +77,12 @@ abstract class AbstractGenerator { * @returns {boolean} Whether the file can have docblocks generated for it. */ isFileIncluded(fileName: string): boolean { - const baseFilePath = this.getBasePath(fileName) + const baseFilePath = getBasePath(fileName) return this.options.paths.some((path) => - baseFilePath.startsWith(this.getBasePath(path)) + baseFilePath.startsWith(getBasePath(path)) ) } - /** - * Retrieve the pathname of a file without the relative part before `packages/` - * - * @param fileName - The file name/path - * @returns The path without the relative part. - */ - getBasePath(fileName: string) { - let basePath = fileName - const packageIndex = fileName.indexOf("packages/") - if (packageIndex) { - basePath = basePath.substring(packageIndex) - } - - return basePath - } - /** * Reset the generator's properties for new usage. */ diff --git a/www/utils/packages/docblock-generator/src/classes/generators/oas.ts b/www/utils/packages/docblock-generator/src/classes/generators/oas.ts index 75eb2fc6ed..9864bb837a 100644 --- a/www/utils/packages/docblock-generator/src/classes/generators/oas.ts +++ b/www/utils/packages/docblock-generator/src/classes/generators/oas.ts @@ -3,6 +3,7 @@ import AbstractGenerator from "./index.js" import ts from "typescript" import OasKindGenerator from "../kinds/oas.js" import { GeneratorEvent } from "../helpers/generator-event-manager.js" +import getBasePath from "../../utils/get-base-path.js" /** * A class used to generate OAS yaml comments. The comments are written @@ -82,7 +83,7 @@ class OasGenerator extends AbstractGenerator { isFileIncluded(fileName: string): boolean { return ( super.isFileIncluded(fileName) && - minimatch(this.getBasePath(fileName), "packages/medusa/**/api**/**", { + minimatch(getBasePath(fileName), "packages/medusa/**/api**/**", { matchBase: true, }) ) 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 e3a98342fa..28f5ef6bf2 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 @@ -678,6 +678,63 @@ class KnowledgeBaseFactory { return result } + + /** + * Retrieve summary and description for a property of an object, interface, or type. + * + * @param param0 - The property's details. + * @returns The property's summary. + */ + tryToGetObjectPropertySummary({ + retrieveOptions, + propertyDetails: { isClassOrInterface, isBoolean, classOrInterfaceName }, + }: { + /** + * The options used to retrieve the summary from the knowledge base, if available. + */ + retrieveOptions: RetrieveOptions + /** + * The details of the property. + */ + propertyDetails: { + /** + * Whether the property's value is a class or interface. This applies to all + * object-like types. + * + * If `true`, the property is considered to represent a relationship to another + * class / interface. + */ + isClassOrInterface: boolean + /** + * Whether the property's value is a boolean + */ + isBoolean: boolean + /** + * The name of the class / interface this property's value is associated to. + * This is only used if {@link isClassOrInterface} is `true`. + */ + classOrInterfaceName?: string + } + }): string { + let summary = this.tryToGetSummary(retrieveOptions) + + if (summary) { + return summary + } + + if (isClassOrInterface) { + summary = `The associated ${classOrInterfaceName}.` + } else if (isBoolean) { + summary = `Whether the ${retrieveOptions.templateOptions + ?.parentName} ${snakeToWords(retrieveOptions.str)}.` + } else { + summary = `The ${snakeToWords( + retrieveOptions.str + )} of the ${retrieveOptions.templateOptions?.parentName}` + } + + return summary + } } 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 feadb4d4c9..bab0ab2ef6 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 @@ -4,7 +4,7 @@ import Formatter from "./formatter.js" import { join } from "path" import { DOCBLOCK_LINE_ASTRIX } from "../../constants.js" import ts from "typescript" -import getOasOutputBasePath from "../../utils/get-oas-output-base-path.js" +import { getOasOutputBasePath } from "../../utils/get-output-base-paths.js" import { parse } from "yaml" import formatOas from "../../utils/format-oas.js" import pluralize from "pluralize" diff --git a/www/utils/packages/docblock-generator/src/classes/kinds/default.ts b/www/utils/packages/docblock-generator/src/classes/kinds/default.ts index 0a14afc843..f6390fe17e 100644 --- a/www/utils/packages/docblock-generator/src/classes/kinds/default.ts +++ b/www/utils/packages/docblock-generator/src/classes/kinds/default.ts @@ -24,7 +24,7 @@ export type GeneratorOptions = { checker: ts.TypeChecker kinds?: ts.SyntaxKind[] generatorEventManager: GeneratorEventManager - additionalOptions: Pick + additionalOptions?: Pick } export type GetDocBlockOptions = { @@ -65,7 +65,7 @@ class DefaultKindGenerator { checker, kinds, generatorEventManager, - additionalOptions, + additionalOptions = {}, }: GeneratorOptions) { this.allowedKinds = kinds || DefaultKindGenerator.DEFAULT_ALLOWED_NODE_KINDS this.checker = checker diff --git a/www/utils/packages/docblock-generator/src/classes/kinds/dml.ts b/www/utils/packages/docblock-generator/src/classes/kinds/dml.ts new file mode 100644 index 0000000000..0436bd0778 --- /dev/null +++ b/www/utils/packages/docblock-generator/src/classes/kinds/dml.ts @@ -0,0 +1,277 @@ +import ts from "typescript" +import DefaultKindGenerator, { GetDocBlockOptions } from "./default.js" +import getBasePath from "../../utils/get-base-path.js" +import { getDmlOutputBasePath } from "../../utils/get-output-base-paths.js" +import path from "path" +import { camelToWords, RELATION_NAMES, snakeToPascal } from "utils" +import toJsonFormatted from "../../utils/to-json-formatted.js" +import { DmlFile, DmlObject } from "../../types/index.js" + +/** + * DML generator for data models created with DML. + */ +class DmlKindGenerator extends DefaultKindGenerator { + protected allowedKinds: ts.SyntaxKind[] = [ts.SyntaxKind.CallExpression] + public name = "dml" + + /** + * Checks whether the node is a call expression, is `model.define`, and has at least + * two arguments. + * + * @param node - The node to check + * @returns Whether this kind generator can be used for this node. + */ + isAllowed(node: ts.Node): node is ts.CallExpression { + if (!super.isAllowed(node)) { + return false + } + + const isModelUtility = + "expression" in node.expression && + (node.expression.expression as ts.Node).getText() === "model" + const isDefineMethod = + "name" in node.expression && + (node.expression.name as ts.Identifier).getText() === "define" + const hasAllArguments = node.arguments.length >= 2 + + return isModelUtility && isDefineMethod && hasAllArguments + } + + /** + * Retrieve the DML object from the node's associated file, if it exists. + * + * @param node - The node to retrieve its DML object. + * @param dataModelName - The data model's name + * @returns The DML object, if available. + */ + getExistingDmlObjectFromFile( + node: ts.CallExpression, + dataModelName: string + ): DmlObject | undefined { + const filePath = this.getAssociatedFileName(node) + const existingJson = ts.sys.readFile(filePath) + + if (!existingJson) { + return + } + + const parsedJson = JSON.parse(existingJson) as DmlFile + if (!Object.hasOwn(parsedJson, dataModelName)) { + return + } + + return parsedJson[dataModelName].properties + } + + /** + * Get new or updated JSON dml object of the data model. + * + * @param node - The node to get its JSON DML object. + * @param options - General options + * @returns The JSON dml object. + */ + async getDocBlock( + node: ts.CallExpression | ts.Node, + options?: GetDocBlockOptions + ): Promise { + if (!this.isAllowed(node)) { + return await super.getDocBlock(node, options) + } + const dataModelName = this.getDataModelName(node.arguments[0]) + + const existingDmlObject = this.getExistingDmlObjectFromFile( + node, + dataModelName + ) + + const properties = existingDmlObject + ? this.updateExistingDmlObject({ + node, + dmlObject: existingDmlObject, + dataModelName, + }) + : this.getNewDmlObject({ + node, + dataModelName, + }) + + const dmlFile: DmlFile = { + [dataModelName]: { + filePath: getBasePath(node.getSourceFile().fileName), + properties, + }, + } + + return toJsonFormatted(dmlFile) + } + + /** + * Get a new DML object for a node. + * + * @param param0 - The node and data model's details + * @returns The DML object. + */ + getNewDmlObject({ + node, + dataModelName, + }: { + node: ts.CallExpression + dataModelName: string + }): DmlObject { + return this.getPropertiesFromNode(node, dataModelName) + } + + /** + * Update existing dml object by removing properties no longer + * available and adding new ones. + * + * @param param0 - The node and data model's details. + * @returns The updated DML object. + */ + updateExistingDmlObject({ + node, + dmlObject, + dataModelName, + }: { + node: ts.CallExpression + dmlObject: DmlObject + dataModelName: string + }): DmlObject { + const newDmlObject = Object.assign({}, dmlObject) + const newProperties = this.getPropertiesFromNode(node, dataModelName) + + const newPropertyNames = Object.keys(newProperties) + const oldPropertyNames = Object.keys(newDmlObject) + + // delete properties not available anymore + oldPropertyNames.forEach((oldPropertyName) => { + if (!newPropertyNames.includes(oldPropertyName)) { + delete newDmlObject[oldPropertyName] + } + }) + + // add new properties + newPropertyNames.forEach((newPropertyName) => { + if (oldPropertyNames.includes(newPropertyName)) { + return + } + + newDmlObject[newPropertyName] = newProperties[newPropertyName] + }) + + return newDmlObject + } + + /** + * Get the data model's name. It's either a string passed to `model.define`, or + * in the `name` property of the object passed as a parameter to `model.define`. + * + * @param node - The node to retrieve its data model name. + * @returns The data model's name. + */ + getDataModelName(node: ts.Node): string { + let name = node.getText() + if (ts.isObjectLiteralExpression(node)) { + const nameProperty = node.properties.find((propertyNode) => { + return ( + propertyNode.name?.getText() === "name" && + "initializer" in propertyNode + ) + }) as ts.ObjectLiteralElementLike & { + initializer: ts.Node + } + + if (nameProperty) { + name = nameProperty.initializer.getText() + } + } + + return snakeToPascal(name.replace(/^"/, "").replace(/"$/, "")) + } + + /** + * Get the properties of a node. + * + * @param node - The node to get its properties. + * @param dataModelName - The data model's name. + * @returns The properties and their description. + */ + getPropertiesFromNode( + node: ts.CallExpression, + dataModelName: string + ): DmlObject { + const formattedDataModelName = this.formatDataModelName(dataModelName) + const propertyNodes = + "properties" in node.arguments[1] + ? (node.arguments[1].properties as ts.Node[]) + : [] + + const properties: DmlObject = {} + + propertyNodes.forEach((propertyNode) => { + const propertyName = + "name" in propertyNode + ? (propertyNode.name as ts.Identifier).getText() + : propertyNode.getText() + const propertyType = this.checker.getTypeAtLocation(propertyNode) + const propertyTypeStr = this.checker.typeToString(propertyType) + + const isRelation = RELATION_NAMES.some((relationName) => + propertyTypeStr.includes(relationName) + ) + const isBoolean = propertyTypeStr.includes("BooleanProperty") + const relationName = isRelation ? camelToWords(propertyName) : undefined + + let propertyDescription = + this.knowledgeBaseFactory.tryToGetObjectPropertySummary({ + retrieveOptions: { + str: propertyName, + kind: propertyNode.kind, + templateOptions: { + parentName: formattedDataModelName, + rawParentName: dataModelName, + }, + }, + propertyDetails: { + isClassOrInterface: isRelation, + isBoolean, + classOrInterfaceName: relationName, + }, + }) + + if (isRelation) { + propertyDescription += `\n\n@expandable` + } + + properties[propertyName] = propertyDescription + }) + + return properties + } + + /** + * Get the name of the output JSON file associated with a node. + * + * @param node - The node to get its file name. + * @returns The file name. + */ + getAssociatedFileName(node: ts.Node): string { + const filePath = getBasePath(node.getSourceFile().fileName).split("/") + // since modules are at packages/modules/, the name should be at index 2 + const moduleName = filePath[2] + + return path.join(getDmlOutputBasePath(), `${moduleName}.json`) + } + + /** + * Format the data model name for presentation purposes. + * + * @param name - The raw data model's name. + * @returns The formatted name. + */ + formatDataModelName(name: string): string { + return camelToWords(name) + } +} + +export default DmlKindGenerator diff --git a/www/utils/packages/docblock-generator/src/classes/kinds/dto-property.ts b/www/utils/packages/docblock-generator/src/classes/kinds/dto-property.ts index 1e1f5d8ebf..5a20aebf00 100644 --- a/www/utils/packages/docblock-generator/src/classes/kinds/dto-property.ts +++ b/www/utils/packages/docblock-generator/src/classes/kinds/dto-property.ts @@ -1,7 +1,7 @@ import ts from "typescript" import DefaultKindGenerator, { GetDocBlockOptions } from "./default.js" import { DOCBLOCK_END_LINE, DOCBLOCK_START } from "../../constants.js" -import { camelToWords, snakeToWords } from "utils" +import { camelToWords } from "utils" import { normalizeName } from "../../utils/str-formatting.js" /** @@ -40,36 +40,32 @@ class DTOPropertyGenerator extends DefaultKindGenerator { let str = DOCBLOCK_START const rawParentName = this.getParentName(node.parent) const parentName = this.formatInterfaceName(rawParentName) + // check if the property's type is interface/type/class + const propertyType = this.checker.getTypeAtLocation(node) + const isPropertyClassOrInterface = propertyType.isClassOrInterface() // try first to retrieve the summary from the knowledge base if it exists. - const summary = this.knowledgeBaseFactory.tryToGetSummary({ - str: node.name.getText(), - kind: node.kind, - templateOptions: { - rawParentName, - parentName, + const summary = this.knowledgeBaseFactory.tryToGetObjectPropertySummary({ + retrieveOptions: { + str: node.name.getText(), + kind: node.kind, + templateOptions: { + rawParentName, + parentName, + }, + }, + propertyDetails: { + isClassOrInterface: isPropertyClassOrInterface, + isBoolean: + "intrinsicName" in propertyType && + propertyType.intrinsicName === "boolean", + classOrInterfaceName: isPropertyClassOrInterface + ? this.formatInterfaceName(this.checker.typeToString(propertyType)) + : undefined, }, }) - if (summary) { - str += summary - } else { - // check if the property's type is interface/type/class - const propertyType = this.checker.getTypeAtLocation(node) - if (propertyType.isClassOrInterface()) { - str += `The associated ${this.formatInterfaceName( - this.checker.typeToString(propertyType) - )}.` - } else if ( - "intrinsicName" in propertyType && - propertyType.intrinsicName === "boolean" - ) { - str += `Whether the ${parentName} ${snakeToWords(node.name.getText())}.` - } else { - // format summary - str += `The ${snakeToWords(node.name.getText())} of the ${parentName}.` - } - } + str += summary return `${str}${DOCBLOCK_END_LINE}` } 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 ba05721a7a..e2bde964d7 100644 --- a/www/utils/packages/docblock-generator/src/classes/kinds/oas.ts +++ b/www/utils/packages/docblock-generator/src/classes/kinds/oas.ts @@ -12,7 +12,7 @@ import { } from "../../types/index.js" import formatOas from "../../utils/format-oas.js" import getCorrectZodTypeName from "../../utils/get-correct-zod-type-name.js" -import getOasOutputBasePath from "../../utils/get-oas-output-base-path.js" +import { getOasOutputBasePath } from "../../utils/get-output-base-paths.js" import isZodObject from "../../utils/is-zod-object.js" import parseOas, { ExistingOas } from "../../utils/parse-oas.js" import OasExamplesGenerator from "../examples/oas.js" diff --git a/www/utils/packages/docblock-generator/src/classes/kinds/registry.ts b/www/utils/packages/docblock-generator/src/classes/kinds/registry.ts index 7dd9d6521a..f4d74ccf58 100644 --- a/www/utils/packages/docblock-generator/src/classes/kinds/registry.ts +++ b/www/utils/packages/docblock-generator/src/classes/kinds/registry.ts @@ -5,6 +5,7 @@ import MedusaReactHooksKindGenerator from "./medusa-react-hooks.js" import SourceFileKindGenerator from "./source-file.js" import DTOPropertyGenerator from "./dto-property.js" import OasKindGenerator from "./oas.js" +import DmlKindGenerator from "./dml.js" /** * A class that is used as a registry for the kind generators. @@ -20,6 +21,7 @@ class KindsRegistry { > ) { this.kindInstances = [ + new DmlKindGenerator(options), new OasKindGenerator(options), new MedusaReactHooksKindGenerator(options), new FunctionKindGenerator(options), diff --git a/www/utils/packages/docblock-generator/src/commands/clean-dml.ts b/www/utils/packages/docblock-generator/src/commands/clean-dml.ts new file mode 100644 index 0000000000..08b54a5edc --- /dev/null +++ b/www/utils/packages/docblock-generator/src/commands/clean-dml.ts @@ -0,0 +1,43 @@ +import { readdirSync, existsSync, readFileSync, writeFileSync } from "fs" +import { getDmlOutputBasePath } from "../utils/get-output-base-paths.js" +import path from "path" +import getMonorepoRoot from "../utils/get-monorepo-root.js" +import { DmlFile } from "../types/index.js" +import toJsonFormatted from "../utils/to-json-formatted.js" + +export default async function () { + console.log("Cleaning DML JSON files...") + + const dmlOutputPath = getDmlOutputBasePath() + const monorepoRoot = getMonorepoRoot() + + const jsonFiles = readdirSync(dmlOutputPath) + + jsonFiles.forEach((jsonFile) => { + const jsonFilePath = path.join(dmlOutputPath, jsonFile) + const parsedJson = JSON.parse( + readFileSync(jsonFilePath, "utf-8") + ) as DmlFile + + const dataModelKeys = Object.keys(parsedJson) + let dataUpdated = false + + dataModelKeys.forEach((dataModelName) => { + const { filePath } = parsedJson[dataModelName] + + const fullFilePath = path.join(monorepoRoot, filePath) + + if (existsSync(fullFilePath)) { + return + } + + // delete data model from json object + delete parsedJson[dataModelName] + dataUpdated = true + }) + + if (dataUpdated) { + writeFileSync(jsonFilePath, toJsonFormatted(parsedJson)) + } + }) +} diff --git a/www/utils/packages/docblock-generator/src/commands/clean-oas.ts b/www/utils/packages/docblock-generator/src/commands/clean-oas.ts index bd8be3bf8b..66803edac5 100644 --- a/www/utils/packages/docblock-generator/src/commands/clean-oas.ts +++ b/www/utils/packages/docblock-generator/src/commands/clean-oas.ts @@ -15,7 +15,7 @@ import OasKindGenerator, { OasArea } from "../classes/kinds/oas.js" import { DEFAULT_OAS_RESPONSES } from "../constants.js" import { OpenApiDocument, OpenApiSchema } from "../types/index.js" import getMonorepoRoot from "../utils/get-monorepo-root.js" -import getOasOutputBasePath from "../utils/get-oas-output-base-path.js" +import { getOasOutputBasePath } from "../utils/get-output-base-paths.js" import parseOas from "../utils/parse-oas.js" const OAS_PREFIX_REGEX = /@oas \[(?(get|post|delete))\] (?.+)/ diff --git a/www/utils/packages/docblock-generator/src/commands/run-git-changes.ts b/www/utils/packages/docblock-generator/src/commands/run-git-changes.ts index 50ce7c4ab4..0e3aa89c00 100644 --- a/www/utils/packages/docblock-generator/src/commands/run-git-changes.ts +++ b/www/utils/packages/docblock-generator/src/commands/run-git-changes.ts @@ -4,6 +4,7 @@ import getMonorepoRoot from "../utils/get-monorepo-root.js" import { GitManager } from "../classes/helpers/git-manager.js" import { CommonCliOptions } from "../types/index.js" import OasGenerator from "../classes/generators/oas.js" +import DmlGenerator from "../classes/generators/dml.js" export default async function runGitChanges({ type, @@ -43,5 +44,14 @@ export default async function runGitChanges({ await oasGenerator.run() } + if (type === "all" || type === "dml") { + const dmlGenerator = new DmlGenerator({ + paths: files, + ...options, + }) + + await dmlGenerator.run() + } + console.log(`Finished generating docs for ${files.length} files.`) } diff --git a/www/utils/packages/docblock-generator/src/commands/run-git-commit.ts b/www/utils/packages/docblock-generator/src/commands/run-git-commit.ts index 1ed2cd2332..f55c018c9c 100644 --- a/www/utils/packages/docblock-generator/src/commands/run-git-commit.ts +++ b/www/utils/packages/docblock-generator/src/commands/run-git-commit.ts @@ -5,6 +5,7 @@ import DocblockGenerator from "../classes/generators/docblock.js" import OasGenerator from "../classes/generators/oas.js" import { CommonCliOptions } from "../types/index.js" import { GitManager } from "../classes/helpers/git-manager.js" +import DmlGenerator from "../classes/generators/dml.js" export default async function ( commitSha: string, @@ -51,5 +52,14 @@ export default async function ( await oasGenerator.run() } + if (type === "all" || type === "dml") { + const dmlGenerator = new DmlGenerator({ + paths: filteredFiles, + ...options, + }) + + await dmlGenerator.run() + } + console.log(`Finished generating docs for ${filteredFiles.length} files.`) } diff --git a/www/utils/packages/docblock-generator/src/commands/run-release.ts b/www/utils/packages/docblock-generator/src/commands/run-release.ts index 5af53a24c1..53c5bd0bd7 100644 --- a/www/utils/packages/docblock-generator/src/commands/run-release.ts +++ b/www/utils/packages/docblock-generator/src/commands/run-release.ts @@ -5,6 +5,7 @@ import getMonorepoRoot from "../utils/get-monorepo-root.js" import { GitManager } from "../classes/helpers/git-manager.js" import OasGenerator from "../classes/generators/oas.js" import { CommonCliOptions } from "../types/index.js" +import DmlGenerator from "../classes/generators/dml.js" export default async function ({ type, tag, ...options }: CommonCliOptions) { const gitManager = new GitManager() @@ -49,5 +50,14 @@ export default async function ({ type, tag, ...options }: CommonCliOptions) { await oasGenerator.run() } + if (type === "all" || type === "dml") { + const dmlGenerator = new DmlGenerator({ + paths: filteredFiles, + ...options, + }) + + await dmlGenerator.run() + } + console.log(`Finished generating docs for ${filteredFiles.length} files.`) } diff --git a/www/utils/packages/docblock-generator/src/commands/run.ts b/www/utils/packages/docblock-generator/src/commands/run.ts index 3085517d8d..c1daa79688 100644 --- a/www/utils/packages/docblock-generator/src/commands/run.ts +++ b/www/utils/packages/docblock-generator/src/commands/run.ts @@ -1,3 +1,4 @@ +import DmlGenerator from "../classes/generators/dml.js" import DocblockGenerator from "../classes/generators/docblock.js" import { Options } from "../classes/generators/index.js" import OasGenerator from "../classes/generators/oas.js" @@ -27,5 +28,14 @@ export default async function run( await oasGenerator.run() } + if (type === "all" || type === "dml") { + const dmlGenerator = new DmlGenerator({ + paths, + ...options, + }) + + await dmlGenerator.run() + } + console.log(`Finished running.`) } diff --git a/www/utils/packages/docblock-generator/src/index.ts b/www/utils/packages/docblock-generator/src/index.ts index fda2f90ef5..c15697f808 100644 --- a/www/utils/packages/docblock-generator/src/index.ts +++ b/www/utils/packages/docblock-generator/src/index.ts @@ -6,6 +6,7 @@ import runGitChanges from "./commands/run-git-changes.js" import runGitCommit from "./commands/run-git-commit.js" import runRelease from "./commands/run-release.js" import cleanOas from "./commands/clean-oas.js" +import cleanDml from "./commands/clean-dml.js" const program = new Command() @@ -13,7 +14,7 @@ program.name("docblock-generator").description("Generate TSDoc doc-blocks") // define common options const typeOption = new Option("--type ", "The type of docs to generate.") - .choices(["all", "docs", "oas"]) + .choices(["all", "docs", "oas", "dml"]) .default("all") const generateExamplesOption = new Option( @@ -68,4 +69,11 @@ program ) .action(cleanOas) +program + .command("clean:dml") + .description( + "Check generated DML files under the `dml-output` directory and remove any data models that no longer exists." + ) + .action(cleanDml) + program.parse() diff --git a/www/utils/packages/docblock-generator/src/types/index.d.ts b/www/utils/packages/docblock-generator/src/types/index.d.ts index b40a2c743e..7c2e047a8d 100644 --- a/www/utils/packages/docblock-generator/src/types/index.d.ts +++ b/www/utils/packages/docblock-generator/src/types/index.d.ts @@ -12,7 +12,7 @@ export declare type OpenApiOperation = Partial & { } export declare type CommonCliOptions = { - type: "all" | "oas" | "docs" + type: "all" | "oas" | "docs" | "dml" generateExamples?: boolean tag?: string } @@ -28,3 +28,12 @@ export declare interface OpenApiTagObject extends OpenAPIV3.TagObject { export declare interface OpenApiDocument extends OpenAPIV3.Document { tags?: OpenApiTagObject[] } + +export declare type DmlObject = Record + +export declare type DmlFile = { + [k: string]: { + filePath: string + properties: DmlObject + } +} diff --git a/www/utils/packages/docblock-generator/src/utils/get-base-path.ts b/www/utils/packages/docblock-generator/src/utils/get-base-path.ts new file mode 100644 index 0000000000..e030a89802 --- /dev/null +++ b/www/utils/packages/docblock-generator/src/utils/get-base-path.ts @@ -0,0 +1,15 @@ +/** + * Retrieve the pathname of a file without the relative part before `packages/` + * + * @param fileName - The file name/path + * @returns The path without the relative part. + */ +export default function getBasePath(fileName: string) { + let basePath = fileName + const packageIndex = fileName.indexOf("packages/") + if (packageIndex) { + basePath = basePath.substring(packageIndex) + } + + return basePath +} diff --git a/www/utils/packages/docblock-generator/src/utils/get-monorepo-root.ts b/www/utils/packages/docblock-generator/src/utils/get-monorepo-root.ts index b5040b0ffc..0ab751f76b 100644 --- a/www/utils/packages/docblock-generator/src/utils/get-monorepo-root.ts +++ b/www/utils/packages/docblock-generator/src/utils/get-monorepo-root.ts @@ -7,7 +7,7 @@ import dirname from "./dirname.js" * * @returns {string} The absolute path to the monorepository. */ -export default function getMonorepoRoot() { +export default function getMonorepoRoot(): string { return ( process.env.MONOREPO_ROOT_PATH || path.join(dirname(import.meta.url), "..", "..", "..", "..", "..", "..") diff --git a/www/utils/packages/docblock-generator/src/utils/get-oas-output-base-path.ts b/www/utils/packages/docblock-generator/src/utils/get-oas-output-base-path.ts deleted file mode 100644 index ae2c91c875..0000000000 --- a/www/utils/packages/docblock-generator/src/utils/get-oas-output-base-path.ts +++ /dev/null @@ -1,6 +0,0 @@ -import path from "path" -import getMonorepoRoot from "./get-monorepo-root.js" - -export default function getOasOutputBasePath() { - return path.join(getMonorepoRoot(), "www", "utils", "generated", "oas-output") -} diff --git a/www/utils/packages/docblock-generator/src/utils/get-output-base-paths.ts b/www/utils/packages/docblock-generator/src/utils/get-output-base-paths.ts new file mode 100644 index 0000000000..f73668b410 --- /dev/null +++ b/www/utils/packages/docblock-generator/src/utils/get-output-base-paths.ts @@ -0,0 +1,16 @@ +import path from "path" +import getMonorepoRoot from "./get-monorepo-root.js" + +/** + * Retrieves the base path to the `oas-output` directory. + */ +export function getOasOutputBasePath() { + return path.join(getMonorepoRoot(), "www", "utils", "generated", "oas-output") +} + +/** + * Retrieves the base path to the `dml-output` directory + */ +export function getDmlOutputBasePath() { + return path.join(getMonorepoRoot(), "www", "utils", "generated", "dml-output") +} diff --git a/www/utils/packages/docblock-generator/src/utils/to-json-formatted.ts b/www/utils/packages/docblock-generator/src/utils/to-json-formatted.ts new file mode 100644 index 0000000000..15b46d559d --- /dev/null +++ b/www/utils/packages/docblock-generator/src/utils/to-json-formatted.ts @@ -0,0 +1,9 @@ +/** + * Retrieves the stringified JSON of a variable formatted. + * + * @param item The item to stringify + * @returns The formatted JSON string + */ +export default function toJsonFormatted(item: unknown): string { + return JSON.stringify(item, undefined, "\t") +} diff --git a/www/utils/packages/utils/src/str-formatting.ts b/www/utils/packages/utils/src/str-formatting.ts index e11c409bc6..870f96afff 100644 --- a/www/utils/packages/utils/src/str-formatting.ts +++ b/www/utils/packages/utils/src/str-formatting.ts @@ -32,6 +32,13 @@ export function snakeToWords(str: string): string { return str.replaceAll("_", " ").toLowerCase() } +export function snakeToPascal(str: string): string { + return str + .split("_") + .map((word) => capitalize(word)) + .join("") +} + export function kebabToTitle(str: string): string { return str .split("-") @@ -76,6 +83,10 @@ export function pascalToCamel(str: string): string { return `${str.charAt(0).toLowerCase()}${str.substring(1)}` } +export function pascalToWords(str: string): string { + return str.replaceAll(/([A-Z])/g, " $1") +} + /** * Remove parts of the name such as DTO, Filterable, etc... * diff --git a/www/utils/turbo.json b/www/utils/turbo.json index e5ca6e4078..cc260a10f5 100644 --- a/www/utils/turbo.json +++ b/www/utils/turbo.json @@ -20,6 +20,11 @@ "dependsOn": [ "^build" ] + }, + "generate:dml": { + "dependsOn": [ + "^build" + ] } } } \ No newline at end of file