UNPKG

@autobe/compiler

Version:

AI backend server code generator

413 lines (391 loc) 11.5 kB
import { AutoBePrisma } from "@autobe/interface"; import { MapUtil, StringUtil } from "@autobe/utils"; import crypto from "crypto"; import { ArrayUtil } from "../utils/ArrayUtil"; export function writePrismaApplication(props: { dbms: "postgres" | "sqlite"; application: AutoBePrisma.IApplication; }): Record<string, string> { for (const file of props.application.files) for (const model of file.models) fillMappingName(model); return { ...Object.fromEntries( props.application.files .filter((file) => file.filename !== "main.prisma") .map((file) => [ file.filename, writeFile({ ...props, file, }), ]), ), "main.prisma": props.dbms === "postgres" ? POSTGRES_MAIN_FILE : SQLITE_MAIN_FILE, }; } function writeFile(props: { dbms: "postgres" | "sqlite"; application: AutoBePrisma.IApplication; file: AutoBePrisma.IFile; }): string { return props.file.models .map((model) => writeModel({ ...props, model, }), ) .join("\n\n"); } function writeModel(props: { dbms: "postgres" | "sqlite"; application: AutoBePrisma.IApplication; file: AutoBePrisma.IFile; model: AutoBePrisma.IModel; }): string { return [ writeComment( [ props.model.description, "", ...(props.model.material ? [] : [`@namespace ${props.file.namespace}`]), "@author AutoBE - https://github.com/wrtnlabs/autobe", ].join("\n"), 80, ), `model ${props.model.name} {`, addIndent( ArrayUtil.paddle([writeColumns(props), writeRelations(props)]).join("\n"), ), "}", ].join("\n"); } function fillMappingName(model: AutoBePrisma.IModel): void { const group: Map<string, AutoBePrisma.IForeignField[]> = new Map(); for (const ff of model.foreignFields) { MapUtil.take(group, ff.relation.targetModel, () => []).push(ff); if (ff.relation.targetModel == model.name) ff.relation.mappingName = "recursive"; } for (const array of group.values()) if (array.length !== 1) for (const ff of array) ff.relation.mappingName = shortName(`${model.name}_of_${ff.name}`); } /* ----------------------------------------------------------- COLUMNS ----------------------------------------------------------- */ function writeColumns(props: { dbms: "postgres" | "sqlite"; model: AutoBePrisma.IModel; }): string[] { return [ "//----", "// COLUMNS", "//----", writePrimary({ dbms: props.dbms, field: props.model.primaryField, }), ...props.model.foreignFields .map((x) => [ "", writeField({ dbms: props.dbms, field: x, }), ]) .flat(), ...props.model.plainFields .map((x) => [ "", writeField({ dbms: props.dbms, field: x, }), ]) .flat(), ]; } function writePrimary(props: { dbms: "postgres" | "sqlite"; field: AutoBePrisma.IPrimaryField; }): string { const type: string | undefined = props.dbms === "postgres" ? POSTGRES_PHYSICAL_TYPES.uuid : undefined; return [ writeComment(props.field.description, 78), `${props.field.name} String @id${type ? ` ${type}` : ""}`, ].join("\n"); } function writeField(props: { dbms: "postgres" | "sqlite"; field: AutoBePrisma.IPlainField; }): string { const logical: string = LOGICAL_TYPES[props.field.type]; const physical: string | undefined = props.dbms === "postgres" ? POSTGRES_PHYSICAL_TYPES[ props.field.type as keyof typeof POSTGRES_PHYSICAL_TYPES ] : undefined; return [ writeComment(props.field.description, 78), [ props.field.name, `${logical}${props.field.nullable ? "?" : ""}`, ...(physical ? [physical] : []), ].join(" "), ].join("\n"); } /* ----------------------------------------------------------- RELATIONS ----------------------------------------------------------- */ function writeRelations(props: { dbms: "postgres" | "sqlite"; application: AutoBePrisma.IApplication; model: AutoBePrisma.IModel; }): string[] { interface IHasRelationship { modelName: string; unique: boolean; mappingName?: string; } const hasRelationships: IHasRelationship[] = props.application.files .map((otherFile) => otherFile.models.map((otherModel) => otherModel.foreignFields .filter( (otherForeign) => otherForeign.relation.targetModel === props.model.name, ) .map((otherForeign) => ({ modelName: otherModel.name, unique: otherForeign.unique, mappingName: otherForeign.relation.mappingName, })), ), ) .flat(2); const foreignIndexes: AutoBePrisma.IForeignField[] = props.model.foreignFields.filter((f) => { if (f.unique === true) return props.model.uniqueIndexes.every( (u) => u.fieldNames.length !== 1 || u.fieldNames[0] !== f.name, ); return ( props.model.uniqueIndexes.every((u) => u.fieldNames[0] !== f.name) && props.model.plainIndexes.every((p) => p.fieldNames[0] !== f.name) ); }); const contents: string[][] = [ props.model.foreignFields.map((foreign) => writeConstraint({ dbms: props.dbms, model: props.model, foreign, }), ), hasRelationships.map((r) => [ r.mappingName ?? r.modelName, `${r.modelName}${r.unique ? "?" : "[]"}`, ...(r.mappingName ? [`@relation("${r.mappingName}")`] : []), ].join(" "), ), foreignIndexes.map((field) => writeForeignIndex({ model: props.model, field, }), ), [ ...props.model.uniqueIndexes.map((unique) => writeUniqueIndex({ model: props.model, unique, }), ), ...props.model.plainIndexes.map((plain) => writePlainIndex({ model: props.model, plain, }), ), ...(props.dbms === "postgres" ? props.model.ginIndexes.map((gin) => writeGinIndex({ model: props.model, gin, }), ) : []), ], ]; if (contents.every((c) => c.length === 0)) return []; return [ "//----", "// RELATIONS", "//----", // paddled content ...ArrayUtil.paddle(contents), ]; } function writeConstraint(props: { dbms: "postgres" | "sqlite"; model: AutoBePrisma.IModel; foreign: AutoBePrisma.IForeignField; }): string { // spellchecker:ignore-next-line const name: string = `${props.model.name}_${props.foreign.name}_rela`; return [ props.foreign.relation.name, `${props.foreign.relation.targetModel}${props.foreign.nullable ? "?" : ""}`, `@relation(${[ ...(props.foreign.relation.mappingName ? [`"${props.foreign.relation.mappingName}"`] : []), `fields: [${props.foreign.name}]`, `references: [id]`, `onDelete: Cascade`, ...(props.dbms === "sqlite" || name.length <= MAX_IDENTIFIER_LENGTH ? [] : [`map: "${shortName(name)}"`]), ].join(", ")})`, ].join(" "); } function writeForeignIndex(props: { model: AutoBePrisma.IModel; field: AutoBePrisma.IForeignField; }): string { const name: string = `${props.model.name}_${props.field.name}_fkey`; const prefix: string = `@@${props.field.unique === true ? "unique" : "index"}([${props.field.name}]`; if (name.length <= MAX_IDENTIFIER_LENGTH) return `${prefix})`; return `${prefix}, map: "${shortName(name)}")`; } function writeUniqueIndex(props: { model: AutoBePrisma.IModel; unique: AutoBePrisma.IUniqueIndex; }): string { const name: string = `${props.model.name}_${props.unique.fieldNames.join("_")}_key`; const prefix: string = `@@unique([${props.unique.fieldNames.join(", ")}]`; if (name.length <= MAX_IDENTIFIER_LENGTH) return `${prefix})`; return `${prefix}, map: "${shortName(name)}")`; } function writePlainIndex(props: { model: AutoBePrisma.IModel; plain: AutoBePrisma.IPlainIndex; }): string { const name: string = `${props.model.name}_${props.plain.fieldNames.join("_")}_idx`; const prefix: string = `@@index([${props.plain.fieldNames.join(", ")}]`; if (name.length <= MAX_IDENTIFIER_LENGTH) return `${prefix})`; return `${prefix}, map: "${shortName(name)}")`; } function writeGinIndex(props: { model: AutoBePrisma.IModel; gin: AutoBePrisma.IGinIndex; }): string { const name: string = `${props.model.name}_${props.gin.fieldName}_idx`; const prefix: string = `@@index([${props.gin.fieldName}(ops: raw("gin_trgm_ops"))], type: Gin`; if (name.length <= MAX_IDENTIFIER_LENGTH) return `${prefix})`; return `${prefix}, map: "${shortName(name)}")`; } /* ----------------------------------------------------------- BACKGROUND ----------------------------------------------------------- */ function writeComment(content: string, length: number): string { return content .split("\r\n") .join("\n") .split("\n") .map((line) => line.trim()) .map((line) => { // 77자에서 "/// " 4자를 뺀 73자가 실제 컨텐츠 최대 길이 if (line.length <= length - 4) return [line]; const words: string[] = line.split(" "); const result: string[] = []; let currentLine = ""; for (const word of words) { const potentialLine = currentLine ? `${currentLine} ${word}` : word; if (potentialLine.length <= 73) { currentLine = potentialLine; } else { if (currentLine) result.push(currentLine); currentLine = word; } } if (currentLine) result.push(currentLine); return result; }) .flat() .map((str) => `///${str.length ? ` ${str}` : ""}`) .join("\n") .trim(); } function addIndent(content: string): string { return content .split("\r\n") .join("\n") .split("\n") .map((str) => ` ${str}`) .join("\n"); } function shortName(name: string): string { if (name.length <= MAX_IDENTIFIER_LENGTH) return name; const hash: string = crypto .createHash("md5") .update(name) .digest("hex") .substring(0, HASH_TRUNCATION_LENGTH); return `${name.substring(0, MAX_IDENTIFIER_LENGTH - HASH_TRUNCATION_LENGTH - 1)}_${hash}`; } const LOGICAL_TYPES = { // native types boolean: "Boolean", int: "Int", double: "Float", string: "String", // formats datetime: "DateTime", uuid: "String", uri: "String", }; const POSTGRES_PHYSICAL_TYPES = { int: "@db.Integer", double: "@db.DoublePrecision", uuid: "@db.Uuid", datetime: "@db.Timestamptz", uri: "@db.VarChar(80000)", }; const POSTGRES_MAIN_FILE = StringUtil.trim` generator client { provider = "prisma-client-js" previewFeatures = ["postgresqlExtensions", "views"] binaryTargets = ["native"] } datasource db { provider = "postgresql" url = env("DATABASE_URL") extensions = [pg_trgm] } generator markdown { provider = "prisma-markdown" output = "../../docs/ERD.md" } `; const SQLITE_MAIN_FILE = StringUtil.trim` generator client { provider = "prisma-client-js" } datasource db { provider = "sqlite" url = "file:../../data.db" } generator markdown { provider = "prisma-markdown" output = "../../docs/ERD.md" } `; const MAX_IDENTIFIER_LENGTH = 63; const HASH_TRUNCATION_LENGTH = 8;