From 81b5a73972e77356571d5f0efc9a76fd5533f5fa Mon Sep 17 00:00:00 2001 From: 1ilit <1ilit@proton.me> Date: Thu, 27 Feb 2025 02:37:44 +0400 Subject: [PATCH] Add export and import functions --- package-lock.json | 53 ++++++++++ package.json | 1 + src/components/EditorHeader/ControlPanel.jsx | 40 +++++--- src/utils/exportAs/dbml.js | 102 +++++++++++++++++++ src/utils/importFrom/dbml.js | 69 +++++++++++++ 5 files changed, 253 insertions(+), 12 deletions(-) create mode 100644 src/utils/exportAs/dbml.js create mode 100644 src/utils/importFrom/dbml.js diff --git a/package-lock.json b/package-lock.json index 09f4473..958525c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@codemirror/lang-json": "^6.0.1", "@codemirror/lang-sql": "^6.6.3", + "@dbml/core": "^3.9.7-alpha.0", "@douyinfe/semi-ui": "^2.51.3", "@lexical/react": "^0.12.5", "@uiw/codemirror-theme-github": "^4.21.25", @@ -536,6 +537,34 @@ "w3c-keyname": "^2.2.4" } }, + "node_modules/@dbml/core": { + "version": "3.9.7-alpha.0", + "resolved": "https://registry.npmjs.org/@dbml/core/-/core-3.9.7-alpha.0.tgz", + "integrity": "sha512-KGXr7p80XuoqQJumOs2+RHRBBH703gNxM0uiEvT1FF945+H4LriNK4ZgbXqe2ObmRNbwF2/TYFou+lqkh+tbUw==", + "license": "Apache-2.0", + "dependencies": { + "@dbml/parse": "^3.9.7-alpha.0", + "antlr4": "^4.13.1", + "lodash": "^4.17.15", + "parsimmon": "^1.13.0", + "pluralize": "^8.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@dbml/parse": { + "version": "3.9.7-alpha.0", + "resolved": "https://registry.npmjs.org/@dbml/parse/-/parse-3.9.7-alpha.0.tgz", + "integrity": "sha512-QT0rmbbnjn6hKbGXMhvdw62Gn8YgXjvG5a+0+9EoZFpFdl/Y8VSPlHqpHbdMas2kOpusMgpa1YRFaTMApZM7Mw==", + "license": "Apache-2.0", + "dependencies": { + "lodash": "^4.17.21" + }, + "peerDependencies": { + "lodash": "^4.17.21" + } + }, "node_modules/@dnd-kit/accessibility": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.0.tgz", @@ -2351,6 +2380,15 @@ "node": ">=4" } }, + "node_modules/antlr4": { + "version": "4.13.2", + "resolved": "https://registry.npmjs.org/antlr4/-/antlr4-4.13.2.tgz", + "integrity": "sha512-QiVbZhyy4xAZ17UPEuG3YTOt8ZaoeOR1CvEAqrEsDBsOqINslaB147i9xqljZqoyf5S+EUlGStaj+t22LT9MOg==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=16" + } + }, "node_modules/any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", @@ -5065,6 +5103,12 @@ "node": ">=6" } }, + "node_modules/parsimmon": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/parsimmon/-/parsimmon-1.18.1.tgz", + "integrity": "sha512-u7p959wLfGAhJpSDJVYXoyMCXWYwHia78HhRBWqk7AIbxdmlrfdp5wX0l3xv/iTSH5HvhN9K7o26hwwpgS5Nmw==", + "license": "MIT" + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -5139,6 +5183,15 @@ "node": ">= 6" } }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/postcss": { "version": "8.4.41", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz", diff --git a/package.json b/package.json index f1b38c5..ed5ce69 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "dependencies": { "@codemirror/lang-json": "^6.0.1", "@codemirror/lang-sql": "^6.6.3", + "@dbml/core": "^3.9.7-alpha.0", "@douyinfe/semi-ui": "^2.51.3", "@lexical/react": "^0.12.5", "@uiw/codemirror-theme-github": "^4.21.25", diff --git a/src/components/EditorHeader/ControlPanel.jsx b/src/components/EditorHeader/ControlPanel.jsx index bb39be6..c16789f 100644 --- a/src/components/EditorHeader/ControlPanel.jsx +++ b/src/components/EditorHeader/ControlPanel.jsx @@ -74,6 +74,7 @@ import { isRtl } from "../../i18n/utils/rtl"; import { jsonToDocumentation } from "../../utils/exportAs/documentation"; import { IdContext } from "../Workspace"; import { socials } from "../../data/socials"; +import { toDBML } from "../../utils/exportAs/dbml"; export default function ControlPanel({ diagramId, @@ -963,6 +964,21 @@ export default function ControlPanel({ setModal(MODAL.IMG); }, }, + { + SVG: () => { + const filter = (node) => node.tagName !== "i"; + toSvg(document.getElementById("canvas"), { filter: filter }).then( + function (dataUrl) { + setExportData((prev) => ({ + ...prev, + data: dataUrl, + extension: "svg", + })); + }, + ); + setModal(MODAL.IMG); + }, + }, { JSON: () => { setModal(MODAL.CODE); @@ -988,18 +1004,18 @@ export default function ControlPanel({ }, }, { - SVG: () => { - const filter = (node) => node.tagName !== "i"; - toSvg(document.getElementById("canvas"), { filter: filter }).then( - function (dataUrl) { - setExportData((prev) => ({ - ...prev, - data: dataUrl, - extension: "svg", - })); - }, - ); - setModal(MODAL.IMG); + DBML: () => { + setModal(MODAL.CODE); + const result = toDBML({ + tables, + relationships, + enums, + }); + setExportData((prev) => ({ + ...prev, + data: result, + extension: "dbml", + })); }, }, { diff --git a/src/utils/exportAs/dbml.js b/src/utils/exportAs/dbml.js new file mode 100644 index 0000000..bd38c49 --- /dev/null +++ b/src/utils/exportAs/dbml.js @@ -0,0 +1,102 @@ +import { Cardinality } from "../../data/constants"; +import { parseDefault } from "../exportSQL/shared"; + +function hasColumnSettings(field) { + return ( + field.primary || + field.notNull || + field.increment || + field.unique || + (field.comment && field.comment.trim() != "") || + (field.default && field.default.trim() != "") + ); +} + +function columnDefault(field, database) { + if (!field.default || field.default.trim() === "") { + return ""; + } + + return `default: ${parseDefault(field, database)}`; +} + +function columnComment(field) { + if (!field.comment || field.comment.trim() === "") { + return ""; + } + + return `note: '${field.comment}'`; +} + +function columnSettings(field, database) { + if (!hasColumnSettings(field)) { + return ""; + } + + return ` [ ${field.primary ? "pk " : ""}${ + field.increment ? "increment " : "" + }${field.notNull ? "not null " : ""}${ + field.unique ? "unique " : "" + }${columnDefault(field, database)}${columnComment(field, database)}]`; +} + +function cardinality(rel) { + switch (rel.cardinality) { + case Cardinality.ONE_TO_ONE: + return "-"; + case Cardinality.ONE_TO_MANY: + return "<"; + case Cardinality.MANY_TO_ONE: + return ">"; + } +} + +export function toDBML(diagram) { + return `${diagram.enums + .map( + (en) => + `enum ${en.name} {\n${en.values.map((v) => `\t${v}`).join("\n")}\n}\n\n`, + ) + .join("\n\n")}${diagram.tables + .map( + (table) => + `Table ${table.name} {\n${table.fields + .map( + (field) => + `\t${field.name} ${field.type.toLowerCase()}${columnSettings( + field, + diagram.database, + )}`, + ) + .join("\n")}${ + table.indices.length > 0 + ? "\n\n\tindexes {\n" + + table.indices + .map( + (index) => + `\t\t(${index.fields.join(", ")}) [ name: '${ + index.name + }'${index.unique ? " unique" : ""} ]`, + ) + .join("\n") + + "\n\t}" + : "" + }${ + table.comment && table.comment.trim() !== "" + ? `\n\n\tNote: '${table.comment}'` + : "" + }\n}`, + ) + .join("\n\n")}\n\n${diagram.relationships + .map( + (rel) => + `Ref ${rel.name} {\n\t${ + diagram.tables[rel.startTableId].name + }.${diagram.tables[rel.startTableId].fields[rel.startFieldId].name} ${cardinality( + rel, + )} ${diagram.tables[rel.endTableId].name}.${ + diagram.tables[rel.endTableId].fields[rel.endFieldId].name + } [ delete: ${rel.deleteConstraint.toLowerCase()}, on update: ${rel.updateConstraint.toLowerCase()} ]\n}`, + ) + .join("\n\n")}`; +} diff --git a/src/utils/importFrom/dbml.js b/src/utils/importFrom/dbml.js new file mode 100644 index 0000000..855bbae --- /dev/null +++ b/src/utils/importFrom/dbml.js @@ -0,0 +1,69 @@ +import { Parser } from "@dbml/core"; +import { arrangeTables } from "../arrangeTables"; + +const parser = new Parser(); + +export function fromDBML(src) { + const ast = parser.parse(src, "dbml"); + + const tables = []; + const enums = []; + + for (const schema of ast.schemas) { + for (const table of schema.tables) { + let parsedTable = {}; + parsedTable.id = tables.length; + parsedTable.name = table.name; + parsedTable.comment = table.note ?? ""; + parsedTable.color = "#175e7a"; + parsedTable.fields = []; + parsedTable.indices = []; + + for (const column of table.fields) { + const field = {}; + + field.id = parsedTable.fields.length; + field.name = column.name; + field.type = column.type.type_name.toUpperCase(); + field.default = column.dbdefault ?? ""; + field.check = ""; + field.primary = !!column.pk; + field.unique = !!column.pk; + field.notNull = !!column.not_null; + field.increment = !!column.increment; + field.comment = column.note ?? ""; + + parsedTable.fields.push(field); + } + + for (const idx of table.indexes) { + const parsedIndex = {}; + + parsedIndex.id = idx.id - 1; + parsedIndex.fields = idx.columns.map((x) => x.value); + parsedIndex.name = + idx.name ?? `${parsedTable.name}_index_${parsedIndex.id}`; + parsedIndex.unique = !!idx.unique; + + parsedTable.indices.push(parsedIndex); + } + + tables.push(parsedTable); + } + + for (const schemaEnum of schema.enums) { + const parsedEnum = {}; + + parsedEnum.name = schemaEnum.name; + parsedEnum.values = schemaEnum.values.map((x) => x.name); + + enums.push(parsedEnum); + } + } + + const diagram = { tables, enums }; + + arrangeTables(diagram); + + return diagram; +}