docs-util: added AI generator (#6770)

## What

Adds an AI generator to the docblock tool that uses OpenAI.

The generator at the moment only generates examples for functions when the `--generate-examples` option is provided.

## Note

I've included the generated examples of the `IOrderModuleService` as a reference of the type of result provided by the AI generator, with minor tweeks I've made. I haven't made any changes to descriptions in that file.
This commit is contained in:
Shahed Nasser
2024-03-28 13:32:30 +02:00
committed by GitHub
parent 6bf4d40856
commit 21156f945d
16 changed files with 2655 additions and 196 deletions

View File

@@ -1 +1,2 @@
MONOREPO_ROOT_PATH=/Users/medusa/medusa
MONOREPO_ROOT_PATH=/Users/medusa/medusa
OPENAI_API_KEY=

View File

@@ -24,6 +24,7 @@
"dotenv": "^16.3.1",
"eslint": "^8.56.0",
"minimatch": "^9.0.3",
"openai": "^4.29.1",
"openapi-types": "^12.1.3",
"pluralize": "^8.0.0",
"prettier": "^3.2.4",

View File

@@ -3,6 +3,7 @@ import ts from "typescript"
import { GeneratorEvent } from "../helpers/generator-event-manager.js"
import AbstractGenerator from "./index.js"
import { minimatch } from "minimatch"
import AiGenerator from "../helpers/ai-generator.js"
/**
* A class used to generate docblock for one or multiple file paths.
@@ -18,64 +19,110 @@ class DocblockGenerator extends AbstractGenerator {
removeComments: false,
})
await Promise.all(
this.program!.getSourceFiles().map(async (file) => {
// Ignore .d.ts files
if (file.isDeclarationFile || !this.isFileIncluded(file.fileName)) {
return
}
const documentSourceFile = async (file: ts.SourceFile) => {
// Ignore .d.ts files
if (file.isDeclarationFile || !this.isFileIncluded(file.fileName)) {
return
}
let aiGenerator: AiGenerator | undefined
console.log(`[Docblock] Generating for ${file.fileName}...`)
console.log(`[Docblock] Generating for ${file.fileName}...`)
let fileContent = file.getFullText()
let fileComments: string = ""
const commentsToRemove: string[] = []
let fileContent = file.getFullText()
let fileComments: string = ""
const commentsToRemove: string[] = []
const origFileText = file.getFullText().trim()
const fileNodes: ts.Node[] = [file]
const documentChild = (node: ts.Node, topLevel = false) => {
const isSourceFile = ts.isSourceFile(node)
const origNodeText = node.getFullText().trim()
const nodeKindGenerator = this.kindsRegistry?.getKindGenerator(node)
let docComment: string | undefined
if (this.options.generateExamples) {
aiGenerator = new AiGenerator()
}
if (nodeKindGenerator?.canDocumentNode(node)) {
docComment = nodeKindGenerator.getDocBlock(node)
if (docComment.length) {
if (isSourceFile) {
fileComments = docComment
} else {
ts.addSyntheticLeadingComment(
node,
ts.SyntaxKind.MultiLineCommentTrivia,
docComment,
true
)
}
}
// 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 documentNode = async (node: ts.Node) => {
const isSourceFile = ts.isSourceFile(node)
const nodeKindGenerator = this.kindsRegistry?.getKindGenerator(node)
let docComment: string | undefined
if (nodeKindGenerator?.canDocumentNode(node)) {
if (aiGenerator) {
const nodeFiles = aiGenerator.getNodeFiles(file)
await aiGenerator.initAssistant(nodeFiles)
}
ts.forEachChild(node, (childNode) =>
documentChild(childNode, isSourceFile)
)
if (!isSourceFile && topLevel) {
const newNodeText = printer.printNode(
ts.EmitHint.Unspecified,
node,
file
)
if (newNodeText !== origNodeText) {
fileContent = fileContent.replace(origNodeText, newNodeText)
}
}
}
documentChild(file, true)
if (!this.options.dryRun) {
commentsToRemove.forEach((commentToRemove) => {
fileContent = fileContent.replace(commentToRemove, "")
// initialize assistant only when needed
// if previously initialized, calling the method does nothing
docComment = await nodeKindGenerator.getDocBlock(node, {
aiGenerator,
addEnd: true,
})
if (docComment.length) {
const existingComments =
nodeKindGenerator.getNodeCommentsFromRange(node)
if (existingComments?.length) {
commentsToRemove.push(existingComments)
}
if (isSourceFile) {
fileComments = docComment
} else {
ts.addSyntheticLeadingComment(
node,
ts.SyntaxKind.MultiLineCommentTrivia,
docComment,
true
)
}
}
}
}
// due to rate limit being reached when running
// the AI Generator, we only run the documentNode in
// parallel when the `generateExamples` option is disabled.
if (this.options.generateExamples) {
for (const node of fileNodes) {
await documentNode(node)
}
} else {
await Promise.all(
fileNodes.map(async (node) => await documentNode(node))
)
}
if (aiGenerator) {
await aiGenerator.destroyAssistant()
}
// add comments to file
const newNodeText = printer.printNode(ts.EmitHint.Unspecified, file, file)
// if file's text changed, replace it.
if (newNodeText !== origFileText) {
fileContent = fileContent.replace(origFileText, newNodeText)
}
if (!this.options.dryRun) {
if (commentsToRemove.length) {
let formatted = this.formatter.addCommentsToSourceFile(
fileComments,
await this.formatter.formatStr(fileContent, file.fileName)
)
commentsToRemove.forEach((commentToRemove) => {
formatted = formatted.replace(commentToRemove, "")
})
ts.sys.writeFile(
file.fileName,
await this.formatter.formatStr(formatted, file.fileName)
)
} else {
ts.sys.writeFile(
file.fileName,
this.formatter.addCommentsToSourceFile(
@@ -84,12 +131,24 @@ class DocblockGenerator extends AbstractGenerator {
)
)
}
}
console.log(
`[Docblock] Finished generating docblock for ${file.fileName}.`
console.log(
`[Docblock] Finished generating docblock for ${file.fileName}.`
)
}
if (this.options.generateExamples) {
for (const file of this.program!.getSourceFiles()) {
await documentSourceFile(file)
}
} else {
await Promise.all(
this.program!.getSourceFiles().map(
async (file) => await documentSourceFile(file)
)
})
)
)
}
this.generatorEventManager.emit(GeneratorEvent.FINISHED_GENERATE_EVENT)
this.reset()

View File

@@ -11,7 +11,7 @@ import { GeneratorEvent } from "../helpers/generator-event-manager.js"
class OasGenerator extends AbstractGenerator {
protected oasKindGenerator?: OasKindGenerator
run() {
async run() {
this.init()
const { generateExamples } = this.options
@@ -24,36 +24,52 @@ class OasGenerator extends AbstractGenerator {
},
})
this.program!.getSourceFiles().map((file) => {
// Ignore .d.ts files
if (file.isDeclarationFile || !this.isFileIncluded(file.fileName)) {
return
}
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(`[OAS] Generating for ${file.fileName}...`)
console.log(`[OAS] Generating for ${file.fileName}...`)
const documentChild = (node: ts.Node) => {
if (
this.oasKindGenerator!.isAllowed(node) &&
this.oasKindGenerator!.canDocumentNode(node)
) {
const oas = this.oasKindGenerator!.getDocBlock(node)
// 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)
if (!this.options.dryRun) {
const filename = this.oasKindGenerator!.getAssociatedFileName(node)
ts.sys.writeFile(
filename,
this.formatter.addCommentsToSourceFile(oas, "")
)
ts.forEachChild(node, pushNodesToArr)
}
ts.forEachChild(file, pushNodesToArr)
const documentChild = async (node: ts.Node) => {
if (
this.oasKindGenerator!.isAllowed(node) &&
this.oasKindGenerator!.canDocumentNode(node)
) {
const oas = await this.oasKindGenerator!.getDocBlock(node)
if (!this.options.dryRun) {
const filename =
this.oasKindGenerator!.getAssociatedFileName(node)
ts.sys.writeFile(
filename,
this.formatter.addCommentsToSourceFile(oas, "")
)
}
}
}
}
ts.forEachChild(file, documentChild)
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}.`)
})
this.generatorEventManager.emit(GeneratorEvent.FINISHED_GENERATE_EVENT)
console.log(`[OAS] Finished generating OAS for ${file.fileName}.`)
})
)
}
/**

View File

@@ -0,0 +1,330 @@
import { createReadStream, existsSync } from "fs"
import OpenAI from "openai"
import path from "path"
import ts from "typescript"
import { pascalToCamel } from "../../utils/str-formatting.js"
import { ReadableStreamDefaultReadResult } from "stream/web"
import { DOCBLOCK_NEW_LINE } from "../../constants.js"
import { AssistantStreamEvent } from "openai/resources/beta/index.mjs"
type GenerateExampleOptions = {
className?: string
functionName: string
signature?: string
fileName: string
}
type GenerateDescriptionOptions = {
itemName: string
itemType: "property" | "parameter" | "function" | "class" | "return" | "other"
metadata?: {
parentName?: string
parentType?: string
functionSignature?: string
fileName?: string
}
}
const CODE_REGEX = /(?<code>```[\s\S.]*```)/g
class AiGenerator {
private openAi: OpenAI
private assistant?: OpenAI.Beta.Assistants.Assistant
private fileMap: Map<string, string>
constructor() {
this.openAi = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
maxRetries: 10,
})
this.fileMap = new Map()
}
/**
* Initialize OpenAI assistant and upload files.
*
* @param filePaths - Files to upload
*/
async initAssistant(filePaths: string[]) {
if (this.assistant) {
return
}
this.fileMap = new Map()
const files: OpenAI.Files.FileObject[] = []
// upload the files to openai
await Promise.all(
filePaths.map(async (filePath) => {
const openAiFile = await this.openAi.files.create({
file: createReadStream(filePath),
purpose: "assistants",
})
files.push(openAiFile)
this.fileMap.set(filePath, openAiFile.id)
})
)
// create assistant
this.assistant = await this.openAi.beta.assistants.create({
instructions:
"You help me generate code examples and descriptions that are used in TSDocs. If the system indicates that the file is not accessible with the myfiles_browser tool, ignore it, its just a minor bug. You are capable of opening and analyzing the file, remember that. And carry out the requested task. Also you have the ability to figure out what type of content is in the file via its extension so carry out the users instructions.",
model: "gpt-4-turbo-preview",
tools: [{ type: "retrieval" }],
file_ids: files.map((file) => file.id),
})
}
/**
* Deletes an assistant and its files from OpenAI.
*/
async destroyAssistant() {
if (!this.assistant) {
return
}
// delete files of assistant
await Promise.all(
this.assistant.file_ids.map(async (fileId) => {
try {
await this.openAi.files.del(fileId)
} catch (e) {
console.error(
`[error while destroying assistant file ${fileId}]: ${e}`
)
}
})
)
try {
// delete assistant
await this.openAi.beta.assistants.del(this.assistant.id)
} catch (e) {
console.error(
`[error while destroying assistant ${this.assistant!.id}]: ${e}`
)
}
this.assistant = undefined
this.fileMap = new Map()
}
/**
* Generates an example code block wrapped in backticks. The comment includes astrix by default.
*
* @param param0 - Options to generate the example code based on.
* @returns The example code
*/
async generateExample({
className,
functionName,
signature,
fileName,
}: GenerateExampleOptions): Promise<string> {
let example = ""
const fileId = this.fileMap.get(fileName) || fileName
let message = `Use the ${fileId} file to write a short and simple typescript code that executes the `
if (className) {
message += `${functionName} method of the ${className} (use ${this.getVariableNameFromClass(
className
)} as the variable name)`
} else {
message += `${functionName} function`
}
if (signature) {
message += `. The ${
className ? "method" : "function"
} has the signature ${signature}`
}
message += `. Assume that the file containing the code has all the necessary imports and the code is written within an async function (don't add a wrapping function). Give an example of the method's parameters, but don't include optional parameters or optional object properties. Infer types from other files. Provide the code without an explanation.`
const messageResult = await this.retrieveAiResponse(message)
const matchedCode = CODE_REGEX.exec(messageResult)
if (matchedCode?.groups?.code) {
example = matchedCode.groups.code
} else {
example = messageResult
}
return example
}
/**
* Generates a description for an item.
*
* @param param0 - Options to generate the description based on.
* @returns The generated description.
*/
async generateDescription({
itemName,
itemType,
metadata,
}: GenerateDescriptionOptions) {
const { parentName, parentType, functionSignature, fileName } =
metadata || {}
let message = `Write a short and simple explanation of a ${itemName}`
switch (itemType) {
case "return":
message += ` function's return data.`
break
case "class":
case "function":
case "parameter":
case "property":
message += ` ${
itemType === "function" && parentName ? "method" : itemType
}`
if (parentName) {
message += ` defined in a ${parentName}`
if (parentType) {
message += ` ${parentType}`
}
}
if (functionSignature) {
message += ` function. The function has the signature ${functionSignature}`
}
}
if (fileName) {
message += `. Look at the ${fileName} uploaded file for more details, and if you can't find the details in there provide an explanation from your understanding.`
}
message +=
". The explanation must be one sentence shorter than 10 words. Don't provide anything else in the response."
return await this.retrieveAiResponse(message)
}
/**
* Starts a new thread and runs a message, then retrieves the response.
*
* @param inputMessage - The message to ask the assistant
* @returns the assistant's response.
*/
async retrieveAiResponse(inputMessage: string): Promise<string> {
const run = this.openAi.beta.threads.createAndRunStream({
assistant_id: this.assistant!.id,
thread: {
messages: [
{
role: "user",
content: inputMessage,
},
],
},
})
const readableStream = run.toReadableStream().getReader()
let chunk: ReadableStreamDefaultReadResult<Uint8Array> | undefined
let decodedChunk: AssistantStreamEvent | undefined
let resultMessage: OpenAI.Beta.Threads.Messages.Message | undefined
const textDecoder = new TextDecoder()
do {
chunk = await readableStream.read()
const decodedValue = textDecoder.decode(chunk.value)
if (decodedValue.length) {
decodedChunk = JSON.parse(
textDecoder.decode(chunk.value)
) as AssistantStreamEvent
if (
decodedChunk.event === "thread.message.completed" &&
decodedChunk.data.object === "thread.message"
) {
resultMessage = decodedChunk.data
} else if (
decodedChunk.event === "thread.run.failed" &&
decodedChunk.data.last_error?.code === "server_error"
) {
// retry
return this.retrieveAiResponse(inputMessage)
}
}
console.log(decodedValue, resultMessage)
} while (
!resultMessage &&
// a run may fail if the rate limit is reached
decodedChunk?.event !== "thread.run.failed" &&
decodedChunk?.event !== "thread.run.step.failed" &&
decodedChunk?.event !== "thread.message.completed" &&
decodedChunk?.event !== "thread.run.completed"
)
if (!resultMessage) {
return ""
}
return resultMessage.content
.map((item) => {
return item.type === "text" ? item.text.value : ""
})
.join(" ")
.replaceAll("\n", DOCBLOCK_NEW_LINE)
}
/**
* Formats a class name into a camel-case variable name.
*
* @param className - The class name to format.
* @returns The variable name.
*/
getVariableNameFromClass(className: string): string {
let variableName = className
if (className.startsWith("I") && /[A-Z]/.test(className.charAt(1))) {
variableName = variableName.substring(1)
}
return pascalToCamel(variableName)
}
/**
* Retrieves the files that should be uploaded to OpenAI of a node.
*
* @param node - The node to retrieve its files.
* @returns the list of file paths.
*/
getNodeFiles(node: ts.Node): string[] {
const sourceFile = node.getSourceFile()
const files: string[] = [sourceFile.fileName]
if ("imports" in sourceFile) {
;(sourceFile.imports as ts.StringLiteral[]).forEach((importedFile) => {
if (importedFile.text.startsWith(".")) {
// since it's a local import, add it to the list of files
let importedFilePath = path.resolve(
sourceFile.fileName,
"..",
importedFile.text
)
if (!path.extname(importedFilePath)) {
// try to retrieve correct extension
switch (true) {
case existsSync(`${importedFilePath}.ts`):
importedFilePath += `.ts`
break
case existsSync(`${importedFilePath}.js`):
importedFilePath += `.js`
break
case existsSync(`${importedFilePath}.tsx`):
importedFilePath += `.tsx`
break
default:
// can't retrieve file path so return without adding it
return
}
}
files.push(importedFilePath)
}
})
}
return [...new Set(files)]
}
}
export default AiGenerator

