UNPKG

prisma-erd-generator

Version:

<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section --> [![All Contributors](https://img.shields.io/badge/all_contributors-20-orange.svg?style=flat-square)](#contributors-) <!-- ALL-CONTRIBUTORS-BADGE:END -->

397 lines (394 loc) 15.2 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/generate.ts var generate_exports = {}; __export(generate_exports, { default: () => generate_default, mapPrismaToDb: () => mapPrismaToDb, parseDatamodel: () => parseDatamodel }); module.exports = __toCommonJS(generate_exports); var path = __toESM(require("path")); var child_process = __toESM(require("child_process")); var import_node_fs = __toESM(require("fs")); var import_node_os = __toESM(require("os")); var dotenv = __toESM(require("dotenv")); dotenv.config(); function getDataModelFieldWithoutParsing(parsed) { const startOfField = parsed.indexOf('"datamodel"'); const openingBracket = parsed.indexOf("{", startOfField); let numberOfOpeningBrackets = 0; let closingBracket = openingBracket; while (closingBracket < parsed.length) { const char = parsed[closingBracket++]; if (char === "{") { numberOfOpeningBrackets++; } else if (char === "}") { numberOfOpeningBrackets--; if (numberOfOpeningBrackets === 0) { break; } } } return parsed.slice(openingBracket, closingBracket); } async function parseDatamodel(engine, model, tmpDir) { const tmpSchema = path.resolve(path.join(tmpDir, "schema.prisma")); import_node_fs.default.writeFileSync(tmpSchema, model); const parsed = await new Promise((resolve2, reject) => { const process2 = child_process.exec( `"${engine}" --datamodel-path="${tmpSchema}" cli dmmf` ); let output = ""; process2.stderr?.on("data", (l) => { if (l.includes("error:")) { reject(l.slice(l.indexOf("error:"), l.indexOf("\\n"))); } }); process2.stdout?.on("data", (d) => { output += d; }); process2.on("exit", () => { resolve2(output); }); }); return getDataModelFieldWithoutParsing(parsed); } function renderDml(dml, options) { const { tableOnly = false, ignoreEnums = false, includeRelationFromFields = false, disableEmoji = false } = options ?? {}; const diagram = "erDiagram"; const modellikes = dml.models.concat(dml.types); const enums = tableOnly || ignoreEnums ? "" : dml.enums.map( (model) => ` ${model.dbName || model.name} { ${model.values.map( (value) => `${value.name || value.dbName} ${value.dbName || value.name}` ).join("\n")} } ` ).join("\n\n"); const pkSigil = disableEmoji ? '"PK"' : '"\u{1F5DD}\uFE0F"'; const nullableSigil = disableEmoji ? '"nullable"' : '"\u2753"'; const classes = modellikes.map( (model) => ` "${model.dbName || model.name}" { ${tableOnly ? "" : model.fields.filter(isFieldShownInSchema(model, includeRelationFromFields)).map((field) => { return ` ${field.type.trimStart()} ${field.name.replace( /^_/, "z_" )} ${field.isId || model.primaryKey?.fields?.includes(field.name) ? pkSigil : ""}${field.isRequired ? "" : nullableSigil}`; }).join("\n")} } ` ).join("\n\n"); let relationships = ""; for (const model of modellikes) { for (const field of model.fields) { const isEnum = field.kind === "enum"; if (isEnum && (tableOnly || ignoreEnums)) { continue; } const relationshipName = `${isEnum ? "enum:" : ""}${field.name}`; const thisSide = `"${model.dbName || model.name}"`; const otherSide = `"${modellikes.find((ml) => ml.name === field.type)?.dbName || field.type}"`; if (field.relationFromFields && field.relationFromFields.length > 0 || isEnum) { let thisSideMultiplicity = "||"; if (field.isList) { thisSideMultiplicity = "}o"; } else if (!field.isRequired) { thisSideMultiplicity = "|o"; } const otherModel = modellikes.find( (model2) => model2.name === otherSide ); const otherField = otherModel?.fields.find( ({ relationName }) => relationName === field.relationName ); const otherSideMultiplicity = thisSideMultiplicity; if (otherField?.isList) { thisSideMultiplicity = "o{"; } else if (!otherField?.isRequired) { thisSideMultiplicity = "o|"; } relationships += ` ${thisSide} ${thisSideMultiplicity}--${otherSideMultiplicity} ${otherModel?.dbName || otherSide} : "${relationshipName}" `; } else if (modellikes.find( (m) => m.name === field.type || m.dbName === field.type ) && field.relationFromFields?.length === 0) { relationships += ` ${thisSide} o{--}o ${otherSide} : "${field.name}" `; } else if (field.kind === "object") { const otherSideCompositeType = dml.types.find( (model2) => model2.name.replace(/^_/, "z_").replace(/\s/g, "") // remove spaces === otherSide ); console.log(otherSide, otherSideCompositeType); if (otherSideCompositeType) { let thisSideMultiplicity = "||"; if (field.isList) { thisSideMultiplicity = "}o"; } else if (!field.isRequired) { thisSideMultiplicity = "|o"; } const otherField = otherSideCompositeType?.fields.find( ({ relationName }) => relationName === field.relationName ); const otherSideMultiplicity = thisSideMultiplicity; if (otherField?.isList) { thisSideMultiplicity = "o{"; } else if (!otherField?.isRequired) { thisSideMultiplicity = "o|"; } relationships += ` ${thisSide} ${thisSideMultiplicity}--${otherSideMultiplicity} ${otherSideCompositeType.dbName || otherSide} : "${relationshipName}" `; } } } } return `${diagram} ${enums} ${classes} ${relationships}`; } var isFieldShownInSchema = (model, includeRelationFromFields) => (field) => { if (includeRelationFromFields) { return field.kind !== "object"; } return field.kind !== "object" && !model.fields.find( ({ relationFromFields }) => relationFromFields?.includes(field.name) ); }; var mapPrismaToDb = (dmlModels, dataModel) => { const splitDataModel = dataModel?.split("\n").filter((line) => line.includes("@map") || line.includes("model ")).map((line) => line.trim()); return dmlModels.map((model) => { return { ...model, fields: model.fields.map((field) => { let filterStatus = "None"; const lineInDataModel = splitDataModel.filter((line) => { if (filterStatus === "Match" && line.includes("model ")) { filterStatus = "End"; } if (filterStatus === "None" && line.includes(`model ${model.name} `)) { filterStatus = "Match"; } return filterStatus === "Match"; }).find( (line) => line.includes(`${field.name} `) && line.includes("@map") ); if (lineInDataModel) { const regex = new RegExp(/@map\(\"(.*?)\"\)/, "g"); const match = regex.exec(lineInDataModel); if (match?.[1]) { const name = match[1].replace(/^_/, "z_").replace(/\s/g, ""); field.name = name; } } return field; }) }; }); }; var generate_default = async (options) => { try { const output = options.generator.output?.value || "./prisma/ERD.svg"; const config2 = options.generator.config; const theme = config2.theme ?? "forest"; let mermaidCliNodePath = path.resolve( path.join(config2.mmdcPath || "node_modules/.bin", "mmdc") ); const tableOnly = config2.tableOnly === "true"; const disableEmoji = config2.disableEmoji === "true"; const ignoreEnums = config2.ignoreEnums === "true"; const includeRelationFromFields = config2.includeRelationFromFields === "true"; const disabled = process.env.DISABLE_ERD === "true" || config2.disabled === "true"; const debug = config2.erdDebug === "true" || Boolean(process.env.ERD_DEBUG); if (debug) { console.log("debug mode enabled"); console.log("config", config2); } if (disabled) { return console.log("ERD generator is disabled"); } const queryEngines = Object.values(options.binaryPaths?.queryEngine || {}); if (!queryEngines[0]) throw new Error("no query engine found"); const queryEngine = queryEngines[0]; const tmpDir = import_node_fs.default.mkdtempSync(`${import_node_os.default.tmpdir() + path.sep}prisma-erd-`); const datamodelString = await parseDatamodel( queryEngine, options.datamodel, tmpDir ); if (!datamodelString) { throw new Error("could not parse datamodel"); } if (debug && datamodelString) { import_node_fs.default.mkdirSync(path.resolve("prisma/debug"), { recursive: true }); const dataModelFile = path.resolve("prisma/debug/1-datamodel.json"); import_node_fs.default.writeFileSync(dataModelFile, datamodelString); console.log(`data model written to ${dataModelFile}`); } const dml = JSON.parse(datamodelString); dml.models = mapPrismaToDb(dml.models, options.datamodel); if (!dml.types) { dml.types = []; } if (debug && dml.models) { const mapAppliedFile = path.resolve( "prisma/debug/2-datamodel-map-applied.json" ); import_node_fs.default.writeFileSync(mapAppliedFile, JSON.stringify(dml, null, 2)); console.log(`applied @map to fields written to ${mapAppliedFile}`); } const mermaid = renderDml(dml, { tableOnly, ignoreEnums, includeRelationFromFields, disableEmoji }); if (debug && mermaid) { const mermaidFile = path.resolve("prisma/debug/3-mermaid.mmd"); import_node_fs.default.writeFileSync(mermaidFile, mermaid); console.log(`mermaid written to ${mermaidFile}`); } if (!mermaid) throw new Error("failed to construct mermaid instance from dml"); if (output.endsWith(".md")) return import_node_fs.default.writeFileSync(output, `\`\`\`mermaid ${mermaid}\`\`\` `); const tempMermaidFile = path.resolve(path.join(tmpDir, "prisma.mmd")); import_node_fs.default.writeFileSync(tempMermaidFile, mermaid); const defaultMermaidConfig = { deterministicIds: true, maxTextSize: 9e4, er: { useMaxWidth: true }, theme }; let mermaidConfig = defaultMermaidConfig; if (config2?.mermaidConfig) { const importedMermaidConfig = await import(path.resolve(config2.mermaidConfig)); if (debug) { console.log("imported mermaid config: ", importedMermaidConfig); } mermaidConfig = { ...defaultMermaidConfig, ...importedMermaidConfig }; } const tempConfigFile = path.resolve(path.join(tmpDir, "config.json")); import_node_fs.default.writeFileSync(tempConfigFile, JSON.stringify(mermaidConfig)); let puppeteerConfig = config2.puppeteerConfig; if (puppeteerConfig && !import_node_fs.default.existsSync(puppeteerConfig)) { throw new Error( `Puppeteer config file "${puppeteerConfig}" does not exist` ); } if (!puppeteerConfig) { const tempPuppeteerConfigFile = path.resolve( path.join(tmpDir, "puppeteerConfig.json") ); let executablePath; const puppeteerConfigJson = { logLevel: debug ? "warn" : "error", executablePath }; if (import_node_os.default.platform() === "darwin" && import_node_os.default.arch() === "arm64") { try { const executablePath2 = child_process.execSync("which chromium").toString().replace("\n", ""); if (!executablePath2) { throw new Error( "Could not find chromium executable. Refer to https://github.com/keonik/prisma-erd-generator#issues for next steps." ); } puppeteerConfigJson.executablePath = executablePath2; puppeteerConfigJson.args = ["--no-sandbox"]; } catch (error) { console.error(error); console.log( ` Prisma ERD Generator: Unable to find chromium path for you MacOS arm64 machine. Attempting to use the default at ${executablePath}. To learn more visit https://github.com/keonik/prisma-erd-generator#-arm64-users- ` ); executablePath = "/usr/bin/chromium-browser"; } } import_node_fs.default.writeFileSync( tempPuppeteerConfigFile, JSON.stringify(puppeteerConfigJson) ); puppeteerConfig = tempPuppeteerConfigFile; } if (config2.mmdcPath) { if (!import_node_fs.default.existsSync(mermaidCliNodePath)) { throw new Error( ` Mermaid CLI provided path does not exist. ${mermaidCliNodePath}` ); } } else if (!import_node_fs.default.existsSync(mermaidCliNodePath)) { const findMermaidCli = child_process.execSync("find ../.. -name mmdc").toString().split("\n").filter((path2) => path2).pop(); if (!findMermaidCli || !import_node_fs.default.existsSync(findMermaidCli)) { throw new Error( `Expected mermaid CLI at ${mermaidCliNodePath} or ${findMermaidCli} but this package was not found.` ); } mermaidCliNodePath = path.resolve(findMermaidCli); } const mermaidCommand = `"${mermaidCliNodePath}" -i "${tempMermaidFile}" -o "${output}" -c "${tempConfigFile}" -p "${puppeteerConfig}"`; if (debug && mermaidCommand) console.log("mermaid command: ", mermaidCommand); child_process.execSync(mermaidCommand); if (!import_node_fs.default.existsSync(output)) { throw new Error( `Issue generating ER Diagram. Expected ${output} to be created` ); } } catch (error) { console.error(error); throw error; } }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { mapPrismaToDb, parseDatamodel });