@autobe/agent
Version:
AI backend server code generator
573 lines (525 loc) • 18.7 kB
text/typescript
import {
AutoBeDatabase,
AutoBeOpenApi,
AutoBeRealizeTransformerPlan,
} from "@autobe/interface";
import { AutoBeOpenApiTypeChecker, StringUtil } from "@autobe/utils";
import { AutoBeRealizeTransformerProgrammer } from "../AutoBeRealizeTransformerProgrammer";
export function writeRealizeTransformerTemplate(props: {
plan: AutoBeRealizeTransformerPlan;
schema: AutoBeOpenApi.IJsonSchemaDescriptive.IObject;
schemas: Record<string, AutoBeOpenApi.IJsonSchema>;
neighbors?: AutoBeRealizeTransformerPlan[];
relations?: Array<{
propertyKey: string;
targetModel: string;
relationType: string;
fkColumns: string;
}>;
model?: AutoBeDatabase.IModel;
}): string {
const relations = AutoBeRealizeTransformerProgrammer.getRecursiveRelations({
schemas: props.schemas,
typeName: props.plan.dtoTypeName,
});
if (relations.parent !== null || relations.children !== null)
return writeRecursiveTemplate({
plan: props.plan,
schema: props.schema,
parentProperty: relations.parent,
childrenProperty: relations.children,
model: props.model,
});
const neighborRelations =
props.neighbors && props.relations
? AutoBeRealizeTransformerProgrammer.computeNeighborRelations({
schema: props.schema,
neighbors: props.neighbors,
relations: props.relations,
})
: [];
return writeNormalTemplate({
plan: props.plan,
schema: props.schema,
neighborRelations,
model: props.model,
});
}
function isScalarProperty(schema: AutoBeOpenApi.IJsonSchema): boolean {
if (AutoBeOpenApiTypeChecker.isString(schema)) return true;
if (AutoBeOpenApiTypeChecker.isNumber(schema)) return true;
if (AutoBeOpenApiTypeChecker.isInteger(schema)) return true;
if (AutoBeOpenApiTypeChecker.isBoolean(schema)) return true;
if (AutoBeOpenApiTypeChecker.isConstant(schema)) return true;
if (AutoBeOpenApiTypeChecker.isNull(schema)) return true;
if (AutoBeOpenApiTypeChecker.isOneOf(schema))
return schema.oneOf.every((s) => isScalarProperty(s));
return false;
}
function buildSelectEntries(props: {
schema: AutoBeOpenApi.IJsonSchemaDescriptive.IObject;
skipKeys: Set<string>;
neighborRelations: AutoBeRealizeTransformerProgrammer.INeighborRelation[];
model?: AutoBeDatabase.IModel;
}): { entries: string[]; hasUnresolved: boolean } {
if (props.model) {
return buildSelectEntriesFromModel({
schema: props.schema,
skipKeys: props.skipKeys,
neighborRelations: props.neighborRelations,
model: props.model,
});
}
return buildSelectEntriesFromDto(props);
}
function buildSelectEntriesFromModel(props: {
schema: AutoBeOpenApi.IJsonSchemaDescriptive.IObject;
skipKeys: Set<string>;
neighborRelations: AutoBeRealizeTransformerProgrammer.INeighborRelation[];
model: AutoBeDatabase.IModel;
}): { entries: string[]; hasUnresolved: boolean } {
const entries: string[] = [];
let hasUnresolved = false;
const coveredRelations = new Set(
props.neighborRelations.map((n) => n.relationKey),
);
// Primary key
if (!props.skipKeys.has(props.model.primaryField.name)) {
entries.push(`${props.model.primaryField.name}: true,`);
}
// Plain scalar fields
for (const field of props.model.plainFields) {
if (props.skipKeys.has(field.name)) continue;
entries.push(`${field.name}: true,`);
}
// Foreign keys / belongsTo relations
for (const fk of props.model.foreignFields) {
if (props.skipKeys.has(fk.name) || props.skipKeys.has(fk.relation.name))
continue;
if (coveredRelations.has(fk.relation.name)) {
const nr = props.neighborRelations.find(
(n) => n.relationKey === fk.relation.name,
)!;
entries.push(`${nr.relationKey}: ${nr.transformerName}.select(),`);
} else {
entries.push(`${fk.name}: true,`);
}
}
// hasMany/hasOne relations covered by neighbors (not already handled via FK)
for (const nr of props.neighborRelations) {
if (props.skipKeys.has(nr.relationKey)) continue;
const isBelongsTo = props.model.foreignFields.some(
(f) => f.relation.name === nr.relationKey,
);
if (!isBelongsTo) {
entries.push(`${nr.relationKey}: ${nr.transformerName}.select(),`);
}
}
// Check for unresolved non-scalar DTO properties
for (const k of Object.keys(props.schema.properties)) {
if (props.skipKeys.has(k)) continue;
if (props.neighborRelations.some((n) => n.dtoProperty === k)) continue;
if (!isScalarProperty(props.schema.properties[k]!)) {
hasUnresolved = true;
break;
}
}
return { entries, hasUnresolved };
}
function buildSelectEntriesFromDto(props: {
schema: AutoBeOpenApi.IJsonSchemaDescriptive.IObject;
skipKeys: Set<string>;
neighborRelations: AutoBeRealizeTransformerProgrammer.INeighborRelation[];
}): { entries: string[]; hasUnresolved: boolean } {
const entries: string[] = [];
let hasUnresolved = false;
for (const k of Object.keys(props.schema.properties)) {
if (props.skipKeys.has(k)) continue;
const nr = props.neighborRelations.find((n) => n.dtoProperty === k);
if (nr) {
entries.push(`${nr.relationKey}: ${nr.transformerName}.select(),`);
} else if (isScalarProperty(props.schema.properties[k]!)) {
entries.push(`${k}: true,`);
} else {
hasUnresolved = true;
}
}
return { entries, hasUnresolved };
}
function formatSelectBody(entries: string[], hasUnresolved: boolean): string {
return [...entries, ...(hasUnresolved ? ["..."] : [])].join("\n ");
}
/**
* Find the self-referential FK field for recursive templates. If multiple
* self-referential FKs exist, try to match by DTO property name.
*/
function findRecursiveFk(
model: AutoBeDatabase.IModel | undefined,
dtoProperty: string,
): AutoBeDatabase.IForeignField | undefined {
if (!model) return undefined;
const selfFks = model.foreignFields.filter(
(f) => f.relation.targetModel === model.name,
);
if (selfFks.length === 1) return selfFks[0];
return selfFks.find((f) => f.relation.name === dtoProperty) ?? selfFks[0];
}
function writeNormalTemplate(props: {
plan: AutoBeRealizeTransformerPlan;
schema: AutoBeOpenApi.IJsonSchemaDescriptive.IObject;
neighborRelations: AutoBeRealizeTransformerProgrammer.INeighborRelation[];
model?: AutoBeDatabase.IModel;
}): string {
const name: string = AutoBeRealizeTransformerProgrammer.getName(
props.plan.dtoTypeName,
);
const dto: string = props.plan.dtoTypeName;
const table: string = props.plan.databaseSchemaName;
const transformBody: string = Object.keys(props.schema.properties)
.map((k) => {
const nr = props.neighborRelations.find((n) => n.dtoProperty === k);
if (!nr) {
const hint = AutoBeOpenApiTypeChecker.getTypeName(
props.schema.properties[k]!,
);
return ` ${k}: {${hint}},`;
}
if (nr.isArray) {
const call = `await ArrayUtil.asyncMap(input.${nr.relationKey}, ${nr.transformerName}.transform)`;
if (nr.isNullable)
return ` ${k}: input.${nr.relationKey} ? ${call} : null,`;
return ` ${k}: ${call},`;
}
if (nr.isNullable)
return ` ${k}: input.${nr.relationKey} ? await ${nr.transformerName}.transform(input.${nr.relationKey}) : null,`;
return ` ${k}: await ${nr.transformerName}.transform(input.${nr.relationKey}),`;
})
.join("\n");
const { entries, hasUnresolved } = buildSelectEntries({
schema: props.schema,
skipKeys: new Set(),
neighborRelations: props.neighborRelations,
model: props.model,
});
const selectBody = formatSelectBody(entries, hasUnresolved);
return StringUtil.trim`
export namespace ${name} {
export type Payload = Prisma.${table}GetPayload<ReturnType<typeof select>>;
export function select() {
// implicit return type for better type inference
return {
select: {
${selectBody}
},
} satisfies Prisma.${table}FindManyArgs;
}
export async function transform(input: Payload): Promise<${dto}> {
return {
${transformBody}
};
}
}
`;
}
function writeRecursiveTemplate(props: {
plan: AutoBeRealizeTransformerPlan;
schema: AutoBeOpenApi.IJsonSchemaDescriptive.IObject;
parentProperty: string | null;
childrenProperty: string | null;
model?: AutoBeDatabase.IModel;
}): string {
const { parentProperty: pp, childrenProperty: cp } = props;
if (pp !== null && cp !== null)
return writeBothRecursiveTemplate({
...props,
parentProperty: pp,
childrenProperty: cp,
});
if (pp !== null)
return writeParentOnlyRecursiveTemplate({ ...props, parentProperty: pp });
return writeChildrenOnlyRecursiveTemplate({
...props,
childrenProperty: cp!,
});
}
function writeParentOnlyRecursiveTemplate(props: {
plan: AutoBeRealizeTransformerPlan;
schema: AutoBeOpenApi.IJsonSchemaDescriptive.IObject;
parentProperty: string;
model?: AutoBeDatabase.IModel;
}): string {
const name: string = AutoBeRealizeTransformerProgrammer.getName(
props.plan.dtoTypeName,
);
const dto: string = props.plan.dtoTypeName;
const table: string = props.plan.databaseSchemaName;
const pp: string = props.parentProperty;
const selfFk = findRecursiveFk(props.model, pp);
const fk: string = selfFk?.name ?? `${pp}_id`;
const relationName: string = selfFk?.relation.name ?? pp;
const transformBody: string = Object.keys(props.schema.properties)
.map((k) =>
k === pp
? ` ${k}: input.${fk} ? await cache.get(input.${fk}) : null,`
: ` ${k}: {${AutoBeOpenApiTypeChecker.getTypeName(props.schema.properties[k]!)}},`,
)
.join("\n");
const skipKeys = new Set([fk, relationName, pp]);
const { entries, hasUnresolved } = buildSelectEntries({
schema: props.schema,
skipKeys,
neighborRelations: [],
model: props.model,
});
const selectBody = formatSelectBody(
[
...entries,
`${fk}: true,`,
`${relationName}: undefined, // DO NOT select recursive relation`,
],
hasUnresolved,
);
return StringUtil.trim`
export namespace ${name} {
export type Payload = Prisma.${table}GetPayload<ReturnType<typeof select>>;
export function select() {
// implicit return type for better type inference
return {
select: {
${selectBody}
},
} satisfies Prisma.${table}FindManyArgs;
}
export async function transform(
input: Payload,
cache: VariadicSingleton<Promise<${dto}>, [string]> = createParentCache(),
): Promise<${dto}> {
return {
${transformBody}
};
}
export async function transformAll(
inputs: Payload[],
): Promise<${dto}[]> {
const cache = createParentCache();
return await ArrayUtil.asyncMap(inputs, (x) => transform(x, cache));
}
function createParentCache() {
const cache = new VariadicSingleton(
async (id: string): Promise<${dto}> => {
const record =
await MyGlobal.prisma.${table}.findFirstOrThrow({
...select(),
where: { id },
});
return transform(record, cache);
},
);
return cache;
}
}
`;
}
function writeChildrenOnlyRecursiveTemplate(props: {
plan: AutoBeRealizeTransformerPlan;
schema: AutoBeOpenApi.IJsonSchemaDescriptive.IObject;
childrenProperty: string;
model?: AutoBeDatabase.IModel;
}): string {
const name: string = AutoBeRealizeTransformerProgrammer.getName(
props.plan.dtoTypeName,
);
const dto: string = props.plan.dtoTypeName;
const table: string = props.plan.databaseSchemaName;
const cp: string = props.childrenProperty;
const selfFk = findRecursiveFk(props.model, cp);
const fk: string = selfFk?.name ?? "parent_id";
const transformBody: string = Object.keys(props.schema.properties)
.map((k) =>
k === cp
? ` ${k}: await cache.get(input.id),`
: ` ${k}: {${AutoBeOpenApiTypeChecker.getTypeName(props.schema.properties[k]!)}},`,
)
.join("\n");
const { entries, hasUnresolved } = buildSelectEntries({
schema: props.schema,
skipKeys: new Set([cp]),
neighborRelations: [],
model: props.model,
});
const selectBody = formatSelectBody(
[...entries, `${cp}: undefined, // DO NOT select recursive relation`],
hasUnresolved,
);
return StringUtil.trim`
export namespace ${name} {
export type Payload = Prisma.${table}GetPayload<ReturnType<typeof select>>;
export function select() {
// implicit return type for better type inference
return {
select: {
${selectBody}
},
} satisfies Prisma.${table}FindManyArgs;
}
export async function transform(
input: Payload,
cache: VariadicSingleton<Promise<${dto}[]>, [string]> = createChildrenCache(),
): Promise<${dto}> {
return {
${transformBody}
};
}
export async function transformAll(
inputs: Payload[],
): Promise<${dto}[]> {
const cache = createChildrenCache();
return await ArrayUtil.asyncMap(inputs, (x) => transform(x, cache));
}
function createChildrenCache() {
const cache = new VariadicSingleton(
async (parentId: string): Promise<${dto}[]> => {
const records =
await MyGlobal.prisma.${table}.findMany({
...select(),
where: { ${fk}: parentId },
});
return await ArrayUtil.asyncMap(records, (r) => transform(r, cache));
},
);
return cache;
}
}
`;
}
function writeBothRecursiveTemplate(props: {
plan: AutoBeRealizeTransformerPlan;
schema: AutoBeOpenApi.IJsonSchemaDescriptive.IObject;
parentProperty: string;
childrenProperty: string;
model?: AutoBeDatabase.IModel;
}): string {
const name: string = AutoBeRealizeTransformerProgrammer.getName(
props.plan.dtoTypeName,
);
const dto: string = props.plan.dtoTypeName;
const table: string = props.plan.databaseSchemaName;
const pp: string = props.parentProperty;
const cp: string = props.childrenProperty;
const selfFk = findRecursiveFk(props.model, pp);
const fk: string = selfFk?.name ?? `${pp}_id`;
const relationName: string = selfFk?.relation.name ?? pp;
const transformBody: string = Object.keys(props.schema.properties)
.map((k) => {
if (k === pp)
return ` ${k}: input.${fk} ? await parentCache.get(input.${fk}) : null,`;
if (k === cp) return ` ${k}: await childrenCache.get(input.id),`;
return ` ${k}: {${AutoBeOpenApiTypeChecker.getTypeName(props.schema.properties[k]!)}},`;
})
.join("\n");
const skipKeys = new Set([fk, relationName, pp, cp]);
const { entries, hasUnresolved } = buildSelectEntries({
schema: props.schema,
skipKeys,
neighborRelations: [],
model: props.model,
});
const selectBody = formatSelectBody(
[
...entries,
`${fk}: true,`,
`${relationName}: undefined, // DO NOT select recursive relation`,
`${cp}: undefined, // DO NOT select recursive relation`,
],
hasUnresolved,
);
return StringUtil.trim`
export namespace ${name} {
export type Payload = Prisma.${table}GetPayload<ReturnType<typeof select>>;
export function select() {
// implicit return type for better type inference
return {
select: {
${selectBody}
},
} satisfies Prisma.${table}FindManyArgs;
}
export async function transform(
input: Payload,
parentCache: VariadicSingleton<Promise<${dto}>, [string]> = createParentCache(),
childrenCache: VariadicSingleton<Promise<${dto}[]>, [string]> = createChildrenCache(),
): Promise<${dto}> {
return {
${transformBody}
};
}
export async function transformAll(
inputs: Payload[],
): Promise<${dto}[]> {
// Create mutually-referencing caches so the entire tree shares
// one deduplication scope across both parent and children lookups.
// Use definite assignment assertions (!) so TypeScript does not
// flag the cross-references as "used before assigned" — the async
// callbacks only execute after both variables are fully initialized.
let parentCache!: VariadicSingleton<Promise<${dto}>, [string]>;
let childrenCache!: VariadicSingleton<Promise<${dto}[]>, [string]>;
parentCache = new VariadicSingleton(
async (id: string): Promise<${dto}> => {
const record =
await MyGlobal.prisma.${table}.findFirstOrThrow({
...select(),
where: { id },
});
return transform(record, parentCache, childrenCache);
},
);
childrenCache = new VariadicSingleton(
async (parentId: string): Promise<${dto}[]> => {
const records =
await MyGlobal.prisma.${table}.findMany({
...select(),
where: { ${fk}: parentId },
});
return await ArrayUtil.asyncMap(records, (r) =>
transform(r, parentCache, childrenCache),
);
},
);
return await ArrayUtil.asyncMap(inputs, (x) =>
transform(x, parentCache, childrenCache),
);
}
function createParentCache() {
const cache = new VariadicSingleton(
async (id: string): Promise<${dto}> => {
const record =
await MyGlobal.prisma.${table}.findFirstOrThrow({
...select(),
where: { id },
});
return transform(record, cache);
},
);
return cache;
}
function createChildrenCache() {
const cache = new VariadicSingleton(
async (parentId: string): Promise<${dto}[]> => {
const records =
await MyGlobal.prisma.${table}.findMany({
...select(),
where: { ${fk}: parentId },
});
// createParentCache() is called once per batch so all siblings
// in the same children list share one parent-deduplication scope.
const parentCache = createParentCache();
return await ArrayUtil.asyncMap(records, (r) =>
transform(r, parentCache, cache),
);
},
);
return cache;
}
}
`;
}