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 -->

726 lines (721 loc) 28.3 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 __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, { get: (a, b) => (typeof require !== "undefined" ? require : a)[b] }) : x)(function(x) { if (typeof require !== "undefined") return require.apply(this, arguments); throw Error('Dynamic require of "' + x + '" is not supported'); }); var __commonJS = (cb, mod) => function __require2() { return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; }; 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 )); // node_modules/.pnpm/dotenv@16.4.7/node_modules/dotenv/package.json var require_package = __commonJS({ "node_modules/.pnpm/dotenv@16.4.7/node_modules/dotenv/package.json"(exports, module) { module.exports = { name: "dotenv", version: "16.4.7", description: "Loads environment variables from .env file", main: "lib/main.js", types: "lib/main.d.ts", exports: { ".": { types: "./lib/main.d.ts", require: "./lib/main.js", default: "./lib/main.js" }, "./config": "./config.js", "./config.js": "./config.js", "./lib/env-options": "./lib/env-options.js", "./lib/env-options.js": "./lib/env-options.js", "./lib/cli-options": "./lib/cli-options.js", "./lib/cli-options.js": "./lib/cli-options.js", "./package.json": "./package.json" }, scripts: { "dts-check": "tsc --project tests/types/tsconfig.json", lint: "standard", pretest: "npm run lint && npm run dts-check", test: "tap run --allow-empty-coverage --disable-coverage --timeout=60000", "test:coverage": "tap run --show-full-coverage --timeout=60000 --coverage-report=lcov", prerelease: "npm test", release: "standard-version" }, repository: { type: "git", url: "git://github.com/motdotla/dotenv.git" }, funding: "https://dotenvx.com", keywords: [ "dotenv", "env", ".env", "environment", "variables", "config", "settings" ], readmeFilename: "README.md", license: "BSD-2-Clause", devDependencies: { "@types/node": "^18.11.3", decache: "^4.6.2", sinon: "^14.0.1", standard: "^17.0.0", "standard-version": "^9.5.0", tap: "^19.2.0", typescript: "^4.8.4" }, engines: { node: ">=12" }, browser: { fs: false } }; } }); // node_modules/.pnpm/dotenv@16.4.7/node_modules/dotenv/lib/main.js var require_main = __commonJS({ "node_modules/.pnpm/dotenv@16.4.7/node_modules/dotenv/lib/main.js"(exports, module) { "use strict"; var fs2 = __require("fs"); var path2 = __require("path"); var os2 = __require("os"); var crypto = __require("crypto"); var packageJson = require_package(); var version = packageJson.version; var LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg; function parse(src) { const obj = {}; let lines = src.toString(); lines = lines.replace(/\r\n?/mg, "\n"); let match; while ((match = LINE.exec(lines)) != null) { const key = match[1]; let value = match[2] || ""; value = value.trim(); const maybeQuote = value[0]; value = value.replace(/^(['"`])([\s\S]*)\1$/mg, "$2"); if (maybeQuote === '"') { value = value.replace(/\\n/g, "\n"); value = value.replace(/\\r/g, "\r"); } obj[key] = value; } return obj; } function _parseVault(options) { const vaultPath = _vaultPath(options); const result = DotenvModule.configDotenv({ path: vaultPath }); if (!result.parsed) { const err = new Error(`MISSING_DATA: Cannot parse ${vaultPath} for an unknown reason`); err.code = "MISSING_DATA"; throw err; } const keys = _dotenvKey(options).split(","); const length = keys.length; let decrypted; for (let i = 0; i < length; i++) { try { const key = keys[i].trim(); const attrs = _instructions(result, key); decrypted = DotenvModule.decrypt(attrs.ciphertext, attrs.key); break; } catch (error) { if (i + 1 >= length) { throw error; } } } return DotenvModule.parse(decrypted); } function _log(message) { console.log(`[dotenv@${version}][INFO] ${message}`); } function _warn(message) { console.log(`[dotenv@${version}][WARN] ${message}`); } function _debug(message) { console.log(`[dotenv@${version}][DEBUG] ${message}`); } function _dotenvKey(options) { if (options && options.DOTENV_KEY && options.DOTENV_KEY.length > 0) { return options.DOTENV_KEY; } if (process.env.DOTENV_KEY && process.env.DOTENV_KEY.length > 0) { return process.env.DOTENV_KEY; } return ""; } function _instructions(result, dotenvKey) { let uri; try { uri = new URL(dotenvKey); } catch (error) { if (error.code === "ERR_INVALID_URL") { const err = new Error("INVALID_DOTENV_KEY: Wrong format. Must be in valid uri format like dotenv://:key_1234@dotenvx.com/vault/.env.vault?environment=development"); err.code = "INVALID_DOTENV_KEY"; throw err; } throw error; } const key = uri.password; if (!key) { const err = new Error("INVALID_DOTENV_KEY: Missing key part"); err.code = "INVALID_DOTENV_KEY"; throw err; } const environment = uri.searchParams.get("environment"); if (!environment) { const err = new Error("INVALID_DOTENV_KEY: Missing environment part"); err.code = "INVALID_DOTENV_KEY"; throw err; } const environmentKey = `DOTENV_VAULT_${environment.toUpperCase()}`; const ciphertext = result.parsed[environmentKey]; if (!ciphertext) { const err = new Error(`NOT_FOUND_DOTENV_ENVIRONMENT: Cannot locate environment ${environmentKey} in your .env.vault file.`); err.code = "NOT_FOUND_DOTENV_ENVIRONMENT"; throw err; } return { ciphertext, key }; } function _vaultPath(options) { let possibleVaultPath = null; if (options && options.path && options.path.length > 0) { if (Array.isArray(options.path)) { for (const filepath of options.path) { if (fs2.existsSync(filepath)) { possibleVaultPath = filepath.endsWith(".vault") ? filepath : `${filepath}.vault`; } } } else { possibleVaultPath = options.path.endsWith(".vault") ? options.path : `${options.path}.vault`; } } else { possibleVaultPath = path2.resolve(process.cwd(), ".env.vault"); } if (fs2.existsSync(possibleVaultPath)) { return possibleVaultPath; } return null; } function _resolveHome(envPath) { return envPath[0] === "~" ? path2.join(os2.homedir(), envPath.slice(1)) : envPath; } function _configVault(options) { _log("Loading env from encrypted .env.vault"); const parsed = DotenvModule._parseVault(options); let processEnv = process.env; if (options && options.processEnv != null) { processEnv = options.processEnv; } DotenvModule.populate(processEnv, parsed, options); return { parsed }; } function configDotenv(options) { const dotenvPath = path2.resolve(process.cwd(), ".env"); let encoding = "utf8"; const debug = Boolean(options && options.debug); if (options && options.encoding) { encoding = options.encoding; } else { if (debug) { _debug("No encoding is specified. UTF-8 is used by default"); } } let optionPaths = [dotenvPath]; if (options && options.path) { if (!Array.isArray(options.path)) { optionPaths = [_resolveHome(options.path)]; } else { optionPaths = []; for (const filepath of options.path) { optionPaths.push(_resolveHome(filepath)); } } } let lastError; const parsedAll = {}; for (const path3 of optionPaths) { try { const parsed = DotenvModule.parse(fs2.readFileSync(path3, { encoding })); DotenvModule.populate(parsedAll, parsed, options); } catch (e) { if (debug) { _debug(`Failed to load ${path3} ${e.message}`); } lastError = e; } } let processEnv = process.env; if (options && options.processEnv != null) { processEnv = options.processEnv; } DotenvModule.populate(processEnv, parsedAll, options); if (lastError) { return { parsed: parsedAll, error: lastError }; } else { return { parsed: parsedAll }; } } function config2(options) { if (_dotenvKey(options).length === 0) { return DotenvModule.configDotenv(options); } const vaultPath = _vaultPath(options); if (!vaultPath) { _warn(`You set DOTENV_KEY but you are missing a .env.vault file at ${vaultPath}. Did you forget to build it?`); return DotenvModule.configDotenv(options); } return DotenvModule._configVault(options); } function decrypt(encrypted, keyStr) { const key = Buffer.from(keyStr.slice(-64), "hex"); let ciphertext = Buffer.from(encrypted, "base64"); const nonce = ciphertext.subarray(0, 12); const authTag = ciphertext.subarray(-16); ciphertext = ciphertext.subarray(12, -16); try { const aesgcm = crypto.createDecipheriv("aes-256-gcm", key, nonce); aesgcm.setAuthTag(authTag); return `${aesgcm.update(ciphertext)}${aesgcm.final()}`; } catch (error) { const isRange = error instanceof RangeError; const invalidKeyLength = error.message === "Invalid key length"; const decryptionFailed = error.message === "Unsupported state or unable to authenticate data"; if (isRange || invalidKeyLength) { const err = new Error("INVALID_DOTENV_KEY: It must be 64 characters long (or more)"); err.code = "INVALID_DOTENV_KEY"; throw err; } else if (decryptionFailed) { const err = new Error("DECRYPTION_FAILED: Please check your DOTENV_KEY"); err.code = "DECRYPTION_FAILED"; throw err; } else { throw error; } } } function populate(processEnv, parsed, options = {}) { const debug = Boolean(options && options.debug); const override = Boolean(options && options.override); if (typeof parsed !== "object") { const err = new Error("OBJECT_REQUIRED: Please check the processEnv argument being passed to populate"); err.code = "OBJECT_REQUIRED"; throw err; } for (const key of Object.keys(parsed)) { if (Object.prototype.hasOwnProperty.call(processEnv, key)) { if (override === true) { processEnv[key] = parsed[key]; } if (debug) { if (override === true) { _debug(`"${key}" is already defined and WAS overwritten`); } else { _debug(`"${key}" is already defined and was NOT overwritten`); } } } else { processEnv[key] = parsed[key]; } } } var DotenvModule = { configDotenv, _configVault, _parseVault, config: config2, decrypt, parse, populate }; module.exports.configDotenv = DotenvModule.configDotenv; module.exports._configVault = DotenvModule._configVault; module.exports._parseVault = DotenvModule._parseVault; module.exports.config = DotenvModule.config; module.exports.decrypt = DotenvModule.decrypt; module.exports.parse = DotenvModule.parse; module.exports.populate = DotenvModule.populate; module.exports = DotenvModule; } }); // src/generate.ts var path = __toESM(__require("node:path")); var child_process = __toESM(__require("node:child_process")); var import_node_fs = __toESM(__require("node:fs")); var import_node_os = __toESM(__require("node:os")); var dotenv = __toESM(require_main()); 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; } }; })();