@autobe/agent
Version:
AI backend server code generator
425 lines (398 loc) • 13.3 kB
text/typescript
import {
AutoBeDatabase,
AutoBeOpenApi,
AutoBeRealizeCollectorMapping,
AutoBeRealizeCollectorPlan,
IAutoBeCompiler,
} from "@autobe/interface";
import { AutoBeOpenApiTypeChecker, StringUtil } from "@autobe/utils";
import {
LlmTypeChecker,
NamingConvention,
OpenApiTypeChecker,
} from "@typia/utils";
import typia, { ILlmApplication, ILlmSchema, IValidation } from "typia";
import { AutoBeContext } from "../../../context/AutoBeContext";
export namespace AutoBeRealizeCollectorProgrammer {
export function filter(key: string): boolean {
return key.endsWith(".ICreate");
}
export function getName(dtoTypeName: string): string {
const replaced: string = dtoTypeName.replace(".ICreate", "");
const entity: string = replaced.startsWith("I")
? replaced.substring(1)
: replaced;
return `${entity}Collector`;
}
export function getNeighbors(code: string): string[] {
const unique: Set<string> = new Set();
const regex: RegExp = /(\w+Collector)\.collect/g;
while (true) {
const match: RegExpExecArray | null = regex.exec(code);
if (match === null) break;
unique.add(match[1]!);
}
return Array.from(unique);
}
export function getMappingMetadata(props: {
application: AutoBeDatabase.IApplication;
model: AutoBeDatabase.IModel;
}): AutoBeRealizeCollectorMapping.Metadata[] {
return [
{
member: props.model.primaryField.name,
kind: "scalar",
nullable: false,
},
...props.model.plainFields.map(
(pf) =>
({
member: pf.name,
kind: "scalar",
nullable: pf.nullable,
}) satisfies AutoBeRealizeCollectorMapping.Metadata,
),
...props.model.foreignFields.map(
(f) =>
({
member: f.relation.name,
kind: "belongsTo",
nullable: f.nullable,
}) satisfies AutoBeRealizeCollectorMapping.Metadata,
),
...props.application.files
.map((f) => f.models)
.flat()
.map((om) =>
om.foreignFields
.filter((fk) => fk.relation.targetModel === props.model.name)
.map(
(fk) =>
({
member: fk.relation.oppositeName,
kind: fk.unique ? "hasOne" : "hasMany",
nullable: null,
}) satisfies AutoBeRealizeCollectorMapping.Metadata,
),
)
.flat(),
];
}
export function writeTemplate(props: {
plan: AutoBeRealizeCollectorPlan;
body: AutoBeOpenApi.IJsonSchema;
model: AutoBeDatabase.IModel;
application: AutoBeDatabase.IApplication;
}): string {
const mappings: string[] = getMappingMetadata(props).map((r) => r.member);
return StringUtil.trim`
export namespace ${getName(props.plan.dtoTypeName)} {
export async function collect(props: {
body: ${props.plan.dtoTypeName};
${
//references
props.plan.references
.map(
(r) =>
`${NamingConvention.camel(r.databaseSchemaName)}: IEntity; // ${r.source}`,
)
.join("\n")
}
${
// ip
AutoBeOpenApiTypeChecker.isObject(props.body) &&
props.body.properties.ip !== undefined &&
props.model.plainFields.some((f) => f.name === "ip")
? `ip: string;`
: ""
}
${
// sequence
AutoBeOpenApiTypeChecker.isObject(props.body) &&
props.body.properties.sequence !== undefined &&
AutoBeOpenApiTypeChecker.isString(props.body.properties.sequence) &&
props.model.plainFields.some(
(f) => f.name === "sequence" && f.type === "int",
)
? `sequence: number;`
: ""
}
}) {
return {
${mappings.map((r) => ` ${r}: ...,`).join("\n")}
} satisfies Prisma.${props.plan.databaseSchemaName}CreateInput;
}
}
`;
}
export async function writeStructures(
ctx: AutoBeContext,
dtoTypeName: string,
): Promise<Record<string, string>> {
const document: AutoBeOpenApi.IDocument = ctx.state().interface!.document;
const components: AutoBeOpenApi.IComponents = {
authorizations: [],
schemas: {},
};
OpenApiTypeChecker.visit({
components: document.components,
schema: { $ref: `#/components/schemas/${dtoTypeName}` },
closure: (s) => {
if (OpenApiTypeChecker.isReference(s)) {
const key: string = s.$ref.split("/").pop()!;
components.schemas[key] = document.components.schemas[key];
}
},
});
const compiler: IAutoBeCompiler = await ctx.compiler();
const entries: [string, string][] = Object.entries(
await compiler.interface.write(
{
components,
operations: [],
},
[],
),
);
return Object.fromEntries(
entries.filter(([key]) => key.startsWith("src/api/structures")),
);
}
export async function replaceImportStatements(
ctx: AutoBeContext,
props: {
dtoTypeName: string;
schemas: Record<string, AutoBeOpenApi.IJsonSchemaDescriptive>;
code: string;
},
): Promise<string> {
const compiler: IAutoBeCompiler = await ctx.compiler();
let code: string = await compiler.typescript.removeImportStatements(
props.code,
);
const imports: string[] = writeImportStatements(props);
const selfName: string = getName(props.dtoTypeName);
code = [
...imports,
"",
...getNeighbors(code)
.filter((trs) => trs !== selfName)
.map((trs) => `import { ${trs} } from "./${trs}";`),
"",
code,
].join("\n");
return await compiler.typescript.beautify(code);
}
function writeImportStatements(props: {
dtoTypeName: string;
schemas: Record<string, AutoBeOpenApi.IJsonSchemaDescriptive>;
}): string[] {
const typeReferences: Set<string> = new Set();
const visit = (key: string) =>
OpenApiTypeChecker.visit({
schema: {
$ref: `#/components/schemas/${key}`,
},
components: { schemas: props.schemas },
closure: (next) => {
if (OpenApiTypeChecker.isReference(next))
typeReferences.add(next.$ref.split("/").pop()!.split(".")[0]!);
},
});
visit(props.dtoTypeName);
const imports: string[] = [
`import { Prisma } from "@prisma/sdk";`,
`import { ArrayUtil } from "@nestia/e2e";`,
`import { v4 } from "uuid";`,
"",
`import { IEntity } from "@ORGANIZATION/PROJECT-api/lib/structures/IEntity";`,
...Array.from(typeReferences).map(
(ref) =>
`import { ${ref} } from "@ORGANIZATION/PROJECT-api/lib/structures/${ref}";`,
),
"",
`import { MyGlobal } from "../MyGlobal";`,
`import { PasswordUtil } from "../utils/PasswordUtil";`,
];
return imports;
}
export function validate(props: {
application: AutoBeDatabase.IApplication;
plan: AutoBeRealizeCollectorPlan;
mappings: AutoBeRealizeCollectorMapping[];
neighbors: AutoBeRealizeCollectorPlan[];
draft: string;
revise: {
review: string;
final: string | null;
};
}): IValidation.IError[] {
const errors: IValidation.IError[] = [];
validateMappings({
application: props.application,
errors,
plan: props.plan,
mappings: props.mappings,
});
validateEmptyCode({
plan: props.plan,
content: props.draft,
path: "$input.request.draft",
errors,
});
validateNeighbors({
plan: props.plan,
neighbors: props.neighbors,
content: props.draft,
path: "$input.request.draft",
errors,
});
if (props.revise.final !== null) {
validateEmptyCode({
plan: props.plan,
content: props.revise.final,
path: "$input.request.revise.final",
errors,
});
validateNeighbors({
plan: props.plan,
neighbors: props.neighbors,
content: props.revise.final,
path: "$input.request.revise.final",
errors,
});
}
return errors;
}
function validateMappings(props: {
application: AutoBeDatabase.IApplication;
errors: IValidation.IError[];
plan: AutoBeRealizeCollectorPlan;
mappings: AutoBeRealizeCollectorMapping[];
}): void {
const model: AutoBeDatabase.IModel = props.application.files
.map((f) => f.models)
.flat()
.find((m) => m.name === props.plan.databaseSchemaName)!;
const required: AutoBeRealizeCollectorMapping.Metadata[] =
getMappingMetadata({
application: props.application,
model,
});
props.mappings.forEach((m, i) => {
const metadata: AutoBeRealizeCollectorMapping.Metadata | undefined =
required.find((r) => r.member === m.member);
if (metadata === undefined)
props.errors.push({
path: `$input.request.mappings[${i}].member`,
value: m.member,
expected: required.map((r) => JSON.stringify(r)).join(" | "),
description: StringUtil.trim`
'${m.member}' is not a valid Prisma member.
Please provide mapping only for existing Prisma members:
${required.map((r) => `- ${r.member}`).join("\n")}
`,
});
else {
if (metadata.kind !== m.kind)
props.errors.push({
path: `$input.request.mappings[${i}].kind`,
value: m.kind,
expected: `"${metadata.kind}"`,
description: StringUtil.trim`
The mapping kind for Prisma member '${m.member}' is invalid.
Expected kind is '${metadata.kind}', but received kind is '${m.kind}'.
`,
});
if (metadata.nullable !== m.nullable)
props.errors.push({
path: `$input.request.mappings[${i}].nullable`,
value: m.nullable,
expected: `${metadata.nullable}`,
description: StringUtil.trim`
The mapping nullable for Prisma member '${m.member}' is invalid.
Expected nullable is '${metadata.nullable}', but received nullable is '${m.nullable}'.
`,
});
}
});
for (const r of required) {
if (props.mappings.some((m) => m.member === r.member)) continue;
props.errors.push({
path: "$input.request.mappings[]",
value: undefined,
expected: StringUtil.trim`{
member: "${r.member}";
kind: "${r.kind}";
how: string;
}`,
description: StringUtil.trim`
You missed mapping for required Prisma member '${r.member}'.
Make sure to provide mapping for all required members.
`,
});
}
}
function validateEmptyCode(props: {
plan: AutoBeRealizeCollectorPlan;
content: string;
path: string;
errors: IValidation.IError[];
}): void {
const name: string = getName(props.plan.dtoTypeName);
if (props.content.includes(`export namespace ${name}`) === false)
props.errors.push({
path: props.path,
expected: `Namespace '${name}' to be present in the code.`,
value: props.content,
description: `The generated code does not contain the expected namespace '${name}'.`,
});
}
function validateNeighbors(props: {
plan: AutoBeRealizeCollectorPlan;
neighbors: AutoBeRealizeCollectorPlan[];
content: string;
path: string;
errors: IValidation.IError[];
}): void {
const selfName: string = getName(props.plan.dtoTypeName);
const neighborNames: string[] = getNeighbors(props.content);
for (const x of neighborNames)
if (
x !== selfName &&
props.neighbors.some((y) => getName(y.dtoTypeName) === x) === false
)
props.errors.push({
path: props.path,
expected: `Use existing transformer.`,
value: props.content,
description: StringUtil.trim`
You've imported and utilized ${x}, but it does not exist.
Use one of them below, or change to another code:
${props.neighbors
.map((y) => `- ${getName(y.dtoTypeName)}`)
.join("\n")}
`,
});
}
export const fixApplication = (props: {
definition: ILlmApplication;
application: AutoBeDatabase.IApplication;
model: AutoBeDatabase.IModel;
}): void => {
const mapping: ILlmSchema | undefined =
props.definition.functions[0].parameters.$defs[
typia.reflect.name<AutoBeRealizeCollectorMapping>()
];
if (mapping === undefined || LlmTypeChecker.isObject(mapping) === false)
throw new Error(
"AutoBeRealizeCollectorMapping type is not defined in the function calling schema.",
);
const member: ILlmSchema | undefined = mapping.properties.member;
if (member === undefined || LlmTypeChecker.isString(member) === false)
throw new Error(
"AutoBeRealizeCollectorMapping.member is not defined or string type.",
);
member.enum = getMappingMetadata(props).map((m) => m.member);
};
}