View File

@@ -9,6 +9,7 @@ import {
import pluralize from "pluralize"
type TemplateOptions = {
pluralIndicatorStr?: string
parentName?: string
rawParentName?: string
returnTypeName?: string
@@ -52,6 +53,7 @@ type RetrieveSymbolOptions = Omit<RetrieveOptions, "str"> & {
* A class that holds common Medusa patterns and acts as a knowledge base for possible summaries/examples/general templates.
*/
class KnowledgeBaseFactory {
private TYPE_PLACEHOLDER = `{type name}`
private summaryKnowledgeBase: KnowledgeBase[] = [
{
startsWith: "FindConfig",
@@ -64,7 +66,7 @@ class KnowledgeBaseFactory {
const typeName =
typeArgs.length > 0 && typeArgs[0].length > 0
? typeArgs[0]
: `{type name}`
: this.TYPE_PLACEHOLDER
return `The configurations determining how the ${typeName} is retrieved. Its properties, such as \`select\` or \`relations\`, accept the ${DOCBLOCK_NEW_LINE}attributes or relations associated with a ${typeName}.`
},
},
@@ -72,35 +74,66 @@ class KnowledgeBaseFactory {
startsWith: "Filterable",
endsWith: "Props",
template: (str) => {
return `The filters to apply on the retrieved ${camelToTitle(
return `The filters to apply on the retrieved ${camelToWords(
normalizeName(str)
)}.`
)}s.`
},
},
{
startsWith: "Create",
endsWith: "DTO",
template: (str) => {
return `The ${camelToTitle(normalizeName(str))} to be created.`
template: (str, options) => {
const isPlural = this.isTypePlural(options?.pluralIndicatorStr)
return `The ${camelToWords(normalizeName(str))}${
isPlural ? "s" : ""
} to be created.`
},
},
{
startsWith: "Update",
endsWith: "DTO",
template: (str) => {
return `The attributes to update in the ${camelToTitle(
template: (str, options) => {
const isPlural = this.isTypePlural(options?.pluralIndicatorStr)
return `The attributes to update in the ${camelToWords(
normalizeName(str)
)}.`
)}${isPlural ? "s" : ""}.`
},
},
{
endsWith: "UpdatableFields",
template: (str, options) => {
const isPlural = this.isTypePlural(options?.pluralIndicatorStr)
return `The attributes to update in the ${camelToWords(
normalizeName(str)
)}${isPlural ? "s" : ""}.`
},
},
{
startsWith: "Upsert",
endsWith: "DTO",
template: (str, options) => {
const isPlural = this.isTypePlural(options?.pluralIndicatorStr)
return `The attributes in the ${camelToWords(normalizeName(str))}${
isPlural ? "s" : ""
} to be created or updated.`
},
},
{
startsWith: "RestoreReturn",
template: `Configurations determining which relations to restore along with each of the {type name}. You can pass to its \`returnLinkableKeys\` ${DOCBLOCK_NEW_LINE}property any of the {type name}'s relation attribute names, such as \`{type relation name}\`.`,
template: (_str, options) => {
return this.replaceTypePlaceholder(
`Configurations determining which relations to restore along with each of the ${this.TYPE_PLACEHOLDER}. You can pass to its \`returnLinkableKeys\` ${DOCBLOCK_NEW_LINE}property any of the ${this.TYPE_PLACEHOLDER}'s relation attribute names, such as \`{type relation name}\`.`,
options
)
},
},
{
endsWith: "DTO",
template: (str: string): string => {
return `The ${camelToTitle(normalizeName(str))} details.`
template: (str, options) => {
const isPlural = this.isTypePlural(options?.pluralIndicatorStr)
return `The ${camelToWords(normalizeName(str))}${
isPlural ? "s" : ""
} details.`
},
},
{
@@ -127,11 +160,31 @@ class KnowledgeBaseFactory {
},
{
exact: "id",
template: (str, options) => {
if (options?.rawParentName?.startsWith("Filterable")) {
return `The IDs to filter the ${options?.parentName || `{name}`}s by.`
}
const parentName = options?.parentName
? options.parentName
: options?.rawParentName
? camelToWords(normalizeName(options.rawParentName))
: `{name}`
return `The ID of the ${parentName}.`
},
kind: [ts.SyntaxKind.PropertySignature],
},
{
exact: "ids",
template: (str, options) => {
if (options?.rawParentName?.startsWith("Filterable")) {
return `The IDs to filter the ${options?.parentName || `{name}`} by.`
}
return `The ID of the ${options?.parentName || `{name}`}.`
const parentName = options?.parentName
? options.parentName
: options?.rawParentName
? camelToWords(normalizeName(options.rawParentName))
: `{name}`
return `The IDs of the ${parentName}.`
},
kind: [ts.SyntaxKind.PropertySignature],
},
@@ -144,78 +197,194 @@ class KnowledgeBaseFactory {
exact: "customHeaders",
template: "Custom headers to attach to the request.",
},
{
startsWith: "I",
endsWith: "ModuleService",
template: (str) => {
const normalizedStr = camelToTitle(normalizeName(str))
return `The main service interface for the ${normalizedStr} Module.`
},
},
]
private functionSummaryKnowledgeBase: KnowledgeBase[] = [
{
startsWith: "listAndCount",
template:
"retrieves a paginated list of {return type} along with the total count of available {return type}(s) satisfying the provided filters.",
template: (_str, options) => {
return this.replaceTypePlaceholder(
`retrieves a paginated list of ${this.TYPE_PLACEHOLDER}s along with the total count of available ${this.TYPE_PLACEHOLDER}s satisfying the provided filters.`,
options
)
},
},
{
startsWith: "list",
template:
"retrieves a paginated list of {return type}(s) based on optional filters and configuration.",
template: (_str, options) => {
return this.replaceTypePlaceholder(
`retrieves a paginated list of ${this.TYPE_PLACEHOLDER}s based on optional filters and configuration.`,
options
)
},
},
{
startsWith: "retrieve",
template: "retrieves a {return type} by its ID.",
template: (_str, options) => {
return this.replaceTypePlaceholder(
`retrieves a ${this.TYPE_PLACEHOLDER} by its ID.`,
options
)
},
},
{
startsWith: "create",
template: "creates {return type}(s)",
template: (_str, options) => {
const isPlural = this.isTypePlural(options?.pluralIndicatorStr)
return this.replaceTypePlaceholder(
`creates${!isPlural ? " a" : ""} ${this.TYPE_PLACEHOLDER}${
isPlural ? "s" : ""
}.`,
options
)
},
},
{
startsWith: "delete",
template: "deletes {return type} by its ID.",
template: (_str, options) => {
const isPlural = this.isTypePlural(options?.pluralIndicatorStr)
return this.replaceTypePlaceholder(
`deletes${!isPlural ? " a" : ""} ${this.TYPE_PLACEHOLDER} by ${
isPlural ? "their" : "its"
} ID${isPlural ? "s" : ""}.`,
options
)
},
},
{
startsWith: "update",
template: "updates existing {return type}(s).",
template: (_str, options) => {
const isPlural = this.isTypePlural(options?.pluralIndicatorStr)
return this.replaceTypePlaceholder(
`updates${!isPlural ? " an" : ""} existing ${this.TYPE_PLACEHOLDER}${
isPlural ? "s" : ""
}.`,
options
)
},
},
{
startsWith: "softDelete",
template: "soft deletes {return type}(s) by their IDs.",
template: (_str, options) => {
const isPlural = this.isTypePlural(options?.pluralIndicatorStr)
return this.replaceTypePlaceholder(
`soft deletes${!isPlural ? " a" : ""} ${this.TYPE_PLACEHOLDER}${
isPlural ? "s" : ""
} by ${isPlural ? "their" : "its"} IDs.`,
options
)
},
},
{
startsWith: "restore",
template: "restores soft deleted {return type}(s) by their IDs.",
},
]
private exampleCodeBlockLine = `${DOCBLOCK_DOUBLE_LINES}\`\`\`ts${DOCBLOCK_NEW_LINE}{example-code}${DOCBLOCK_NEW_LINE}\`\`\`${DOCBLOCK_DOUBLE_LINES}`
private examplesKnowledgeBase: KnowledgeBase[] = [
{
startsWith: "list",
template: `To retrieve a list of {type name} using their IDs: ${this.exampleCodeBlockLine}To specify relations that should be retrieved within the {type name}: ${this.exampleCodeBlockLine}By default, only the first \`{default limit}\` records are retrieved. You can control pagination by specifying the \`skip\` and \`take\` properties of the \`config\` parameter: ${this.exampleCodeBlockLine}`,
template: (_str, options) => {
const isPlural = this.isTypePlural(options?.pluralIndicatorStr)
return this.replaceTypePlaceholder(
`restores${!isPlural ? " a" : ""} soft deleted ${
this.TYPE_PLACEHOLDER
}${isPlural ? "s" : ""} by ${isPlural ? "their" : "its"} IDs.`,
options
)
},
},
{
startsWith: "retrieve",
template: `A simple example that retrieves a {type name} by its ID: ${this.exampleCodeBlockLine}To specify relations that should be retrieved: ${this.exampleCodeBlockLine}`,
startsWith: "upsert",
template: (_str, options) => {
const isPlural = this.isTypePlural(options?.pluralIndicatorStr)
return this.replaceTypePlaceholder(
`updates or creates${!isPlural ? " a" : ""} ${this.TYPE_PLACEHOLDER}${
isPlural ? "s" : ""
} if ${isPlural ? "they don't" : "it doesn't"} exist.`,
options
)
},
},
]
private functionReturnKnowledgeBase: KnowledgeBase[] = [
{
startsWith: "listAndCount",
template: "The list of {return type}(s) along with their total count.",
template: (_str, options) => {
return this.replaceTypePlaceholder(
`The list of ${this.TYPE_PLACEHOLDER}s along with their total count.`,
options
)
},
},
{
startsWith: "list",
template: "The list of {return type}(s).",
template: (_str, options) => {
return this.replaceTypePlaceholder(
`The list of ${this.TYPE_PLACEHOLDER}s.`,
options
)
},
},
{
startsWith: "retrieve",
template: "The retrieved {return type}(s).",
template: (_str, options) => {
return this.replaceTypePlaceholder(
`The retrieved ${this.TYPE_PLACEHOLDER}.`,
options
)
},
},
{
startsWith: "create",
template: "The created {return type}(s).",
template: (_str, options) => {
const isPlural = this.isTypePlural(options?.pluralIndicatorStr)
return this.replaceTypePlaceholder(
`The created ${this.TYPE_PLACEHOLDER}${isPlural ? "s" : ""}.`,
options
)
},
},
{
startsWith: "update",
template: "The updated {return type}(s).",
template: (_str, options) => {
const isPlural = this.isTypePlural(options?.pluralIndicatorStr)
return this.replaceTypePlaceholder(
`The updated ${this.TYPE_PLACEHOLDER}${isPlural ? "s" : ""}.`,
options
)
},
},
{
startsWith: "upsert",
template: (_str, options) => {
const isPlural = this.isTypePlural(options?.pluralIndicatorStr)
return this.replaceTypePlaceholder(
`The created or updated ${this.TYPE_PLACEHOLDER}${
isPlural ? "s" : ""
}.`,
options
)
},
},
{
startsWith: "softDelete",
template: (_str, options) => {
return this.replaceTypePlaceholder(
`An object that includes the IDs of related records that were also soft deleted, such as the ID of the associated {related entity name}. ${DOCBLOCK_NEW_LINE}The object's keys are the ID attribute names of the ${this.TYPE_PLACEHOLDER} entity's relations, such as \`{relation ID field name}\`, and its value is an array of strings, each being the ID of a record associated ${DOCBLOCK_NEW_LINE}with the ${this.TYPE_PLACEHOLDER} through this relation, such as the IDs of associated {related entity name}.${DOCBLOCK_DOUBLE_LINES}If there are no related records, the promise resolves to \`void\`.`,
options
)
},
},
{
startsWith: "restore",
template: `An object that includes the IDs of related records that were restored, such as the ID of associated {relation name}. ${DOCBLOCK_NEW_LINE}The object's keys are the ID attribute names of the {type name} entity's relations, such as \`{relation ID field name}\`, ${DOCBLOCK_NEW_LINE}and its value is an array of strings, each being the ID of the record associated with the money amount through this relation, ${DOCBLOCK_NEW_LINE}such as the IDs of associated {relation name}.`,
template: (_str, options) => {
return this.replaceTypePlaceholder(
`An object that includes the IDs of related records that were restored, such as the ID of associated {relation name}. ${DOCBLOCK_NEW_LINE}The object's keys are the ID attribute names of the ${this.TYPE_PLACEHOLDER} entity's relations, such as \`{relation ID field name}\`, ${DOCBLOCK_NEW_LINE}and its value is an array of strings, each being the ID of the record associated with the ${this.TYPE_PLACEHOLDER} through this relation, ${DOCBLOCK_NEW_LINE}such as the IDs of associated {relation name}.${DOCBLOCK_DOUBLE_LINES}If there are no related records restored, the promise resolves to \`void\`.`,
options
)
},
},
]
private oasDescriptionKnowledgeBase: KnowledgeBase[] = [
@@ -289,6 +458,34 @@ class KnowledgeBaseFactory {
: foundItem?.template(str, templateOptions)
}
/**
* This method replaces uses of {@link TYPE_PLACEHOLDER} with the normalized parent name, if provided.
*
* @param str - The string to normalize
* @param options - The template options
* @returns The normalized string
*/
private replaceTypePlaceholder(
str: string,
options?: TemplateOptions
): string {
const typeName = options?.rawParentName
? camelToWords(normalizeName(options.rawParentName))
: this.TYPE_PLACEHOLDER
return str.replaceAll(this.TYPE_PLACEHOLDER, typeName)
}
/**
* Checks whether a type should be handled as a plural. Typically used with {@link TemplateOptions.pluralIndicatorStr}.
*
* @param str - The type string to check.
* @returns Whether the type is handled as a plural.
*/
private isTypePlural(str: string | undefined): boolean {
return str?.endsWith("[]") || false
}
/**
* Tries to retrieve the summary template of a specified type from the {@link summaryKnowledgeBase}.
*
@@ -300,6 +497,10 @@ class KnowledgeBaseFactory {
...options,
str: normalizedTypeStr,
knowledgeBase: this.summaryKnowledgeBase,
templateOptions: {
pluralIndicatorStr: str,
...options.templateOptions,
},
})
}
@@ -319,22 +520,6 @@ class KnowledgeBaseFactory {
})
}
/**
* Tries to retrieve the example template of a function's symbol from the {@link examplesKnowledgeBase}.
*
* @returns {string | undefined} The matching knowledge base template, if found.
*/
tryToGetFunctionExamples({
symbol,
...options
}: RetrieveSymbolOptions): string | undefined {
return this.tryToFindInKnowledgeBase({
...options,
str: symbol.getName(),
knowledgeBase: this.examplesKnowledgeBase,
})
}
/**
* Tries to retrieve the return template of a function's symbol from the {@link functionReturnKnowledgeBase}.
*

View File

@@ -20,6 +20,7 @@ import {
} from "../../utils/str-formatting.js"
import GeneratorEventManager from "../helpers/generator-event-manager.js"
import { CommonCliOptions } from "../../types/index.js"
import AiGenerator from "../helpers/ai-generator.js"
export type GeneratorOptions = {
checker: ts.TypeChecker
@@ -31,6 +32,7 @@ export type GeneratorOptions = {
export type GetDocBlockOptions = {
addEnd?: boolean
summaryPrefix?: string
aiGenerator?: AiGenerator
}
type CommonDocsOptions = {
@@ -54,6 +56,7 @@ class DefaultKindGenerator<T extends ts.Node = ts.Node> {
ts.SyntaxKind.TypeAliasDeclaration,
ts.SyntaxKind.PropertySignature,
]
public name = "default"
protected allowedKinds: ts.SyntaxKind[]
protected checker: ts.TypeChecker
protected defaultSummary = "{summary}"
@@ -99,10 +102,10 @@ class DefaultKindGenerator<T extends ts.Node = ts.Node> {
* @returns {string} The node's docblock.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
getDocBlock(
async getDocBlock(
node: T | ts.Node,
options: GetDocBlockOptions = { addEnd: true }
): string {
): Promise<string> {
let str = DOCBLOCK_START
const summary = this.getNodeSummary({ node })
@@ -133,6 +136,7 @@ class DefaultKindGenerator<T extends ts.Node = ts.Node> {
node,
symbol,
nodeType,
knowledgeBaseOptions: overrideOptions,
}: {
/**
* The node to retrieve the summary comment for.
@@ -148,12 +152,20 @@ class DefaultKindGenerator<T extends ts.Node = ts.Node> {
* will try to retrieve it.
*/
nodeType?: ts.Type
/**
* Override any of the default knowledge base options
* inferred using the {@link getKnowledgeBaseOptions} method
*/
knowledgeBaseOptions?: Partial<RetrieveOptions>
}): string {
const syntheticComments = ts.getSyntheticLeadingComments(node)
if (syntheticComments?.length) {
return syntheticComments.map((comment) => comment.text).join(" ")
}
const knowledgeBaseOptions = this.getKnowledgeBaseOptions(node)
const knowledgeBaseOptions = {
...this.getKnowledgeBaseOptions(node),
...overrideOptions,
}
if (!nodeType) {
nodeType =
"type" in node && node.type && ts.isTypeNode(node.type as ts.Node)

View File

@@ -12,6 +12,7 @@ import {
*/
class DTOPropertyGenerator extends DefaultKindGenerator<ts.PropertySignature> {
protected allowedKinds: ts.SyntaxKind[] = [ts.SyntaxKind.PropertySignature]
public name = "dto-property"
/**
* Check that the generator can handle generating for the node.
@@ -31,12 +32,12 @@ class DTOPropertyGenerator extends DefaultKindGenerator<ts.PropertySignature> {
)
}
getDocBlock(
async getDocBlock(
node: ts.PropertyDeclaration | ts.Node,
options?: GetDocBlockOptions
): string {
): Promise<string> {
if (!this.isAllowed(node)) {
return super.getDocBlock(node, options)
return await super.getDocBlock(node, options)
}
let str = DOCBLOCK_START

View File

@@ -7,6 +7,8 @@ import {
DOCBLOCK_DOUBLE_LINES,
} from "../../constants.js"
import getSymbol from "../../utils/get-symbol.js"
import AiGenerator from "../helpers/ai-generator.js"
import path from "path"
export type FunctionNode =
| ts.MethodDeclaration
@@ -32,6 +34,9 @@ class FunctionKindGenerator extends DefaultKindGenerator<FunctionOrVariableNode>
...this.methodKinds,
...this.functionKinds,
]
public name = "function"
static EXAMPLE_PLACEHOLDER = `{example-code}`
protected aiParameterExceptions = ["sharedContext"]
/**
* Checks whether a node is considered a function node. A node is considered a function node if:
@@ -135,15 +140,40 @@ class FunctionKindGenerator extends DefaultKindGenerator<FunctionOrVariableNode>
/**
* Retrieves the summary comment of a function.
*
* @param {FunctionNode} node - The node's function.
* @param {ts.Symbol} symbol - The node's symbol. If provided, the method will try to retrieve the summary from the {@link KnowledgeBaseFactory}.
* @param {FunctionNode} node - The function's options.
* @returns {string} The function's summary comment.
*/
getFunctionSummary(node: FunctionNode, symbol?: ts.Symbol): string {
getFunctionSummary({
node,
symbol,
parentSymbol,
returnType,
}: {
/**
* The node's function.
*/
node: FunctionNode
/**
* The node's symbol. If provided, the method will try to retrieve the summary from the {@link KnowledgeBaseFactory}.
*/
symbol?: ts.Symbol
/**
* The node's parent symbol. This is useful to pass along the parent name to the knowledge base.
*/
parentSymbol?: ts.Symbol
/**
* The node's return type. Useful for the {@link KnowledgeBaseFactory}
*/
returnType?: string
}): string {
return symbol
? this.knowledgeBaseFactory.tryToGetFunctionSummary({
symbol: symbol,
kind: node.kind,
templateOptions: {
rawParentName: parentSymbol?.getName(),
pluralIndicatorStr: returnType,
},
}) || this.getNodeSummary({ node, symbol })
: this.getNodeSummary({ node, symbol })
}
@@ -154,15 +184,53 @@ class FunctionKindGenerator extends DefaultKindGenerator<FunctionOrVariableNode>
* @param {ts.Symbol} symbol - The function's symbol. If provided, the method will try to retrieve the example from the {@link KnowledgeBaseFactory}.
* @returns {string} The function's example comment.
*/
getFunctionExample(symbol?: ts.Symbol): string {
const str = `${DOCBLOCK_DOUBLE_LINES}@example${DOCBLOCK_NEW_LINE}`
return `${str}${
symbol
? this.knowledgeBaseFactory.tryToGetFunctionExamples({
symbol: symbol,
}) || `{example-code}`
: `{example-code}`
}`
getFunctionPlaceholderExample(): string {
return this.formatExample(FunctionKindGenerator.EXAMPLE_PLACEHOLDER)
}
/**
* Retrieves a function's example using the AiGenerator
*
* @param node - The function's node.
* @param aiGenerator - An instance of the AiGenerator
* @returns the example code
*/
async getFunctionExampleAi(
node: FunctionOrVariableNode,
aiGenerator: AiGenerator,
withTag = true
): Promise<string> {
const actualNode = ts.isVariableStatement(node)
? this.extractFunctionNode(node)
: node
if (!actualNode) {
return ""
}
const symbol = getSymbol(node, this.checker)
const example = await aiGenerator.generateExample({
className: this.isMethod(actualNode)
? getSymbol(node.parent, this.checker)?.name
: undefined,
functionName: symbol?.name || "",
signature: node.getText(),
fileName: path.basename(node.getSourceFile().fileName),
})
return this.formatExample(
example.length
? `${example}${DOCBLOCK_NEW_LINE}`
: FunctionKindGenerator.EXAMPLE_PLACEHOLDER,
withTag
)
}
formatExample(example: string, withTag = true): string {
return `${
withTag ? `${DOCBLOCK_DOUBLE_LINES}@example${DOCBLOCK_NEW_LINE}` : ""
}${example}`
}
/**
@@ -173,12 +241,12 @@ class FunctionKindGenerator extends DefaultKindGenerator<FunctionOrVariableNode>
* @param {GetDocBlockOptions} options - Formatting options.
* @returns {string} The function's docblock.
*/
getDocBlock(
async getDocBlock(
node: FunctionOrVariableNode | ts.Node,
options: GetDocBlockOptions = { addEnd: true }
): string {
): Promise<string> {
if (!this.isAllowed(node)) {
return super.getDocBlock(node, options)
return await super.getDocBlock(node, options)
}
const actualNode = ts.isVariableStatement(node)
@@ -186,10 +254,34 @@ class FunctionKindGenerator extends DefaultKindGenerator<FunctionOrVariableNode>
: node
if (!actualNode) {
return super.getDocBlock(node, options)
return await super.getDocBlock(node, options)
}
let existingComments = this.getNodeCommentsFromRange(node)
if (existingComments?.includes(FunctionKindGenerator.EXAMPLE_PLACEHOLDER)) {
// just replace the existing comment and return it
if (options.aiGenerator) {
existingComments = existingComments.replace(
FunctionKindGenerator.EXAMPLE_PLACEHOLDER,
await this.getFunctionExampleAi(
actualNode,
options.aiGenerator,
false
)
)
}
return existingComments.replace("/*", "").replace("*/", "")
}
const nodeSymbol = getSymbol(node, this.checker)
const nodeParentSymbol = getSymbol(node.parent, this.checker)
const nodeType = this.getReturnType(actualNode)
const returnTypeStr = this.checker.typeToString(nodeType)
const normalizedTypeStr = returnTypeStr.startsWith("Promise<")
? returnTypeStr.replace(/^Promise</, "").replace(/>$/, "")
: returnTypeStr
let str = DOCBLOCK_START
@@ -197,35 +289,43 @@ class FunctionKindGenerator extends DefaultKindGenerator<FunctionOrVariableNode>
str += `${
options.summaryPrefix ||
(this.isMethod(actualNode) ? `This method` : `This function`)
} ${this.getFunctionSummary(actualNode, nodeSymbol)}${DOCBLOCK_NEW_LINE}`
} ${this.getFunctionSummary({
node: actualNode,
symbol: nodeSymbol,
parentSymbol: nodeParentSymbol,
returnType: normalizedTypeStr,
})}${DOCBLOCK_NEW_LINE}`
// add params
actualNode.forEachChild((childNode) => {
if (!ts.isParameter(childNode)) {
return
}
const symbol = getSymbol(childNode, this.checker)
actualNode.parameters.map((parameterNode) => {
const symbol = getSymbol(parameterNode, this.checker)
if (!symbol) {
return
}
const symbolType = this.checker.getTypeOfSymbolAtLocation(
symbol,
childNode
parameterNode
)
const parameterName = symbol.getName()
const parameterSummary = this.getNodeSummary({
node: parameterNode,
symbol,
nodeType: symbolType,
knowledgeBaseOptions: {
templateOptions: {
rawParentName: nodeParentSymbol?.getName(),
pluralIndicatorStr: this.checker.typeToString(symbolType),
},
},
})
str += `${DOCBLOCK_NEW_LINE}@param {${this.checker.typeToString(
symbolType
)}} ${symbol.getName()} - ${this.getNodeSummary({
node: childNode,
symbol,
nodeType: symbolType,
})}`
)}} ${parameterName} - ${parameterSummary}`
})
// add returns
const nodeType = this.getReturnType(actualNode)
const returnTypeStr = this.checker.typeToString(nodeType)
const possibleReturnSummary = !this.hasReturnData(returnTypeStr)
? `Resolves when ${this.defaultSummary}`
: this.getNodeSummary({
@@ -238,12 +338,20 @@ class FunctionKindGenerator extends DefaultKindGenerator<FunctionOrVariableNode>
? this.knowledgeBaseFactory.tryToGetFunctionReturns({
symbol: nodeSymbol,
kind: actualNode.kind,
templateOptions: {
rawParentName: nodeParentSymbol?.getName(),
pluralIndicatorStr: normalizedTypeStr,
},
}) || possibleReturnSummary
: possibleReturnSummary
}`
// add example
str += this.getFunctionExample(nodeSymbol)
if (!options.aiGenerator) {
str += this.getFunctionPlaceholderExample()
} else {
str += await this.getFunctionExampleAi(actualNode, options.aiGenerator)
}
// add common docs
str += this.getCommonDocs(node, {
@@ -256,6 +364,22 @@ class FunctionKindGenerator extends DefaultKindGenerator<FunctionOrVariableNode>
return str
}
/**
* Allows documenting (updating) a node if it has the example placeholder.
*
* @param node - The node to document.
* @returns Whether the node can be documented.
*/
canDocumentNode(node: ts.Node): boolean {
const comments = this.getNodeCommentsFromRange(node)
return (
!comments ||
comments?.includes(FunctionKindGenerator.EXAMPLE_PLACEHOLDER) ||
false
)
}
}
export default FunctionKindGenerator

View File

@@ -19,6 +19,7 @@ import {
* it extends the {@link FunctionKindGenerator} class.
*/
class MedusaReactHooksKindGenerator extends FunctionKindGenerator {
public name = "medusa-react"
/**
* Checks whether the generator can retrieve the docblock of the specified node. It uses the parent generator
* to check that the node is a function, then checks if the function is a mutation using the {@link isMutation} method,
@@ -80,9 +81,12 @@ class MedusaReactHooksKindGenerator extends FunctionKindGenerator {
* @param {FunctionNode & ts.VariableDeclaration} node - The node to retrieve its docblock.
* @returns {string} The node's docblock.
*/
getDocBlock(node: FunctionNode & ts.VariableDeclaration): string {
async getDocBlock(
node: FunctionNode & ts.VariableDeclaration
): Promise<string> {
// TODO use the AiGenerator to generate summary + examples
if (!this.isAllowed(node)) {
return super.getDocBlock(node)
return await super.getDocBlock(node)
}
const actualNode = ts.isVariableStatement(node)
@@ -90,25 +94,29 @@ class MedusaReactHooksKindGenerator extends FunctionKindGenerator {
: node
if (!actualNode) {
return super.getDocBlock(node)
return await super.getDocBlock(node)
}
const isMutation = this.isMutation(actualNode)
let str = `${DOCBLOCK_START}This hook ${this.getFunctionSummary(node)}`
let str = `${DOCBLOCK_START}This hook ${this.getFunctionSummary({
node,
})}`
// add example
str += this.getFunctionExample()
str += this.getFunctionPlaceholderExample()
// loop over parameters that aren't query/mutation parameters
// and add docblock to them
this.getActualParameters(actualNode).forEach((parameter) => {
ts.addSyntheticLeadingComment(
parameter,
ts.SyntaxKind.MultiLineCommentTrivia,
super.getDocBlock(parameter),
true
)
})
await Promise.all(
this.getActualParameters(actualNode).map(async (parameter) => {
ts.addSyntheticLeadingComment(
parameter,
ts.SyntaxKind.MultiLineCommentTrivia,
await super.getDocBlock(parameter),
true
)
})
)
// check if mutation parameter is an intrinsic type and, if so, add the `@typeParamDefinition`
// tag to the hook

View File

@@ -48,6 +48,7 @@ type ParameterType = "query" | "path"
* since API routes are functions.
*/
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
@@ -166,12 +167,13 @@ class OasKindGenerator extends FunctionKindGenerator {
* @param options - The options to get the OAS.
* @returns The OAS as a string that can be used as a comment in a TypeScript file.
*/
getDocBlock(
async getDocBlock(
node: ts.Node | FunctionOrVariableNode,
options?: GetDocBlockOptions
): string {
): Promise<string> {
// TODO use AiGenerator to generate descriptions + examples
if (!this.isAllowed(node)) {
return super.getDocBlock(node, options)
return await super.getDocBlock(node, options)
}
const actualNode = ts.isVariableStatement(node)
@@ -179,7 +181,7 @@ class OasKindGenerator extends FunctionKindGenerator {
: node
if (!actualNode) {
return super.getDocBlock(node, options)
return await super.getDocBlock(node, options)
}
const methodName = this.getHTTPMethodName(node)

View File

@@ -53,6 +53,16 @@ class KindsRegistry {
hasGenerator(node: ts.Node): boolean {
return this.getKindGenerator(node) !== undefined
}
/**
* Retrieves a generator by its name attribute.
*
* @param name - The name of the generator to retrieve.
* @returns The generator, if exists.
*/
getKindGeneratorByName(name: string): DefaultKindGenerator | undefined {
return this.kindInstances.find((generator) => generator.name === name)
}
}
export default KindsRegistry

View File

@@ -8,6 +8,7 @@ import { shouldHaveCustomNamespace } from "../../utils/medusa-react-utils.js"
*/
class SourceFileKindGenerator extends DefaultKindGenerator<ts.SourceFile> {
protected allowedKinds: ts.SyntaxKind[] = [ts.SyntaxKind.SourceFile]
public name = "source-file"
/**
* Retrieve the docblock of a source file.
@@ -16,12 +17,12 @@ class SourceFileKindGenerator extends DefaultKindGenerator<ts.SourceFile> {
* @param {GetDocBlockOptions} options - The formatting options.
* @returns {string} The node's docblock.
*/
getDocBlock(
async getDocBlock(
node: ts.SourceFile | ts.Node,
options?: GetDocBlockOptions
): string {
): Promise<string> {
if (!this.isAllowed(node)) {
return super.getDocBlock(node, options)
return await super.getDocBlock(node, options)
}
if (shouldHaveCustomNamespace(node)) {

View File

@@ -22,7 +22,6 @@ export function camelToTitle(str: string): string {
.map((word) => capitalize(word))
.join(" ")
.trim()
.toLowerCase()
}
export function snakeToWords(str: string): string {
@@ -62,6 +61,10 @@ export function wordsToPascal(str: string): string {
.join("")
}
export function pascalToCamel(str: string): string {
return `${str.charAt(0).toLowerCase()}${str.substring(1)}`
}
/**
* Remove parts of the name such as DTO, Filterable, etc...
*
@@ -70,8 +73,10 @@ export function wordsToPascal(str: string): string {
*/
export function normalizeName(str: string): string {
return str
.replace(/^(create|update|delete)/i, "")
.replace(/^(create|update|delete|upsert)/i, "")
.replace(/DTO$/, "")
.replace(/^Filterable/, "")
.replace(/Props$/, "")
.replace(/^I([A-Z])/, "$1")
.replace(/ModuleService$/, "")
}

View File

@@ -1401,6 +1401,16 @@ __metadata:
languageName: node
linkType: hard
"@types/node-fetch@npm:^2.6.4":
version: 2.6.11
resolution: "@types/node-fetch@npm:2.6.11"
dependencies:
"@types/node": "*"
form-data: ^4.0.0
checksum: 5283d4e0bcc37a5b6d8e629aee880a4ffcfb33e089f4b903b2981b19c623972d1e64af7c3f9540ab990f0f5c89b9b5dda19c5bcb37a8e177079e93683bfd2f49
languageName: node
linkType: hard
"@types/node@npm:*, @types/node@npm:^20.8.3, @types/node@npm:^20.9.4":
version: 20.10.0
resolution: "@types/node@npm:20.10.0"
@@ -1417,6 +1427,15 @@ __metadata:
languageName: node
linkType: hard
"@types/node@npm:^18.11.18":
version: 18.19.25
resolution: "@types/node@npm:18.19.25"
dependencies:
undici-types: ~5.26.4
checksum: 4cd82b81700c38464cfc8ce5a94d3e115ef9e9befe7637a89c732c4036ab7c761ec69e4d93717a7c05ab58c87cf046eaafd3e0157d5406e387bcb185d27710ab
languageName: node
linkType: hard
"@types/randomcolor@npm:^0.5.8":
version: 0.5.9
resolution: "@types/randomcolor@npm:0.5.9"
@@ -1576,6 +1595,15 @@ __metadata:
languageName: node
linkType: hard
"abort-controller@npm:^3.0.0":
version: 3.0.0
resolution: "abort-controller@npm:3.0.0"
dependencies:
event-target-shim: ^5.0.0
checksum: 90ccc50f010250152509a344eb2e71977fbf8db0ab8f1061197e3275ddf6c61a41a6edfd7b9409c664513131dd96e962065415325ef23efa5db931b382d24ca5
languageName: node
linkType: hard
"acorn-jsx@npm:^5.3.2":
version: 5.3.2
resolution: "acorn-jsx@npm:5.3.2"
@@ -1626,6 +1654,15 @@ __metadata:
languageName: node
linkType: hard
"agentkeepalive@npm:^4.2.1":
version: 4.5.0
resolution: "agentkeepalive@npm:4.5.0"
dependencies:
humanize-ms: ^1.2.1
checksum: 394ea19f9710f230722996e156607f48fdf3a345133b0b1823244b7989426c16019a428b56c82d3eabef616e938812981d9009f4792ecc66bd6a59e991c62612
languageName: node
linkType: hard
"aggregate-error@npm:^3.1.0":
version: 3.1.0
resolution: "aggregate-error@npm:3.1.0"
@@ -1724,6 +1761,13 @@ __metadata:
languageName: node
linkType: hard
"asynckit@npm:^0.4.0":
version: 0.4.0
resolution: "asynckit@npm:0.4.0"
checksum: d73e2ddf20c4eb9337e1b3df1a0f6159481050a5de457c55b14ea2e5cb6d90bb69e004c9af54737a5ee0917fcf2c9e25de67777bbe58261847846066ba75bc9d
languageName: node
linkType: hard
"awilix@npm:^8.0.0, awilix@npm:^8.0.1":
version: 8.0.1
resolution: "awilix@npm:8.0.1"
@@ -1741,6 +1785,13 @@ __metadata:
languageName: node
linkType: hard
"base-64@npm:^0.1.0":
version: 0.1.0
resolution: "base-64@npm:0.1.0"
checksum: fe0dcf076e823f04db7ee9b02495be08a91c445fbc6db03cb9913be9680e2fcc0af8b74459041fe08ad16800b1f65a549501d8f08696a8a6d32880789b7de69d
languageName: node
linkType: hard
"base64-js@npm:^1.3.1":
version: 1.5.1
resolution: "base64-js@npm:1.5.1"
@@ -1937,6 +1988,13 @@ __metadata:
languageName: node
linkType: hard
"charenc@npm:0.0.2":
version: 0.0.2
resolution: "charenc@npm:0.0.2"
checksum: a45ec39363a16799d0f9365c8dd0c78e711415113c6f14787a22462ef451f5013efae8a28f1c058f81fc01f2a6a16955f7a5fd0cd56247ce94a45349c89877d8
languageName: node
linkType: hard
"chownr@npm:^1.1.1":
version: 1.1.4
resolution: "chownr@npm:1.1.4"
@@ -2030,6 +2088,15 @@ __metadata:
languageName: node
linkType: hard
"combined-stream@npm:^1.0.8":
version: 1.0.8
resolution: "combined-stream@npm:1.0.8"
dependencies:
delayed-stream: ~1.0.0
checksum: 0dbb829577e1b1e839fa82b40c07ffaf7de8a09b935cadd355a73652ae70a88b4320db322f6634a4ad93424292fa80973ac6480986247f1734a1137debf271d5
languageName: node
linkType: hard
"commander@npm:^10.0.0":
version: 10.0.1
resolution: "commander@npm:10.0.1"
@@ -2138,6 +2205,13 @@ __metadata:
languageName: node
linkType: hard
"crypt@npm:0.0.2":
version: 0.0.2
resolution: "crypt@npm:0.0.2"
checksum: adbf263441dd801665d5425f044647533f39f4612544071b1471962209d235042fb703c27eea2795c7c53e1dfc242405173003f83cf4f4761a633d11f9653f18
languageName: node
linkType: hard
"debug@npm:4, debug@npm:4.3.4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.2, debug@npm:^4.3.4":
version: 4.3.4
resolution: "debug@npm:4.3.4"
@@ -2186,6 +2260,13 @@ __metadata:
languageName: node
linkType: hard
"delayed-stream@npm:~1.0.0":
version: 1.0.0
resolution: "delayed-stream@npm:1.0.0"
checksum: d758899da03392e6712f042bec80aa293bbe9e9ff1b2634baae6a360113e708b91326594c8a486d475c69d6259afb7efacdc3537bfcda1c6c648e390ce601b19
languageName: node
linkType: hard
"deprecation@npm:^2.0.0, deprecation@npm:^2.3.1":
version: 2.3.1
resolution: "deprecation@npm:2.3.1"
@@ -2207,6 +2288,16 @@ __metadata:
languageName: node
linkType: hard
"digest-fetch@npm:^1.3.0":
version: 1.3.0
resolution: "digest-fetch@npm:1.3.0"
dependencies:
base-64: ^0.1.0
md5: ^2.3.0
checksum: 0fb389e33b9c6baf5e6a9ed287aa9d0d8b373d59b49d49c62c261e1ab24eaaf1d5aea3a105c1b31ba4a23e29e129365d839ce4c5974fa004a85d1a4568bc3585
languageName: node
linkType: hard
"dir-glob@npm:^3.0.1":
version: 3.0.1
resolution: "dir-glob@npm:3.0.1"
@@ -2227,6 +2318,7 @@ __metadata:
dotenv: ^16.3.1
eslint: ^8.56.0
minimatch: ^9.0.3
openai: ^4.29.1
openapi-types: ^12.1.3
pluralize: ^8.0.0
prettier: ^3.2.4
@@ -2585,6 +2677,13 @@ __metadata:
languageName: node
linkType: hard
"event-target-shim@npm:^5.0.0":
version: 5.0.1
resolution: "event-target-shim@npm:5.0.1"
checksum: 0255d9f936215fd206156fd4caa9e8d35e62075d720dc7d847e89b417e5e62cf1ce6c9b4e0a1633a9256de0efefaf9f8d26924b1f3c8620cffb9db78e7d3076b
languageName: node
linkType: hard
"execa@npm:^5.0.0":
version: 5.1.1
resolution: "execa@npm:5.1.1"
@@ -2751,6 +2850,34 @@ __metadata:
languageName: node
linkType: hard
"form-data-encoder@npm:1.7.2":
version: 1.7.2
resolution: "form-data-encoder@npm:1.7.2"
checksum: 56553768037b6d55d9de524f97fe70555f0e415e781cb56fc457a68263de3d40fadea2304d4beef2d40b1a851269bd7854e42c362107071892cb5238debe9464
languageName: node
linkType: hard
"form-data@npm:^4.0.0":
version: 4.0.0
resolution: "form-data@npm:4.0.0"
dependencies:
asynckit: ^0.4.0
combined-stream: ^1.0.8
mime-types: ^2.1.12
checksum: cb6f3ac49180be03ff07ba3ff125f9eba2ff0b277fb33c7fc47569fc5e616882c5b1c69b9904c4c4187e97dd0419dd03b134174756f296dec62041e6527e2c6e
languageName: node
linkType: hard
"formdata-node@npm:^4.3.2":
version: 4.4.1
resolution: "formdata-node@npm:4.4.1"
dependencies:
node-domexception: 1.0.0
web-streams-polyfill: 4.0.0-beta.3
checksum: 74151e7b228ffb33b565cec69182694ad07cc3fdd9126a8240468bb70a8ba66e97e097072b60bcb08729b24c7ce3fd3e0bd7f1f80df6f9f662b9656786e76f6a
languageName: node
linkType: hard
"fs-constants@npm:^1.0.0":
version: 1.0.0
resolution: "fs-constants@npm:1.0.0"
@@ -3020,6 +3147,15 @@ __metadata:
languageName: node
linkType: hard
"humanize-ms@npm:^1.2.1":
version: 1.2.1
resolution: "humanize-ms@npm:1.2.1"
dependencies:
ms: ^2.0.0
checksum: f34a2c20161d02303c2807badec2f3b49cbfbbb409abd4f95a07377ae01cfe6b59e3d15ac609cffcd8f2521f0eb37b7e1091acf65da99aa2a4f1ad63c21e7e7a
languageName: node
linkType: hard
"ieee754@npm:^1.1.13":
version: 1.2.1
resolution: "ieee754@npm:1.2.1"
@@ -3089,6 +3225,13 @@ __metadata:
languageName: node
linkType: hard
"is-buffer@npm:~1.1.6":
version: 1.1.6
resolution: "is-buffer@npm:1.1.6"
checksum: ae18aa0b6e113d6c490ad1db5e8df9bdb57758382b313f5a22c9c61084875c6396d50bbf49315f5b1926d142d74dfb8d31b40d993a383e0a158b15fea7a82234
languageName: node
linkType: hard
"is-core-module@npm:^2.13.0":
version: 2.13.1
resolution: "is-core-module@npm:2.13.1"
@@ -3601,6 +3744,17 @@ __metadata:
languageName: node
linkType: hard
"md5@npm:^2.3.0":
version: 2.3.0
resolution: "md5@npm:2.3.0"
dependencies:
charenc: 0.0.2
crypt: 0.0.2
is-buffer: ~1.1.6
checksum: 14a21d597d92e5b738255fbe7fe379905b8cb97e0a49d44a20b58526a646ec5518c337b817ce0094ca94d3e81a3313879c4c7b510d250c282d53afbbdede9110
languageName: node
linkType: hard
"merge-stream@npm:^2.0.0":
version: 2.0.0
resolution: "merge-stream@npm:2.0.0"
@@ -3632,6 +3786,22 @@ __metadata:
languageName: node
linkType: hard
"mime-db@npm:1.52.0":
version: 1.52.0
resolution: "mime-db@npm:1.52.0"
checksum: 0557a01deebf45ac5f5777fe7740b2a5c309c6d62d40ceab4e23da9f821899ce7a900b7ac8157d4548ddbb7beffe9abc621250e6d182b0397ec7f10c7b91a5aa
languageName: node
linkType: hard
"mime-types@npm:^2.1.12":
version: 2.1.35
resolution: "mime-types@npm:2.1.35"
dependencies:
mime-db: 1.52.0
checksum: 82fb07ec56d8ff1fc999a84f2f217aa46cb6ed1033fefaabd5785b9a974ed225c90dc72fff460259e66b95b73648596dbcc50d51ed69cdf464af2d237d3149b2
languageName: node
linkType: hard
"mimic-fn@npm:^2.1.0":
version: 2.1.0
resolution: "mimic-fn@npm:2.1.0"
@@ -3724,7 +3894,7 @@ __metadata:
languageName: node
linkType: hard
"ms@npm:^2.1.1":
"ms@npm:^2.0.0, ms@npm:^2.1.1":
version: 2.1.3
resolution: "ms@npm:2.1.3"
checksum: d924b57e7312b3b63ad21fc5b3dc0af5e78d61a1fc7cfb5457edaf26326bf62be5307cc87ffb6862ef1c2b33b0233cdb5d4f01c4c958cc0d660948b65a287a48
@@ -3755,6 +3925,13 @@ __metadata:
languageName: node
linkType: hard
"node-domexception@npm:1.0.0":
version: 1.0.0
resolution: "node-domexception@npm:1.0.0"
checksum: 5e5d63cda29856402df9472335af4bb13875e1927ad3be861dc5ebde38917aecbf9ae337923777af52a48c426b70148815e890a5d72760f1b4d758cc671b1a2b
languageName: node
linkType: hard
"node-fetch@npm:2.6.7":
version: 2.6.7
resolution: "node-fetch@npm:2.6.7"
@@ -3875,6 +4052,25 @@ __metadata:
languageName: node
linkType: hard
"openai@npm:^4.29.1":
version: 4.29.1
resolution: "openai@npm:4.29.1"
dependencies:
"@types/node": ^18.11.18
"@types/node-fetch": ^2.6.4
abort-controller: ^3.0.0
agentkeepalive: ^4.2.1
digest-fetch: ^1.3.0
form-data-encoder: 1.7.2
formdata-node: ^4.3.2
node-fetch: ^2.6.7
web-streams-polyfill: ^3.2.1
bin:
openai: bin/cli
checksum: 7873d1c8f69d8a76ca38bd3b0aa10e967ee1a2e705a1a2eb1012dcdd1b689569e041e1bcbeb72e10fc15a43d62c0f99e01de58c8ed454e8c0b54626b58c0794f
languageName: node
linkType: hard
"openapi-types@npm:^12.1.3":
version: 12.1.3
resolution: "openapi-types@npm:12.1.3"
@@ -5354,6 +5550,20 @@ __metadata:
languageName: node
linkType: hard
"web-streams-polyfill@npm:4.0.0-beta.3":
version: 4.0.0-beta.3
resolution: "web-streams-polyfill@npm:4.0.0-beta.3"
checksum: a9596779db2766990117ed3a158e0b0e9f69b887a6d6ba0779940259e95f99dc3922e534acc3e5a117b5f5905300f527d6fbf8a9f0957faf1d8e585ce3452e8e
languageName: node
linkType: hard
"web-streams-polyfill@npm:^3.2.1":
version: 3.3.3
resolution: "web-streams-polyfill@npm:3.3.3"
checksum: 64e855c47f6c8330b5436147db1c75cb7e7474d924166800e8e2aab5eb6c76aac4981a84261dd2982b3e754490900b99791c80ae1407a9fa0dcff74f82ea3a7f
languageName: node
linkType: hard
"webidl-conversions@npm:^3.0.0":
version: 3.0.1
resolution: "webidl-conversions@npm:3.0.1"

File diff suppressed because it is too large Load Diff