UNPKG

erdia

Version:

CLI to generate mermaid.js ER diagram using TypeORM entity

1,410 lines (1,322 loc) 90.5 kB
var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => { __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); return value; }; var __accessCheck = (obj, member, msg) => { if (!member.has(obj)) throw TypeError("Cannot " + msg); }; var __privateGet = (obj, member, getter) => { __accessCheck(obj, member, "read from private field"); return getter ? getter.call(obj) : member.get(obj); }; var __privateAdd = (obj, member, value) => { if (member.has(obj)) throw TypeError("Cannot add the same private member more than once"); member instanceof WeakSet ? member.add(obj) : member.set(obj, value); }; var __privateSet = (obj, member, value, setter) => { __accessCheck(obj, member, "write to private field"); setter ? setter.call(obj, value) : member.set(obj, value); return value; }; // src/common/getColumnHash.ts function getColumnHash(column) { const baseHash = [column.entity, column.dbName].join(":"); const base64 = Buffer.from(baseHash).toString("base64"); return base64; } // src/common/getDatabaseName.ts import { TextDecoder } from "util"; function getDatabaseName(options) { const name = options.database; if (typeof name === "string") { return name; } if (name instanceof Uint8Array) { return new TextDecoder().decode(name); } return "default"; } // src/common/getEntityHash.ts function getEntityHash(entity) { const baseHash = [entity.entity, entity.dbName].join(":"); const base64 = Buffer.from(baseHash).toString("base64"); return base64; } // src/common/getFileVersion.ts import { parse } from "jsonc-parser"; import semver from "semver"; function getFileVersion(buf) { const rawJson = buf.toString(); if (semver.valid(rawJson)) { return rawJson; } const parsed = parse(rawJson); const { version } = parsed; if (version == null || version === "") { throw new Error(`invalid version file: ${rawJson}`); } return version; } // src/common/getIndexHash.ts function getIndexHash(column) { const baseHash = [column.entity, column.dbName].join(":"); const base64 = Buffer.from(baseHash).toString("base64"); return base64; } // src/common/getPackageName.ts function getPackageName(json) { const { name } = json; if (typeof name === "string" && name !== "") { return name; } throw new Error("Cannot get project name from package.json"); } // src/configs/const-enum/CE_PROJECT_NAME_FROM.ts var CE_PROJECT_NAME_FROM = { DATABASE: "db", APPLICATION: "app" }; // src/common/getProjectName.ts async function getProjectName(dataSource, json, option) { if (option.projectName === CE_PROJECT_NAME_FROM.DATABASE) { if (dataSource.options.database != null) { const databaseName = getDatabaseName(dataSource.options); return databaseName; } const name2 = getPackageName(json); return name2; } const name = getPackageName(json); return name; } // src/configs/const-enum/CE_DEFAULT_VALUE.ts var CE_DEFAULT_VALUE = { CONFIG_FILE_NAME: ".erdiarc", TSCONFIG_FILE_NAME: "tsconfig.json", HTML_INDEX_FILENAME: "index.html", HTML_MERMAID_FILENAME: "mermaid.html", MARKDOWN_FILENAME: "erdia.md", DATABASE_FILENAME: "erdiadb.json", VERSION_FILENAME: ".erdiaverrc", TEMPLATES_PATH: "templates", DATA_SOURCE_FILE_FUZZY_SCORE_LIMIT: 50, OUTPUT_DIRECTORY_FUZZY_SCORE_LIMIT: 50 }; // src/configs/modules/getCwd.ts function getCwd(env) { if ((env.USE_INIT_CWD ?? "false") === "false") { return process.cwd(); } if (env.INIT_CWD != null) { return env.INIT_CWD; } return process.cwd(); } // src/modules/files/getFindFile.ts import findUp from "find-up"; async function getFindFile(filename, option) { const finded = await findUp(filename, option); return finded; } // src/modules/files/betterMkdir.ts import { isFalse } from "my-easy-fp"; import { exists, getDirname } from "my-node-fp"; import fs from "fs"; import pathe from "pathe"; async function betterMkdir(filePath) { const isFilePathExist = await exists(filePath); if (isFalse(isFilePathExist)) { const extname = pathe.extname(filePath); const hasExtname = extname !== "" && extname.length > 0; if (hasExtname) { const dirPath = await getDirname(filePath); await fs.promises.mkdir(dirPath, { recursive: true }); } else { await fs.promises.mkdir(filePath, { recursive: true }); } } } // src/modules/files/getOutputDirPath.ts import { isFalse as isFalse2 } from "my-easy-fp"; import { exists as exists2, getDirname as getDirname2, isDirectory } from "my-node-fp"; import pathe2 from "pathe"; async function getOutputDirPath(option, cwd) { const outputDirPath = option.output ?? cwd; const resolvedOutputDirPath = pathe2.resolve(outputDirPath); if (isFalse2(await exists2(resolvedOutputDirPath))) { await betterMkdir(pathe2.join(resolvedOutputDirPath)); return resolvedOutputDirPath; } if (isFalse2(await isDirectory(outputDirPath))) { return pathe2.resolve(await getDirname2(outputDirPath)); } return pathe2.resolve(outputDirPath); } // src/common/getVersion.ts import dayjs from "dayjs"; import fs2 from "fs"; import pathe3 from "pathe"; async function getVersionFilename(option, versionFilename) { if (option.versionPath != null) { const filename2 = await getFindFile( pathe3.join(await getOutputDirPath({ output: option.versionPath }, getCwd(process.env)), versionFilename), { cwd: getCwd(process.env) } ); return filename2; } const filename = await getFindFile(versionFilename, { cwd: getCwd(process.env) }); return filename; } async function getVersion(json, option) { if (option.versionFrom === "package.json") { const { version } = json; if (!(typeof version === "string") || version == null) { throw new Error(`Cannot found version field in package.json`); } return { version }; } if (option.versionFrom === "file") { const getVersionFile = async () => { const filename = await getVersionFilename(option, CE_DEFAULT_VALUE.VERSION_FILENAME); if (filename != null) { return filename; } const fromConfig = await getVersionFilename(option, CE_DEFAULT_VALUE.CONFIG_FILE_NAME); return fromConfig; }; const versionFilename = await getVersionFile(); if (versionFilename == null) { throw new Error(`Cannot found version file: ${CE_DEFAULT_VALUE.VERSION_FILENAME}`); } const versionBuf = await fs2.promises.readFile(versionFilename); const version = getFileVersion(versionBuf); return { version: version.trim() }; } return { version: `${dayjs().valueOf()}` }; } // src/modules/containers/container.ts import { createContainer } from "awilix"; var container = createContainer(); // src/modules/containers/keys/SymbolDataSource.ts var SymbolDataSource = Symbol("data-source"); // src/common/getMetadata.ts import dayjs2 from "dayjs"; import filenamify from "filenamify"; import readPkg from "read-pkg"; async function getMetadata(option) { const dataSource = container.resolve(SymbolDataSource); const json = await readPkg({ normalize: false }); const rawName = await getProjectName(dataSource, json, option); const name = filenamify(rawName, { replacement: "_" }); const { version } = await getVersion(json, option); return { name, title: option.title, version, createdAt: dayjs2().format(), updatedAt: dayjs2().format() }; } // src/common/getPlainRelationType.ts function getPlainRelationType(relationType) { if (relationType === "one-to-one") { return "one-to-one"; } if (relationType === "many-to-many") { return "many-to-many"; } return "one-to-many"; } // src/common/getRelationHash.ts function getRelationHash(relation) { const entities = [relation.entity, relation.inverseEntityName].sort((l, r) => l.localeCompare(r)); const plainRelationType = getPlainRelationType(relation.relationType); const baseHash = [...entities, plainRelationType].join(":"); const base64 = Buffer.from(baseHash).toString("base64"); return base64; } // src/creators/applyPretter.ts import consola from "consola"; import { isError } from "my-easy-fp"; async function applyPrettier(document, format, configPath) { try { const prettier = (await import("prettier")).default; const prettierConfig = await prettier.resolveConfig(configPath ?? "."); const formatted = await prettier.format(document, { ...prettierConfig ?? {}, parser: format === "md" ? "markdown" : format }); return formatted; } catch (caught) { const err = isError(caught, new Error("unknown error raised from prettier appling function")); consola.error(err.message); consola.error(err.stack); return document; } } // src/configs/const-enum/CE_OUTPUT_COMPONENT.ts var CE_OUTPUT_COMPONENT = { TABLE: "table", ER: "er" }; // src/modules/containers/keys/SymbolTemplateRenderer.ts var SymbolTemplateRenderer = Symbol("template-renderer"); // src/templates/cosnt-enum/CE_TEMPLATE_NAME.ts var CE_TEMPLATE_NAME = { HTML_DOCUMENT_TOC: "html-document-toc", HTML_DOCUMENT: "html-document", HTML_MERMAID: "html-mermaid", HTML_MERMAID_TOC: "html-mermaid-toc", HTML_MERMAID_DIAGRAM: "html-mermaid-diagram", HTML_STYLE: "html-style", HTML_TABLE: "html-table", IMAGE_DOCUMENT: "image-document", IMAGE_MERMAID_DIAGRAM: "image-mermaid-diagram", IMAGE_STYLE: "image-style", MARKDOWN_DOCUMENT: "markdown-document", MARKDOWN_MERMAID_DIAGRAM: "markdown-mermaid-diagram", MARKDOWN_TABLE: "markdown-table", MARKDOWN_TOC: "markdown-toc", PDF_DOCUMENT_TOC: "pdf-document-toc", PDF_DOCUMENT: "pdf-document", PDF_MERMAID_DIAGRAM: "pdf-mermaid-diagram", PDF_STYLE: "pdf-style", PDF_TABLE: "pdf-table", CONFIG_JSON: "config-json" }; // src/creators/createHtml.ts import consola2 from "consola"; import pathe4 from "pathe"; async function getTables(option, renderData, outputDir) { if (!option.components.includes(CE_OUTPUT_COMPONENT.TABLE)) { return []; } const renderer = container.resolve(SymbolTemplateRenderer); const rawTables = await renderer.evaluate(CE_TEMPLATE_NAME.HTML_DOCUMENT, renderData); const prettiedTables = await applyPrettier(rawTables, "html", option.prettierConfig); const tablesFileName = pathe4.join(outputDir, CE_DEFAULT_VALUE.HTML_INDEX_FILENAME); return [ { dirname: pathe4.resolve(outputDir), filename: pathe4.resolve(tablesFileName), content: prettiedTables } ]; } async function getDiagram(option, renderData, outputDir) { if (!option.components.includes(CE_OUTPUT_COMPONENT.ER)) { return []; } const renderer = container.resolve(SymbolTemplateRenderer); const rawDiagram = await renderer.evaluate(CE_TEMPLATE_NAME.HTML_MERMAID, renderData); const prettiedDiagram = await applyPrettier(rawDiagram, "html", option.prettierConfig); const diagramFileName = option.components.includes(CE_OUTPUT_COMPONENT.TABLE) ? pathe4.join(outputDir, CE_DEFAULT_VALUE.HTML_MERMAID_FILENAME) : pathe4.join(outputDir, CE_DEFAULT_VALUE.HTML_INDEX_FILENAME); return [ { dirname: pathe4.resolve(outputDir), filename: pathe4.resolve(diagramFileName), content: prettiedDiagram } ]; } async function createHtml(option, renderData) { const outputDir = await getOutputDirPath(option, getCwd(process.env)); consola2.info(`export component: ${option.components.join(", ")}`); const documents = (await Promise.all( option.components.map(async (component) => { if (component === CE_OUTPUT_COMPONENT.TABLE) { return getTables(option, renderData, outputDir); } if (component === CE_OUTPUT_COMPONENT.ER) { return getDiagram(option, renderData, outputDir); } return []; }) )).flat(); return documents; } // src/creators/createImageHtml.ts import { getDirname as getDirname3 } from "my-node-fp"; import { randomUUID } from "crypto"; import pathe5 from "pathe"; async function createImageHtml(option, renderData) { const renderer = container.resolve(SymbolTemplateRenderer); const rawHtml = await renderer.evaluate(CE_TEMPLATE_NAME.IMAGE_DOCUMENT, { ...renderData, option: { ...renderData.option, width: "200vw" } }); const prettiedHtml = await applyPrettier(rawHtml, "html", option.prettierConfig); const outputDirPath = option.output != null ? pathe5.resolve(option.output) : process.cwd(); await betterMkdir(outputDirPath); const tempFileName = pathe5.join(outputDirPath, `${randomUUID()}.html`); return { dirname: await getDirname3(outputDirPath), content: prettiedHtml, filename: pathe5.resolve(tempFileName) }; } // src/creators/createMarkdown.ts import pathe6 from "pathe"; async function createMarkdown(option, renderData) { const renderer = container.resolve(SymbolTemplateRenderer); const rawMarkdown = await renderer.evaluate(CE_TEMPLATE_NAME.MARKDOWN_DOCUMENT, renderData); const prettiedMarkdown = await applyPrettier(rawMarkdown, "md", option.prettierConfig); const markdownFileName = `${renderData.metadata.name}.md`; const outputDir = await getOutputDirPath(option, getCwd(process.env)); return { filename: pathe6.resolve(pathe6.join(outputDir, markdownFileName)), dirname: pathe6.resolve(outputDir), content: prettiedMarkdown }; } // src/creators/createPdfHtml.ts import { getDirname as getDirname4 } from "my-node-fp"; import { randomUUID as randomUUID2 } from "crypto"; import pathe7 from "pathe"; async function createPdfHtml(option, renderData) { const renderer = container.resolve(SymbolTemplateRenderer); const rawHtml = await renderer.evaluate(CE_TEMPLATE_NAME.PDF_DOCUMENT, renderData); const prettiedHtml = await applyPrettier(rawHtml, "html", option.prettierConfig); const outputDirPath = option.output != null ? pathe7.resolve(option.output) : process.cwd(); await betterMkdir(outputDirPath); const tempFileName = pathe7.join(outputDirPath, `${randomUUID2()}.html`); return { dirname: await getDirname4(outputDirPath), content: prettiedHtml, filename: pathe7.resolve(tempFileName) }; } // src/configs/const-enum/CE_OUTPUT_FORMAT.ts var CE_OUTPUT_FORMAT = { HTML: "html", MARKDOWN: "md", PDF: "pdf", IMAGE: "image" }; // src/databases/const-enum/CE_RECORD_KIND.ts var CE_RECORD_KIND = { COLUMN: "column", ENTITY: "entity", RELATION: "relation", INDEX: "index" }; // src/modules/getSlashEndRoutePath.ts function getSlashEndRoutePath(basePath) { if (basePath.endsWith("/")) { return basePath; } return `${basePath}/`; } // src/creators/getRenderData.ts import alasql from "alasql"; import { compareVersions } from "compare-versions"; async function getRenderData(records, metadata, option) { const versionRows = await alasql.promise("SELECT DISTINCT version FROM ?", [records]); const unSortedVersions = versionRows.map((version) => version.version); const versions = option.versionFrom === "timestamp" ? unSortedVersions.sort((l, r) => r.localeCompare(l)) : unSortedVersions.sort((l, r) => compareVersions(r, l)); const renderDatas = await Promise.all( versions.map(async (version) => { const entities = await alasql.promise(`SELECT * FROM ? WHERE [$kind] = ? AND version = ?`, [ records, "entity", version ]); const renderData = await Promise.all( entities.map(async (entity) => { const [columns, relations, indices] = await Promise.all([ await alasql.promise("SELECT * FROM ? WHERE [$kind] = ? AND entity = ? AND version = ?", [ records, CE_RECORD_KIND.COLUMN, entity.entity, version ]), await alasql.promise("SELECT * FROM ? WHERE [$kind] = ? AND entity = ? AND version = ?", [ records, CE_RECORD_KIND.RELATION, entity.entity, version ]), await alasql.promise("SELECT * FROM ? WHERE [$kind] = ? AND entity = ? AND version = ?", [ records, CE_RECORD_KIND.INDEX, entity.entity, version ]) ]); return { ...entity, columns, relations, indices }; }) ); return { version, entities: renderData, latest: version === metadata.version }; }) ); if (option.format === CE_OUTPUT_FORMAT.HTML) { return { versions: renderDatas, option: { ...option, routeBasePath: option.routeBasePath != null ? getSlashEndRoutePath(option.routeBasePath) : void 0 }, metadata }; } return { versions: renderDatas, option, metadata }; } // src/modules/getPuppeteerConfig.ts import fs3 from "fs"; import { parse as parse2 } from "jsonc-parser"; import { exists as exists3 } from "my-node-fp"; async function getPuppeteerConfig(confgFilePath) { try { if (confgFilePath == null) { return {}; } if (await exists3(confgFilePath)) { const buf = await fs3.promises.readFile(confgFilePath); const option = parse2(buf.toString()); return option; } return {}; } catch { return {}; } } // src/creators/writeToImage.ts import consola3 from "consola"; import del from "del"; import { isError as isError2 } from "my-easy-fp"; import fs4 from "fs"; import pathe8 from "pathe"; import * as puppeteer from "puppeteer"; async function writeToImage(document, option, renderData) { let localBrowser; let localPage; try { const puppeteerConfig = await getPuppeteerConfig(option.prettierConfig); const browser = await puppeteer.launch({ ...puppeteerConfig, headless: true }); const page = await browser.newPage(); const puppeteerGotoOption = { waitUntil: "domcontentloaded", timeout: 6e4 }; localBrowser = browser; localPage = page; await betterMkdir(document.filename); await fs4.promises.writeFile(document.filename, document.content); await page.setViewport({ width: option.viewportWidth ?? 1280, height: option.viewportHeight ?? 720 * 2 }); await page.goto(`file://${document.filename}`, puppeteerGotoOption); consola3.debug(`file write start: ${document.filename}`); await page.$eval( "body", (body, backgroundColor) => { body.style.background = backgroundColor; }, option.backgroundColor ?? "white" ); if (option.imageFormat === "svg") { const svg = await page.$eval("#mermaid-diagram-container", (container2) => container2.innerHTML); if (svg == null) { await del(document.filename); throw new Error("invalid image html document template"); } await fs4.promises.writeFile(pathe8.join(document.dirname, `${renderData.metadata.name}.svg`), svg); consola3.debug("file write end"); await del(document.filename); consola3.info(`Component ER diagram successfully write on ${renderData.metadata.name}.svg`); return [pathe8.join(document.dirname, `${renderData.metadata.name}.svg`)]; } const clip = await page.$eval("svg", (htmlSvgElement) => { const react = htmlSvgElement.getBoundingClientRect(); return { x: react.left, y: react.top, width: react.width, height: react.height }; }); await page.screenshot({ path: pathe8.join(document.dirname, `${renderData.metadata.name}.png`), clip, omitBackground: false }); consola3.debug("file write end"); await del(document.filename); consola3.info(`Component ER diagram successfully write on ${renderData.metadata.name}.png`); return [pathe8.join(document.dirname, `${renderData.metadata.name}.png`)]; } catch (caught) { const err = isError2(caught, new Error("unknown error raised from writeToImage")); consola3.error(err.message); consola3.error(err.stack); return false; } finally { consola3.debug("Start page, brower close"); if (localPage !== void 0 && localPage !== null) { await localPage.close(); } if (localBrowser !== void 0 && localBrowser !== null) { await localBrowser.close(); } } } // src/creators/writeToPdf.ts import consola4 from "consola"; import del2 from "del"; import fs5 from "fs"; import { isError as isError3 } from "my-easy-fp"; import pathe9 from "pathe"; import * as puppeteer2 from "puppeteer"; async function writeToPdf(document, option, renderData) { let localBrowser; let localPage; try { const puppeteerConfig = await getPuppeteerConfig(option.puppeteerConfig); const browser = await puppeteer2.launch({ ...puppeteerConfig, headless: true }); const page = await browser.newPage(); const puppeteerGotoOption = { waitUntil: "domcontentloaded", timeout: 6e4 }; localBrowser = browser; localPage = page; consola4.info("filename: ", document.filename); await page.setViewport({ width: option.viewportWidth ?? 1280, height: option.viewportHeight ?? 720 * 2 }); await fs5.promises.writeFile(document.filename, document.content); await page.goto(`file://${document.filename}`, puppeteerGotoOption); await page.pdf({ path: pathe9.join(document.dirname, `${renderData.metadata.name}.pdf`), printBackground: option.backgroundColor !== "transparent" }); await del2(document.filename); return [pathe9.join(document.dirname, `${renderData.metadata.name}.pdf`)]; } catch (caught) { const err = isError3(caught, new Error("unknown error raised from writeToPdf")); consola4.error(err.message); consola4.error(err.stack); return []; } finally { if (localPage !== void 0 && localPage !== null) { consola4.debug("Session Closed"); await localPage.close(); } if (localBrowser !== void 0 && localBrowser !== null) { await localBrowser.close(); } } } // src/databases/const-enum/CE_CHANGE_KIND.ts var CE_CHANGE_KIND = { CHANGE: "change", ADD: "add", DELETE: "delete", NONE: "none" }; // src/databases/compareDatabase.ts import { detailedDiff } from "deep-object-diff"; import { settify } from "my-easy-fp"; function compareDatabase(metadata, next, prev) { if (prev.length <= 0) { return next.map((record) => ({ ...record, change: CE_CHANGE_KIND.NONE })); } const nextMap = next.reduce((aggregation, record) => { switch (record.$kind) { case CE_RECORD_KIND.ENTITY: return { ...aggregation, [getEntityHash(record)]: record }; case CE_RECORD_KIND.COLUMN: return { ...aggregation, [getColumnHash(record)]: record }; case CE_RECORD_KIND.RELATION: return { ...aggregation, [getRelationHash(record)]: record }; case CE_RECORD_KIND.INDEX: return { ...aggregation, [getIndexHash(record)]: record }; default: return aggregation; } }, {}); const prevMap = prev.reduce((aggregation, record) => { switch (record.$kind) { case CE_RECORD_KIND.ENTITY: return { ...aggregation, [getEntityHash(record)]: record }; case CE_RECORD_KIND.COLUMN: return { ...aggregation, [getColumnHash(record)]: record }; case CE_RECORD_KIND.RELATION: return { ...aggregation, [getRelationHash(record)]: record }; case CE_RECORD_KIND.INDEX: return { ...aggregation, [getIndexHash(record)]: record }; default: return aggregation; } }, {}); const compared = settify([...Object.keys(nextMap), ...Object.keys(prevMap)]).map((key) => { const fromNext = nextMap[key]; const fromPrev = prevMap[key]; if (fromNext != null && fromPrev == null) { return { ...fromNext, change: CE_CHANGE_KIND.ADD }; } if (fromNext == null && fromPrev != null) { return { ...fromPrev, change: CE_CHANGE_KIND.DELETE, version: metadata.version }; } const forCompareNext = { ...fromNext, title: fromNext.title ?? "", change: CE_CHANGE_KIND.NONE, createdAt: "", updatedAt: "", version: "" }; const forComparePrev = { ...fromPrev, title: fromPrev.title ?? "", change: CE_CHANGE_KIND.NONE, createdAt: "", updatedAt: "", version: "" }; const diffed = detailedDiff(forCompareNext, forComparePrev); if (Object.keys(diffed.added).length <= 0 && Object.keys(diffed.updated).length <= 0 && Object.keys(diffed.deleted).length <= 0) { return { ...fromNext, change: CE_CHANGE_KIND.NONE }; } return { ...fromNext, change: CE_CHANGE_KIND.CHANGE, prev: fromPrev }; }); return compared; } // src/databases/flushDatabase.ts import fs6 from "fs"; import pathe10 from "pathe"; async function flushDatabase(option, records) { const dirname = await getOutputDirPath({ output: option.databasePath }, process.cwd()); const filename = pathe10.join(dirname, CE_DEFAULT_VALUE.DATABASE_FILENAME); if (filename == null) { throw new Error(`invalid database name: undefined`); } await fs6.promises.writeFile( pathe10.join(dirname, CE_DEFAULT_VALUE.DATABASE_FILENAME), JSON.stringify(records, void 0, 2) ); return records; } // src/databases/openDatabase.ts import { parse as parse3 } from "jsonc-parser"; import { isFalse as isFalse3 } from "my-easy-fp"; import { exists as exists4 } from "my-node-fp"; import fs7 from "fs"; import pathe11 from "pathe"; async function openDatabase(option) { const dirname = await getOutputDirPath({ output: option.databasePath }, process.cwd()); const filename = pathe11.join(dirname, CE_DEFAULT_VALUE.DATABASE_FILENAME); if (filename == null) { return []; } if (isFalse3(await exists4(filename))) { return []; } const db = parse3((await fs7.promises.readFile(filename)).toString()); return db; } // src/databases/processDatabase.ts import alasql2 from "alasql"; import { compareVersions as compareVersions2 } from "compare-versions"; import { atOrThrow } from "my-easy-fp"; async function processDatabase(metadata, db, option) { if (db.length <= 0) { return { next: [], deleted: [], prev: [] }; } const currentVersion = metadata.version; const versions = await alasql2.promise("SELECT DISTINCT version FROM ?", [db]); const sortedVersions = option.versionFrom === "timestamp" ? versions.sort((l, r) => r.version.localeCompare(l.version)) : versions.sort((l, r) => compareVersions2(r.version, l.version)); const firstVersionFromDb = atOrThrow(sortedVersions, 0).version; if (currentVersion !== firstVersionFromDb) { const latestRecords2 = await alasql2.promise("SELECT * FROM ? WHERE version = ?", [ db, firstVersionFromDb ]); return { next: db, deleted: [], prev: latestRecords2 }; } if (sortedVersions.length <= 1) { return { next: [], deleted: [], prev: [] }; } const secondVersionFromDb = atOrThrow(sortedVersions, 1).version; const partialRecords = await alasql2.promise("SELECT * FROM ? WHERE version != ?", [ db, currentVersion ]); const oldRecords = await alasql2.promise("SELECT * FROM ? WHERE version = ?", [ db, currentVersion ]); const latestRecords = await alasql2.promise("SELECT * FROM ? WHERE version = ?", [ db, secondVersionFromDb ]); return { next: partialRecords, deleted: oldRecords, prev: latestRecords }; } // src/templates/TemplateRenderer.ts import consola5 from "consola"; import { Eta } from "eta"; import { isError as isError4, orThrow } from "my-easy-fp"; var _eta, _templates, _defaultTemplates; var TemplateRenderer = class { constructor(templates, defaultTemplates) { __privateAdd(this, _eta, void 0); __privateAdd(this, _templates, void 0); __privateAdd(this, _defaultTemplates, void 0); __privateSet(this, _templates, templates); __privateSet(this, _defaultTemplates, defaultTemplates); __privateSet(this, _eta, new Eta({ views: "erdia", autoEscape: false })); __privateGet(this, _eta).resolvePath = (templatePath) => templatePath; __privateGet(this, _eta).readFile = (templatePath) => { const template = __privateGet(this, _templates).get(templatePath) ?? __privateGet(this, _defaultTemplates).get(templatePath); return orThrow(template, new Error(`cannot found template: ${templatePath}`)); }; } async evaluate(name, data) { try { const rendered = __privateGet(this, _eta).render(name, data); return rendered; } catch (caught) { const err = isError4(caught, new Error(`raise error from evaluateTemplate: ${name}`)); consola5.error(`template: ${name}`, data); consola5.error(err); throw err; } } }; _eta = new WeakMap(); _templates = new WeakMap(); _defaultTemplates = new WeakMap(); // src/typeorm/loadDataSource.ts import { isError as isError5 } from "my-easy-fp"; import { InstanceChecker } from "typeorm"; import { importOrRequireFile } from "typeorm/util/ImportUtils"; async function loadDataSource(dataSourceFilePath) { let dataSourceFileExports; try { [dataSourceFileExports] = await importOrRequireFile(dataSourceFilePath); } catch (caught) { const err = isError5(caught, new Error(`Unable to open file: "${dataSourceFilePath}".`)); throw new Error(`Unable to open file: "${dataSourceFilePath}". ${err.message}`); } if (!dataSourceFileExports || typeof dataSourceFileExports !== "object") { throw new Error(`Given data source file must contain export of a DataSource instance`); } if (InstanceChecker.isDataSource(dataSourceFileExports)) { return dataSourceFileExports; } const dataSourceExports = []; for (const fileExportKey in dataSourceFileExports) { const fileExport = dataSourceFileExports[fileExportKey]; const awaitedFileExport = await fileExport; if (InstanceChecker.isDataSource(awaitedFileExport)) { dataSourceExports.push(awaitedFileExport); } } if (dataSourceExports.length === 0) { throw new Error(`Given data source file must contain export of a DataSource instance`); } if (dataSourceExports.length > 1) { throw new Error(`Given data source file must contain only one export of DataSource instance`); } return dataSourceExports[0]; } // src/typeorm/getDataSource.ts import { isFalse as isFalse4 } from "my-easy-fp"; import { exists as exists5 } from "my-node-fp"; import pathe12 from "pathe"; async function getDataSource(options) { const dataSourcePath = pathe12.resolve(options.dataSourcePath); if (isFalse4(await exists5(dataSourcePath))) { throw new Error(`Cannot found dataSource: ${dataSourcePath}`); } const dataSource = await loadDataSource(dataSourcePath); if (dataSource == null) { throw new Error(`Cannot found dataSource in ${options.dataSourcePath}`); } return dataSource; } // src/cli/builders/buildOptionBuilder.ts function buildOptionBuilder(args) { args.option("route-base-path", { describe: "define the route base path. The route base path is used as the base path for navbar anchor when generating HTML documents", type: "string", default: void 0 }).option("title", { describe: "define what will be written in the HTML document title tag", type: "string", default: void 0 }).option("prettier-config", { describe: "define the path to the prettier configuration file", type: "string", default: void 0 }).option("puppeteer-config", { describe: "define the path to the puppeteer configuration file", type: "string" }).option("width", { describe: "define the ER diagram width. The width is defined by the HTML document css attribute width", type: "string", default: "100%" }).option("viewport-width", { describe: "define the viewport width to puppeteer. The width is defined by the HTML document css attribute width", type: "number", default: 1280 }).option("viewport-height", { describe: "define the viewport height to puppeteer. The width is defined by the HTML document css attribute height", type: "number", default: 720 * 2 }).option("image-format", { describe: "define the format to image file", type: "string", choices: ["svg", "png"], default: "svg" }).option("background-color", { describe: "define the background color to html documents. eg. transparent, red, '#F0F0F0'", type: "string", default: "white" }); return args; } // src/cli/builders/outputOptionBuilder.ts function outputOptionBuilder(args) { args.option("output", { alias: "o", describe: "define the directory to output file", type: "string" }); return args; } // src/cli/builders/commonOptionBuilder.ts function commonOptionBuilder(args) { outputOptionBuilder(args).option("config", { alias: "c", describe: "define the path to to configuration file: .erdiarc", type: "string" }).option("data-source-path", { alias: "d", describe: "define the path to TypeORM data source file", type: "string" }).option("show-logo", { describe: "define the logo display on cli interface", type: "boolean", default: false }).demandOption("data-source-path"); return args; } // src/configs/const-enum/CE_ENTITY_VERSION_FROM.ts var CE_ENTITY_VERSION_FROM = { TIMESTAMP: "timestamp", PACKAGE_JSON: "package.json", FILE: "file" }; // src/configs/const-enum/CE_MERMAID_THEME.ts var CE_MERMAID_THEME = { DEFAULT: "default", FOREST: "forest", DARK: "dark", NEUTRAL: "neutral", NULL: "null" }; // src/cli/builders/documentOptionBuilder.ts function documentOptionBuilder(args) { args.option("components", { alias: "t", describe: "define the output component to builded documents", choices: [CE_OUTPUT_COMPONENT.TABLE, CE_OUTPUT_COMPONENT.ER], type: "array", default: [CE_OUTPUT_COMPONENT.TABLE, CE_OUTPUT_COMPONENT.ER] }).option("project-name", { describe: "define whether project name will come from the `package.json` name field or database name", type: "string", choices: [CE_PROJECT_NAME_FROM.APPLICATION, CE_PROJECT_NAME_FROM.DATABASE], default: CE_PROJECT_NAME_FROM.APPLICATION }).option("database-path", { describe: "define the directory to store `erdiadb.json`", type: "string", default: void 0 }).option("template-path", { describe: "define the directory to ETA templates", type: "string" }).option("skip-image-in-html", { describe: "enabling the this option will skip attaching the ER diagram image file to the html document", type: "boolean", default: false }).option("format", { describe: "define the output format to builded documents", choices: [CE_OUTPUT_FORMAT.HTML, CE_OUTPUT_FORMAT.MARKDOWN, CE_OUTPUT_FORMAT.PDF, CE_OUTPUT_FORMAT.IMAGE], type: "string", default: CE_OUTPUT_FORMAT.HTML }).option("version-from", { describe: "define whether document version will come from the `package.json` version field or specific file, timestamp", choices: [CE_ENTITY_VERSION_FROM.PACKAGE_JSON, CE_ENTITY_VERSION_FROM.FILE, CE_ENTITY_VERSION_FROM.TIMESTAMP], type: "string", default: CE_ENTITY_VERSION_FROM.PACKAGE_JSON }).option("version-path", { describe: "If the versionFrom option set `file`, read the file from this path", type: "string", default: void 0 }).option("theme", { describe: "define the mermaid.js plugin theme. see https://mermaid-js.github.io/mermaid/#/Setup?id=theme", choices: [ CE_MERMAID_THEME.DEFAULT, CE_MERMAID_THEME.DARK, CE_MERMAID_THEME.FOREST, CE_MERMAID_THEME.DARK, CE_MERMAID_THEME.NEUTRAL, CE_MERMAID_THEME.NULL ], default: CE_MERMAID_THEME.DARK, type: "string" }); return args; } // src/modules/containers/keys/SymbolDefaultTemplate.ts var SymbolDefaultTemplate = Symbol("default-template"); // src/modules/containers/keys/SymbolLogger.ts var SymbolLogger = Symbol("symbol-logger"); // src/modules/containers/keys/SymbolTemplate.ts var SymbolTemplate = Symbol("template"); // src/modules/loggers/Logger.ts import consola6 from "consola"; var _enable; var Logger = class { constructor(enable) { __privateAdd(this, _enable, void 0); __publicField(this, "info", this.logging.bind(this, "info")); __publicField(this, "warn", this.logging.bind(this, "warn")); __publicField(this, "silent", this.logging.bind(this, "silent")); __publicField(this, "error", this.logging.bind(this, "error")); __publicField(this, "success", this.logging.bind(this, "success")); __publicField(this, "fail", this.logging.bind(this, "fail")); __publicField(this, "fatal", this.logging.bind(this, "fatal")); __publicField(this, "debug", this.logging.bind(this, "debug")); __publicField(this, "trace", this.logging.bind(this, "trace")); __publicField(this, "verbose", this.logging.bind(this, "verbose")); __publicField(this, "ready", this.logging.bind(this, "ready")); __publicField(this, "box", this.logging.bind(this, "box")); __publicField(this, "log", this.logging.bind(this, "log")); __privateSet(this, _enable, enable ?? false); } get enable() { return __privateGet(this, _enable); } set enable(value) { __privateSet(this, _enable, value); } get level() { return consola6.level; } set level(level) { consola6.level = level; } logging(level, message, ...args) { if (__privateGet(this, _enable)) { consola6[level](message, ...args); } } }; _enable = new WeakMap(); // src/modules/loggers/createLogger.ts import { asValue } from "awilix"; function createLogger(enable) { if (!container.hasRegistration(SymbolLogger)) { const logger = new Logger(enable ?? false); container.register(SymbolLogger, asValue(logger)); } } // src/templates/modules/configTemplate.ts var configTemplate = ` { // directory for output files "output": "<%= it.config.output %>", // typeorm dataSourcePath "data-source-path": "<%= it.config.dataSourceFile %>", // type of generated document "components": <%~ JSON.stringify(it.config.components) %>, // kind of document name // - db: database name from TypeORM // - app: application name from package.json "project-name": "<%= it.config.projectName %>", // custom template file path. erdia are using [ETA](https://eta.js.org/) template engine <% if (it.config.templatePath != null) { %> "template-path": "<%= it.config.templatePath %>", <% } else { %> // "template-path": "", <% } %> // erdia entity database file path <% if (it.config.databasePath != null) { %> "database-path": "<%= it.config.databasePath %>", <% } else { %> // "database-path": "", <% } %> // erdia entity database file path <% if (it.config.routeBasePath != null) { %> "route-base-path": "<%= it.config.routeBasePath %>", <% } else { %> // "route-base-path": "", <% } %> // document version using package.json version or timestamp "version-from": "<%= it.config.versionFrom %>", // If the versionFrom option set file, read the file from this path <% if (it.config.versionPath != null) { %> "version-path": "<%= it.config.versionPath %>", <% } else { %> // "version-path": "", <% } %> // output format of generated documents // - html // - md // - pdf // - image "format": "<%= it.config.format %>", // skip image file attachment in html document "skipImageInHtml": false, // mermaid.js plugin theme configuration // @url https://mermaid-js.github.io/mermaid/#/Setup?id=theme "theme": "<%= it.config.theme %>", // prettier config path // "prettier-config": "set your .prettierrc file path", // title tag content that inside of html document // "title": "set title tag content in html document", // ER diagram width, it will be set width css attribute // @format pdf, image <% if (it.config.format === 'pdf' || it.config.format === 'image') { -%> "width": "100%", <% } else { -%> // "width": "100%", <% } -%> // puppeteer viewport width // @format pdf, image <% if (it.config.format === 'pdf' || it.config.format === 'image') { -%> "viewport-width": 1280, <% } else { -%> // "viewport-width": 1280, <% } -%> // puppeteer viewport height // @format pdf, image <% if (it.config.format === 'pdf' || it.config.format === 'image') { -%> "viewport-height": 1440, <% } else { -%> // "viewport-height": 1440, <% } -%> // puppeteer config file path // @format pdf, image // "puppeteer-config-path": "set your puppeteer configuration file path", // Background color. Example: transparent, red, '#F0F0F0'. Optional. Default: white // @format pdf, image // "background-color": "#FFFFFF", // ER diagram export image file format // @format image <% if (it.config.format === 'image') { %> "image-format": "<%= it.config.imageFormat %>", <% } else { %> // "image-format": "svg", <% } %> } `; // src/templates/modules/getTemplateModulePath.ts import { exists as exists6 } from "my-node-fp"; import pathe13 from "pathe"; async function getTemplateModulePath(templatePathParam) { const currentFilePath = pathe13.resolve(__dirname); if (templatePathParam != null) { const currentWithTemplatePath = pathe13.resolve(pathe13.join(currentFilePath, templatePathParam)); if (await exists6(currentWithTemplatePath)) { return currentWithTemplatePath; } } const packageRootTemplatePath = pathe13.resolve( pathe13.join(currentFilePath, "..", "..", "..", CE_DEFAULT_VALUE.TEMPLATES_PATH) ); if (await exists6(packageRootTemplatePath)) { return packageRootTemplatePath; } const distTemplatePath = pathe13.resolve(pathe13.join(currentFilePath, "..", "..", CE_DEFAULT_VALUE.TEMPLATES_PATH)); if (await exists6(distTemplatePath)) { return distTemplatePath; } throw new Error("cannot found template directory!"); } // src/templates/modules/getTemplatePath.ts import { exists as exists7 } from "my-node-fp"; import pathe14 from "pathe"; async function getTemplatePath(templatePathParam) { if (templatePathParam != null && await exists7(pathe14.resolve(templatePathParam))) { return pathe14.resolve(templatePathParam); } return getTemplateModulePath(templatePathParam); } // src/modules/files/getGlobFiles.ts import pathe15 from "pathe"; function getGlobFiles(glob) { const filePathSet = /* @__PURE__ */ new Set(); for (const filePath of glob) { filePathSet.add(typeof filePath === "string" ? filePath : pathe15.join(filePath.path, filePath.name)); } return Array.from(filePathSet); } // src/modules/scopes/defaultExclude.ts var defaultExclude = ["node_modules/**", "flow-typed/**", "coverage/**", ".git/**"]; // src/templates/modules/getTemplate.ts import { isTrue } from "my-easy-fp"; import { basenames, exists as exists8, getDirname as getDirname5 } from "my-node-fp"; import fs8 from "fs"; import pathe16 from "pathe"; async function getTemplate(dirPath, filePath) { if (isTrue(await exists8(filePath))) { const buf = await fs8.promises.readFile(filePath); const relative = pathe16.relative(dirPath, filePath).replace(`.${pathe16.sep}`, ""); const dirname = await getDirname5(relative); const basename = basenames(relative, [".eta", ".ejs"]); return { key: pathe16.join(dirname, basename), content: buf.toString() }; } return void 0; } // src/templates/modules/getTemplates.ts import { Glob } from "glob"; import pathe17 from "pathe"; async function getTemplates(templatePath, globOptions) { const resolvedTemplatePath = pathe17.resolve(templatePath); const globs = new Glob(pathe17.join(resolvedTemplatePath, `**`, "*.eta"), { ...globOptions, absolute: true, ignore: defaultExclude, cwd: resolvedTemplatePath, windowsPathsNoEscape: true }); const templateFilePaths = getGlobFiles(globs).map((filePath) => [filePath, true]).map(([filePath, _flag]) => filePath); const loadedTemplateFiles = (await Promise.all(templateFilePaths.map((templateFilePath) => getTemplate(resolvedTemplatePath, templateFilePath)))).filter((template) => template != null); return loadedTemplateFiles; } // src/templates/modules/loadTemplates.ts import pathe18 from "pathe"; async function loadTemplates(option) { const defaultTemplatePath = await getTemplatePath(CE_DEFAULT_VALUE.TEMPLATES_PATH); const [defaultHtml, defaultMarkdown, defaultImage, defaultPdf] = await Promise.all([ getTemplates(pathe18.join(defaultTemplatePath, "html"), {}), getTemplates(pathe18.join(defaultTemplatePath, "markdown"), {}), getTemplates(pathe18.join(defaultTemplatePath, "image"), {}), getTemplates(pathe18.join(defaultTemplatePath, "pdf"), {}) ]); const defaultTemplateMap = new Map([ ["config-json", configTemplate.trim()], ...defaultHtml.map((template) => [`html-${template.key}`, template.content]), ...defaultMarkdown.map((template) => [`markdown-${template.key}`, template.content]), ...defaultImage.map((template) => [`image-${template.key}`, template.content]), ...defaultPdf.map((template) => [`pdf-${template.key}`, template.content]) ]); if (option?.templatePath == null) { return { default: defaultTemplateMap, template: defaultTemplateMap }; } const templatePath = await getTemplatePath(option.templatePath); const [templateHtml, templateMarkdown, templateImage, templatePdf] = await Promise.all([ getTemplates(pathe18.join(templatePath, "html"), {}), getTemplates(pathe18.join(templatePath, "markdown"), {}), getTemplates(pathe18.join(templatePath, "image"), {}), getTemplates(pathe18.join(templatePath, "pdf"), {}) ]); const templateMap = new Map([ ["config-json", configTemplate.trim()], ...templateHtml.map((template) => [`html-${template.key}`, template.content]), ...templateMarkdown.map((template) => [`markdown-${template.key}`, template.content]), ...templateImage.map((template) => [`image-${template.key}`, template.content]), ...templatePdf.map((template) => [`pdf-${template.key}`, template.content]) ]); return { default: defaultTemplateMap, template: templateMap }; } // src/configs/const-enum/CE_COLUMN_ATTRIBUTE.ts var CE_COLUMN_ATTRIBUTE = { PK: "PK", FK: "FK", UK: "UK" }; // src/creators/columns/getColumnWeight.ts import { bignumber } from "mathjs"; import { populate } from "my-easy-fp"; function getColumnWeight(column) { const weight = bignumber(0); const type = bignumber( populate(column.columnType.length).reduce((sum, index) => sum + column.columnType.charCodeAt(index), 0) ).mul(1e3); return weight.add(column.attributeKey.indexOf(CE_COLUMN_ATTRIBUTE.PK) >= 0 ? 2e7 : 0).add(column.attributeKey.indexOf(CE_COLUMN_ATTRIBUTE.FK) >= 0 ? 1e7 : 0).add(type).add(bignumber(122).sub(bignumber(column.name.toLowerCase().charCodeAt(0)))).add( bignumber(122).sub(bignumber(column.name.toLowerCase().charCodeAt(1))).div(100) ); } // src/typeorm/columns/getColumnAttributeKey.ts import alasql3 from "alasql"; import { atOrUndefined } from "my-easy-fp"; function getColumnAttributeKey(columnMetadata, dbName, tableDBName, indexRecords) { const indices = alasql3("SELECT * FROM ? WHERE ? = ANY (columnNames) and tableDBName = ?", [ indexRecords, dbName, tableDBName ]); const index = atOrUndefined(indices, 0); return [ columnMetadata.relationMetadata != null ? CE_COLUMN_ATTRIBUTE.FK : void 0, columnMetadata.isPrimary ? CE_COLUMN_ATTRIBUTE.PK : void 0, index?.isUnique ? CE_COLUMN_ATTRIBUTE.UK : void 0 ].filter((attribute) => attribute != null); } // src/typeorm/columns/getIsNullable.ts function getIsNullable(metadata) { if (metadata.isPrimary) { return ""; } if (metadata.isNullable === false) { return ""; } return "nullable"; } // src/typeorm/columns/getColumnType.ts import { isTrue as isTrue2 } from "my-easy-fp"; function getColumnType(columnMetadata, includeLength) { const nullable = getIsNullable(columnMetadata); if (typeof columnMetadata.type === "function") { if (isTrue2(includeLength ?? false) && columnMetadata.length !== "") { const name3 = columnMetadata.type.name.toString().toLowerCase().replace(/\s/g, "-"); const withNullable3 = nullable === "nullable" ? name3 : `*${name3}`; return `${withNullable3}(${columnMetadata.length})`; } const name2 = columnMetadata.type.name.toString().toLowerCase().replace(/\s/g, "-"); const withNullable2 = nullable === "nullable" ? name2 : `*${name2}`; return withNullable2; } if (isTrue2(includeLength ?? false) && columnMetadata.length !== "") { const name2 = columnMetadata.type.toString().replace(/\s/g, "-"); const withNullable2 = nullable === "nullable" ? name2 : `*${name2}`; return `${withNullable2}(${columnMetadata.length})`; } const name = columnMetadata.type.toString().replace(/\s/g, "-"); const withNullable = nullable === "nullable" ? name : `*${name}`; return withNullable; } // src/typeorm/columns/getComment.ts function getComment(option, comment) { if (comment == null) { return ""; } if (option.format === CE_OUTPUT_FORMAT.MARKDOWN) { return comment.replace(/\r\n/g, "\\\\r\\\\n").replace(/\n\r/g, "\\\\n\\\\r").replace(/\n/g, "\\\\n"); } if (option.format === CE_OUTPUT_FORMAT.HTML) { return comment.replace(/\r\n/g, "<br />").replace(/\n\r/g, "<br />").replace(/\n/g, "<br />"); } return comment.replace(/\r\n/g, "<br />").replace(/\n\r/g, "<br />").replace(/\n/g, "<br />"); } // src/typeorm/entities/getEntityName.ts function getEntityName(entityMeta) { if ("$kind" in entityMeta) { if (entityMeta.dbName == null || entityMeta.dbName === "") { return entityMeta.name; } return entityMeta.dbName; } const { tableName, name } = entityMeta; if (tableName == null || tableName === "") { return name; } return tableName; } // src/typeorm/columns/getColumnRecord.ts function getColumnRecord(columnMetadata, option, metadata, indices) { const entityName = getEntityName(columnMetadata.entityM