@autobe/agent
Version:
AI backend server code generator
339 lines (311 loc) • 11.3 kB
text/typescript
import {
AutoBeOpenApi,
AutoBeRealizeAuthorization,
AutoBeRealizeCollectorFunction,
AutoBeRealizeTransformerFunction,
} from "@autobe/interface";
import { AutoBeOpenApiTypeChecker, StringUtil } from "@autobe/utils";
import { IAutoBeRealizeScenarioResult } from "../../structures/IAutoBeRealizeScenarioResult";
import { AutoBeRealizeCollectorProgrammer } from "../AutoBeRealizeCollectorProgrammer";
import { AutoBeRealizeTransformerProgrammer } from "../AutoBeRealizeTransformerProgrammer";
import {
IResolvedTransformer,
resolvePropertyTransformer,
} from "./resolvePropertyTransformer";
export function writeRealizeOperationTemplate(props: {
scenario: IAutoBeRealizeScenarioResult;
operation: AutoBeOpenApi.IOperation;
imports: string[];
authorization: AutoBeRealizeAuthorization | null;
schemas: Record<string, AutoBeOpenApi.IJsonSchemaDescriptive>;
collectors: AutoBeRealizeCollectorFunction[];
transformers: AutoBeRealizeTransformerFunction[];
}): string {
const functionParameters: string[] = [];
if (props.authorization && props.authorization.actor.name) {
functionParameters.push(
`${props.authorization.actor.name}: ${props.authorization.payload.name}`,
);
}
functionParameters.push(
...props.operation.parameters.map(
(param: AutoBeOpenApi.IParameter): string =>
`${param.name}: ${writeParameterType(param.schema)}`,
),
);
if (
props.operation.requestBody?.typeName.endsWith(".ILogin") ||
props.operation.requestBody?.typeName.endsWith(".IJoin")
)
functionParameters.push("ip: string");
if (props.operation.requestBody?.typeName) {
functionParameters.push(`body: ${props.operation.requestBody.typeName}`);
}
const hasParameters: boolean = functionParameters.length > 0;
const formattedSignature: string = hasParameters
? `props: {\n${functionParameters.map((p: string): string => ` ${p}`).join(";\n")};\n}`
: "";
const returnType: string = props.operation.responseBody?.typeName ?? "void";
const body: string = writeBody({
method: props.operation.method,
operation: props.operation,
schemas: props.schemas,
collectors: props.collectors,
transformers: props.transformers,
});
const indentedBody: string = body
.split("\n")
.map((line: string): string => (line.length > 0 ? ` ${line}` : line))
.join("\n");
return StringUtil.trim`
Complete the code below, disregard the import part and return only the function part.
\`\`\`typescript
${props.imports.join("\n")}
// DON'T CHANGE FUNCTION NAME AND PARAMETERS,
// ONLY YOU HAVE TO WRITE THIS FUNCTION BODY, AND USE IMPORTED.
export async function ${props.scenario.functionName}(${formattedSignature}): Promise<${returnType}> {
${indentedBody}
}
\`\`\`
`;
}
function writeBody(props: {
method: string;
operation: AutoBeOpenApi.IOperation;
schemas: Record<string, AutoBeOpenApi.IJsonSchemaDescriptive>;
collectors: AutoBeRealizeCollectorFunction[];
transformers: AutoBeRealizeTransformerFunction[];
}): string {
const collector: AutoBeRealizeCollectorFunction | undefined = props.operation
.requestBody?.typeName
? props.collectors.find(
(c: AutoBeRealizeCollectorFunction): boolean =>
c.plan.dtoTypeName === props.operation.requestBody!.typeName,
)
: undefined;
const responseTypeName: string | undefined =
props.operation.responseBody?.typeName;
const isPageType: boolean = !!responseTypeName?.startsWith("IPage");
const innerTypeName: string | undefined = isPageType
? responseTypeName!.replace(/^IPage/, "")
: responseTypeName;
const transformer: AutoBeRealizeTransformerFunction | undefined =
innerTypeName
? props.transformers.find(
(t: AutoBeRealizeTransformerFunction): boolean =>
t.plan.dtoTypeName === innerTypeName,
)
: undefined;
// pagination (collector 와 동시에 올 수 없음)
if (isPageType && transformer) {
const tName: string = AutoBeRealizeTransformerProgrammer.getName(
transformer.plan.dtoTypeName,
);
const table: string = transformer.plan.databaseSchemaName;
const isRecursive: boolean =
AutoBeRealizeTransformerProgrammer.getRecursiveProperty({
schemas: props.schemas as Record<string, AutoBeOpenApi.IJsonSchema>,
typeName: transformer.plan.dtoTypeName,
}) !== null;
const dataLine: string = isRecursive
? `data: await ${tName}.transformAll(records),`
: `data: await ArrayUtil.asyncMap(records, ${tName}.transform),`;
return StringUtil.trim`
const records = await MyGlobal.prisma.${table}.findMany({
...${tName}.select(),
...,
});
return {
pagination: {
current: ...,
limit: ...,
records: ...,
pages: ...,
},
${dataLine}
};
`;
}
// collector + transformer (create and return)
if (collector && transformer) {
const cName: string = AutoBeRealizeCollectorProgrammer.getName(
collector.plan.dtoTypeName,
);
const tName: string = AutoBeRealizeTransformerProgrammer.getName(
transformer.plan.dtoTypeName,
);
const table: string = collector.plan.databaseSchemaName;
return StringUtil.trim`
const record = await MyGlobal.prisma.${table}.create({
data: await ${cName}.collect({
body: props.body,
...
}),
...${tName}.select(),
});
return await ${tName}.transform(record);
`;
}
// collector only (create, void return)
if (collector) {
const cName: string = AutoBeRealizeCollectorProgrammer.getName(
collector.plan.dtoTypeName,
);
const table: string = collector.plan.databaseSchemaName;
return StringUtil.trim`
await MyGlobal.prisma.${table}.create({
data: await ${cName}.collect({
body: props.body,
...
}),
});
`;
}
// update + transformer (PUT: manual update, re-fetch, transform)
if (props.method === "put" && transformer) {
const tName: string = AutoBeRealizeTransformerProgrammer.getName(
transformer.plan.dtoTypeName,
);
const table: string = transformer.plan.databaseSchemaName;
return StringUtil.trim`
await MyGlobal.prisma.${table}.update({
where: { ... },
data: { ... },
});
const updated = await MyGlobal.prisma.${table}.findUniqueOrThrow({
where: { ... },
...${tName}.select(),
});
return await ${tName}.transform(updated);
`;
}
// update void (PUT: manual update, void return)
if (props.method === "put") {
const table: string = transformer?.plan.databaseSchemaName ?? "...";
return StringUtil.trim`
await MyGlobal.prisma.${table}.update({
where: { ... },
data: { ... },
});
`;
}
// delete (DELETE: simple delete, void return)
if (props.method === "delete") {
const table: string = transformer?.plan.databaseSchemaName ?? "...";
return StringUtil.trim`
await MyGlobal.prisma.${table}.delete({
where: { ... },
});
`;
}
// transformer only (read single or other non-PUT/DELETE)
if (transformer) {
const tName: string = AutoBeRealizeTransformerProgrammer.getName(
transformer.plan.dtoTypeName,
);
const table: string = transformer.plan.databaseSchemaName;
return StringUtil.trim`
const record = await MyGlobal.prisma.${table}.findFirstOrThrow({
...${tName}.select(),
where: { ... },
});
return await ${tName}.transform(record);
`;
}
// object response with transformer-backed properties
if (responseTypeName) {
const objectBody: string | null = writeObjectBody({
responseTypeName,
schemas: props.schemas,
transformers: props.transformers,
});
if (objectBody !== null) return objectBody;
}
return [
"// No matching Collector/Transformer found for this operation.",
"// You MUST call getDatabaseSchemas first to get exact relation property names.",
"// NEVER guess relation names from table names — always verify against the schema.",
"...",
].join("\n ");
}
function writeObjectBody(props: {
responseTypeName: string;
schemas: Record<string, AutoBeOpenApi.IJsonSchemaDescriptive>;
transformers: AutoBeRealizeTransformerFunction[];
}): string | null {
const schema: AutoBeOpenApi.IJsonSchemaDescriptive | undefined =
props.schemas[props.responseTypeName];
if (!schema || !AutoBeOpenApiTypeChecker.isObject(schema)) return null;
const lines: string[] = [];
let hasMatch: boolean = false;
for (const [key, prop] of Object.entries(schema.properties) as Array<
[string, AutoBeOpenApi.IJsonSchemaProperty]
>) {
const match: IResolvedTransformer | null = resolvePropertyTransformer({
schema: prop,
transformers: props.transformers,
});
if (match) {
hasMatch = true;
const tName: string = AutoBeRealizeTransformerProgrammer.getName(
match.transformer.plan.dtoTypeName,
);
if (match.isArray) {
const isRecursive: boolean =
AutoBeRealizeTransformerProgrammer.getRecursiveProperty({
schemas: props.schemas as Record<string, AutoBeOpenApi.IJsonSchema>,
typeName: match.transformer.plan.dtoTypeName,
}) !== null;
lines.push(
isRecursive
? ` ${key}: await ${tName}.transformAll(...),`
: ` ${key}: await ArrayUtil.asyncMap(..., (r) => ${tName}.transform(r)),`,
);
} else {
lines.push(` ${key}: await ${tName}.transform(...),`);
}
} else {
lines.push(` ${key}: ...,`);
}
}
if (!hasMatch) return null;
return StringUtil.trim`
return {
${lines.join("\n")}
};
`;
}
function writeParameterType(
schema: AutoBeOpenApi.IParameter["schema"],
): string {
const elements: string[] =
schema.type === "integer"
? ["number", `tags.Type<"int32">`]
: [schema.type];
if (schema.type === "number") {
if (schema.minimum !== undefined)
elements.push(`tags.Minimum<${schema.minimum}>`);
if (schema.maximum !== undefined)
elements.push(`tags.Maximum<${schema.maximum}>`);
if (schema.exclusiveMinimum !== undefined)
elements.push(`tags.ExclusiveMinimum<${schema.exclusiveMinimum}>`);
if (schema.exclusiveMaximum !== undefined)
elements.push(`tags.ExclusiveMaximum<${schema.exclusiveMaximum}>`);
if (schema.multipleOf !== undefined)
elements.push(`tags.MultipleOf<${schema.multipleOf}>`);
} else if (schema.type === "string") {
if (schema.format !== undefined)
elements.push(`tags.Format<${JSON.stringify(schema.format)}>`);
if (schema.contentMediaType !== undefined)
elements.push(
`tags.ContentMediaType<${JSON.stringify(schema.contentMediaType)}>`,
);
if (schema.pattern !== undefined)
elements.push(`tags.Pattern<${JSON.stringify(schema.pattern)}>`);
if (schema.minLength !== undefined)
elements.push(`tags.MinLength<${schema.minLength}>`);
if (schema.maxLength !== undefined)
elements.push(`tags.MaxLength<${schema.maxLength}>`);
}
return elements.join(" & ");
}