UNPKG

@autobe/agent

Version:

AI backend server code generator

278 lines (251 loc) 10.4 kB
import { AutoBeOpenApi } from "@autobe/interface"; import { StringUtil } from "@autobe/utils"; import { NamingConvention } from "@typia/utils"; import { IValidation } from "typia"; import { AutoBeJsonSchemaValidator } from "../utils/AutoBeJsonSchemaValidator"; export namespace AutoBeInterfaceOperationProgrammer { export const fix = ( operation: Pick<AutoBeOpenApi.IOperation, "method" | "responseBody">, ): void => { if (operation.method === "delete" && operation.responseBody !== null) operation.responseBody = null; }; export const validate = (props: { errors: IValidation.IError[]; accessor: string; operation: Omit< AutoBeOpenApi.IOperation, "authorizationActor" | "authorizationType" | "prerequisites" >; }): void => { // get method has request body if ( props.operation.method === "get" && props.operation.requestBody !== null ) props.errors.push({ path: `${props.accessor}.requestBody`, expected: "null (GET method cannot have request body)", value: props.operation.requestBody, description: StringUtil.trim` GET method cannot have request body per HTTP specification. Fix: Change method to "patch" for complex queries, or set requestBody to null. `, }); // operation name if (NamingConvention.variable(props.operation.name) === false) props.errors.push({ path: `${props.accessor}.name`, expected: "<valid_variable_name>", value: props.operation.name, description: StringUtil.trim` Operation name "${props.operation.name}" is not a valid JavaScript variable name. It will be used as a controller method name, so it must be a valid identifier. `, }); else if (props.operation.name === "index") { if (props.operation.method !== "patch") props.errors.push({ path: `${props.accessor}.method`, expected: `"patch" when operation name is "index", or change operation name to something else`, value: props.operation.method, description: StringUtil.trim` Operation name "index" is reserved for getting list of resources, or pagination of the resources. Fix: Change method to "patch" when using "index" as operation name. Otherwise, change operation name to something else. `, }); if (props.operation.responseBody === null) props.errors.push({ path: `${props.accessor}.responseBody`, expected: `AutoBeOpenApi.IResponseBody (typeName: "IPageIResource") when operation name is "index"`, value: props.operation.responseBody, description: StringUtil.trim` Operation name "index" is reserved for getting list of resources, so response body must be a paginated type "IPageIResource". Fix: Change response body type to paginated type "IPageIResource", or change operation name to something else. `, }); else if ( props.operation.responseBody.typeName.startsWith("IPage") === false ) props.errors.push({ path: `${props.accessor}.responseBody.typeName`, expected: `"IPage${props.operation.responseBody.typeName}", or change operation name to something else`, value: props.operation.responseBody.typeName, description: StringUtil.trim` Operation name "index" is reserved for getting list of resources, so response body type must be paginated type "IPageIResource". Fix: Change response body type to paginated type "IPage${props.operation.responseBody.typeName}", or change operation name to something else. `, }); } // validate path parameters match with path validatePathParameters({ errors: props.errors, operation: props.operation, accessor: props.accessor, }); // validate types if (props.operation.requestBody !== null) { validatePrimitiveBody({ kind: "requestBody", errors: props.errors, path: `${props.accessor}.requestBody`, body: props.operation.requestBody, }); AutoBeJsonSchemaValidator.validateKey({ errors: props.errors, path: `${props.accessor}.requestBody.typeName`, key: props.operation.requestBody.typeName, }); } if (props.operation.responseBody !== null) { validatePrimitiveBody({ kind: "responseBody", errors: props.errors, path: `${props.accessor}.responseBody`, body: props.operation.responseBody, }); AutoBeJsonSchemaValidator.validateKey({ errors: props.errors, path: `${props.accessor}.responseBody.typeName`, key: props.operation.responseBody.typeName, }); } }; const validatePathParameters = (props: { errors: IValidation.IError[]; operation: Omit< AutoBeOpenApi.IOperation, "authorizationActor" | "authorizationType" | "prerequisites" >; accessor: string; }): void => { // Check parameter → path matching and uniqueness const parameterNames = new Set<string>(); props.operation.parameters.forEach((p, i) => { // Check if parameter exists in path if (props.operation.path.includes(`{${p.name}}`) === false) props.errors.push({ path: `${props.accessor}.parameters[${i}]`, expected: `removed, or expressed in AutoBeOpenApi.IOperation.path`, value: p, description: StringUtil.trim` Parameter "${p.name}" is defined but not used in path "${props.operation.path}". Fix: Remove parameter at index ${i}, or add {${p.name}} to the path. `, }); parameterNames.add(p.name); }); // Check for duplicate parameter names if (parameterNames.size !== props.operation.parameters.length) props.errors.push({ path: `${props.accessor}.parameters`, expected: `All parameter names must be unique`, value: props.operation.parameters, description: StringUtil.trim` Duplicate parameter names found: ${props.operation.parameters.length} parameters, but only ${parameterNames.size} unique names. Parameters: ${props.operation.parameters.map((p, idx) => `[${idx}]="${p.name}"`).join(", ")} Fix: Remove duplicate parameter definitions. `, }); const symbols: string[] = props.operation.path .split("{") .slice(1) .map((s) => s.split("}")[0]); // Check path → parameters matching symbols.forEach((s) => { if (props.operation.parameters.some((p) => p.name === s) === false) props.errors.push({ path: `${props.accessor}.path`, expected: `removed, or defined in AutoBeOpenApi.IOperation.parameters[]`, value: s, description: StringUtil.trim` Path contains "{${s}}" but no corresponding parameter definition exists. Current parameters: ${props.operation.parameters.length === 0 ? "[]" : `[${props.operation.parameters.map((p) => `"${p.name}"`).join(", ")}]`} Fix: Add parameter definition for "${s}", or remove {${s}} from path. `, }); }); // Check for duplicate path parameters const uniqueSymbols = new Set(symbols); if (uniqueSymbols.size !== symbols.length) props.errors.push({ path: `${props.accessor}.path`, expected: `All path parameter names must be unique`, value: props.operation.path, description: StringUtil.trim` Duplicate path parameter names: ${symbols.length} parameters, but only ${uniqueSymbols.size} unique names in "${props.operation.path}". Path parameters: ${symbols.map((s, idx) => `[${idx}]="{${s}}"`).join(", ")} Fix: Rename duplicate parameters to be unique. (e.g., {userId} and {postId} instead of {userId} twice). `, }); }; const validatePrimitiveBody = (props: { kind: "requestBody" | "responseBody"; errors: IValidation.IError[]; path: string; body: AutoBeOpenApi.IRequestBody | AutoBeOpenApi.IResponseBody; }): void => { if (props.body.typeName === "undefined" || props.body.typeName === "null") props.errors.push({ path: props.path, value: props.body, expected: "null", description: StringUtil.trim` Type "${props.body.typeName}" is not valid for ${props.kind}. Use null for empty ${props.kind}. `, }); else if ( props.body.typeName === "number" || props.body.typeName === "string" || props.body.typeName === "boolean" ) props.errors.push({ path: `${props.path}.typeName`, value: props.body.typeName, expected: "An object reference type encapsulating the primitive type", description: StringUtil.trim` Primitive type "${props.body.typeName}" not allowed for ${props.kind}. Encapsulate in object type (e.g., I${props.body.typeName[0].toUpperCase()}${props.body.typeName.slice(1)}Value). `, }); else if ( props.body.typeName === "object" || props.body.typeName === "any" || props.body.typeName === "interface" ) props.errors.push({ path: `${props.path}.typeName`, value: props.body.typeName, expected: "An object reference type", description: StringUtil.trim` Type "${props.body.typeName}" is a reserved word. Use a different type name. `, }); else if (props.body.typeName.startsWith("I") === false) { props.errors.push({ path: `${props.path}.typeName`, value: props.body.typeName, expected: "Type name starting with 'I' (e.g., IUser, IProduct, IOrder)", description: StringUtil.trim` Type name "${props.body.typeName}" must start with 'I' prefix per AutoBE naming convention. Fix: Rename to "I${props.body.typeName.charAt(0).toUpperCase()}${props.body.typeName.slice(1)}" (e.g., IUser, IProduct). `, }); } }; }