UNPKG

prisma-markdown

Version:

Prisma Markdown documents generator including ERD diagrams and comment descriptions

310 lines (286 loc) 9.26 kB
import { DMMF } from "@prisma/generator-helper"; import { IPrismaMarkdownChapter } from "./IPrismaMarkdownChapter"; import { PrismaUtil } from "./utils/PrismaUtil"; import { MapUtil } from "./utils/MapUtil"; import { IPrismaMarkdownConfig } from "./IPrismaMarkdownConfig"; import { PrismaMarkdownMermaidWriter } from "./writers/PrismaMarkdownMermaidWriter"; import { PrismaMarkdownDescriptionWriter } from "./writers/PrismaMarkdownDescriptionWriter"; export namespace PrismaMarkdown { export const write = ( schema: DMMF.Datamodel, config?: IPrismaMarkdownConfig | undefined, ): string => { const chapters: IPrismaMarkdownChapter[] = categorize(schema); const title: string = config?.title ?? "Prisma Markdown"; const preface: string = [ `# ${title}`, "", "> Generated by [`prisma-markdown`](https://github.com/samchon/prisma-markdown)", "", ...chapters.map(({ name }) => `- [${name}](#${name.toLowerCase()})`), ].join("\n"); if (chapters.length === 0) return preface; return [ preface, // table of contents "", // gap between table of contents and first chapter chapters.map(writeChapter).join("\n\n"), // gap between chapters "", ].join("\n"); }; export const writeChapter = (chapter: IPrismaMarkdownChapter): string => [ `## ${chapter.name}`, "", ...(chapter.diagrams.length ? [PrismaMarkdownMermaidWriter.write(chapter.diagrams)] : []), ...(chapter.diagrams.length && chapter.descriptions ? [""] : []), ...(chapter.descriptions.length ? [ chapter.descriptions .map((c) => PrismaMarkdownDescriptionWriter.table(c)) .join("\n\n"), ] : []), ].join("\n"); export const writeDiagram = (diagrams: DMMF.Model[]): string => PrismaMarkdownMermaidWriter.write(diagrams); export const writeDescription = (model: DMMF.Model): string => PrismaMarkdownDescriptionWriter.table(model); export const categorize = ( schema: DMMF.Datamodel, ): IPrismaMarkdownChapter[] => { // LIST UP MODELS const dict: Map<string, IChapter> = new Map(); const modelList: DMMF.Model[] = schema.models.filter( (model) => !isHidden(model), ); findImplicits(modelList); const emplace = (name: string) => MapUtil.take(dict)(name, () => ({ name, descriptions: new Set(), diagrams: new Set(), })); // TOP NAMESPACE for (const model of modelList) { const namespaces: string[] = takeTags("namespace")(model); if (namespaces.length === 0) continue; const top: string = namespaces[0]; const chapter: IChapter = emplace(top); chapter.descriptions.add(model); chapter.diagrams.add(model); } // REMAINING NAMESPACES for (const model of modelList) { const namespaces: string[] = takeTags("namespace")(model); for (const name of namespaces.slice(1)) { const section = emplace(name); section.descriptions.add(model); section.diagrams.add(model); } } // DESCRIPTIONS for (const model of modelList) { const describes: string[] = takeTags("describe")(model); for (const name of describes) { const chapter: IChapter = MapUtil.take(dict)(name, () => ({ name, descriptions: new Set(), diagrams: new Set(), })); chapter.descriptions.add(model); } } // ERD ONLY for (const model of modelList) { const erdList: string[] = takeTags("erd")(model); for (const erd of erdList) { const chapter: IChapter = MapUtil.take(dict)(erd, () => ({ name: erd, descriptions: new Set(), diagrams: new Set(), })); chapter.diagrams.add(model); } } // DEFAULTS for (const model of modelList) { const keywords: string[] = [ ...takeTags("namespace")(model), ...takeTags("describe")(model), ...takeTags("erd")(model), ]; if (keywords.length !== 0) continue; const basic: IChapter = MapUtil.take(dict)("default", () => ({ name: "default", descriptions: new Set(), diagrams: new Set(), })); basic.descriptions.add(model); basic.diagrams.add(model); } // RETURNS return Array.from(dict.values()) .filter((c) => !!c.descriptions.size || !!c.diagrams.size) .map((chapter) => ({ name: chapter.name, descriptions: Array.from(chapter.descriptions), diagrams: Array.from(chapter.diagrams), })); }; const takeTags = (kind: "namespace" | "describe" | "erd") => (model: DMMF.Model): string[] => [ ...new Set( PrismaUtil.tagValues(kind)(model).map((str) => str.split(" ")[0]), ), ]; const isHidden = (model: DMMF.Model): boolean => model.documentation?.includes("@hidden") ?? false; const findImplicits = (modelList: DMMF.Model[]) => { const dict: Map<string, DMMF.Model> = new Map(); for (const model of modelList) for (const field of model.fields) { if ( field.kind !== "object" || field.isList !== true || field.isUnique !== false ) continue; const opposite: DMMF.Model | undefined = modelList.find( (model) => model.name === field.type, ); const oppositeField = opposite?.fields.find( (field) => field.kind === "object" && field.isList && field.type === model.name, ); if ( opposite === undefined || oppositeField === undefined || model === opposite ) continue; const relations: DMMF.Model[] = [model, opposite].sort((x, y) => x.name.localeCompare(y.name), ); const table: string = `_${relations[0].name}To${relations[1].name}`; if (dict.has(table)) continue; const newbie: DMMF.Model = implicitToExplicit(relations[0])( relations[1], ); modelList.push(newbie); dict.set(table, newbie); } }; const implicitToExplicit = (x: DMMF.Model) => (y: DMMF.Model): DMMF.Model => { const name: string = `_${x.name}To${y.name}`; const tagger = (kind: "namespace" | "describe" | "erd"): string[] => [...new Set([...takeTags(kind)(x), ...takeTags(kind)(y)])].map( (value) => `@${kind} ${value}`, ); const description: string[] = [ `Pair relationship table between {@link ${ x.dbName ?? x.name }} and {@link ${y.dbName ?? y.name}}`, "", ...tagger("describe"), ...tagger("erd"), ...tagger("namespace"), ]; if (description.length === 2) description.splice(1, 1); const newbie: DMMF.Model = { name, dbName: null, schema: null, fields: [ { kind: "scalar", name: "A", type: x.primaryKey?.fields[0] ?? "String", isRequired: true, isList: false, isUnique: false, isId: false, isReadOnly: false, hasDefaultValue: false, }, { kind: "scalar", name: "B", type: y.primaryKey?.fields[0] ?? "String", isRequired: true, isList: false, isUnique: false, isId: false, isReadOnly: false, hasDefaultValue: false, }, { kind: "object", name: x.name, type: x.name, isRequired: true, isList: false, isUnique: false, isId: false, isReadOnly: false, hasDefaultValue: false, relationToFields: [x.primaryKey?.fields[0] ?? "id"], relationFromFields: ["A"], }, { kind: "object", name: y.name, type: y.name, isRequired: true, isList: false, isUnique: false, isId: false, isReadOnly: false, hasDefaultValue: false, relationToFields: [y.primaryKey?.fields[0] ?? "id"], relationFromFields: ["B"], }, ], uniqueFields: [["A", "B"]], uniqueIndexes: [], primaryKey: null, documentation: description.join("\n"), }; (x.fields as DMMF.Field[]).push({ kind: "object", name, type: name, isRequired: true, isList: true, isUnique: false, isId: false, isReadOnly: false, hasDefaultValue: false, relationToFields: ["A"], }); (y.fields as DMMF.Field[]).push({ kind: "object", name, type: name, isRequired: true, isList: true, isUnique: false, isId: false, isReadOnly: false, hasDefaultValue: false, relationToFields: ["B"], }); return newbie; }; } interface IChapter { name: string; descriptions: Set<DMMF.Model>; diagrams: Set<DMMF.Model>; }