From 1e06914fe067a8040c44e97716ab779157be246e Mon Sep 17 00:00:00 2001 From: 1ilit <1ilit@proton.me> Date: Sun, 13 Apr 2025 02:02:14 +0400 Subject: [PATCH] add basic dbml editor --- .../EditorSidePanel/DBMLEditor/DBMLEditor.jsx | 37 ------- .../EditorSidePanel/DBMLEditor/index.jsx | 72 +++++++++++++ .../EditorSidePanel/DBMLEditor/styles.css | 23 ---- src/components/EditorSidePanel/SidePanel.jsx | 2 +- src/utils/dbml/fromDBML.js | 73 ------------- src/utils/dbml/toDBML.js | 102 ------------------ src/utils/exportAs/dbml.js | 5 +- src/utils/exportSQL/shared.js | 5 +- src/utils/importFrom/dbml.js | 2 +- src/utils/issues.js | 7 +- src/utils/utils.js | 3 +- 11 files changed, 90 insertions(+), 241 deletions(-) delete mode 100644 src/components/EditorSidePanel/DBMLEditor/DBMLEditor.jsx create mode 100644 src/components/EditorSidePanel/DBMLEditor/index.jsx delete mode 100644 src/components/EditorSidePanel/DBMLEditor/styles.css delete mode 100644 src/utils/dbml/fromDBML.js delete mode 100644 src/utils/dbml/toDBML.js diff --git a/src/components/EditorSidePanel/DBMLEditor/DBMLEditor.jsx b/src/components/EditorSidePanel/DBMLEditor/DBMLEditor.jsx deleted file mode 100644 index 5e3e3ff..0000000 --- a/src/components/EditorSidePanel/DBMLEditor/DBMLEditor.jsx +++ /dev/null @@ -1,37 +0,0 @@ -import { useEffect, useState } from "react"; -import { useDiagram, useEnums } from "../../../hooks"; -import { useDebounceValue } from "usehooks-ts"; -import { fromDBML } from "../../../utils/dbml/fromDBML"; -import { toDBML } from "../../../utils/dbml/toDBML"; -import CodeEditor from "../../CodeEditor"; - -export default function DBMLEditor({ setIssues }) { - const { setTables } = useDiagram(); - const [value, setValue] = useState(""); - const [debouncedValue] = useDebounceValue(value, 1000); - const diagram = useDiagram(); - const { enums } = useEnums(); - - useEffect(() => setValue(toDBML({ ...diagram, enums })), [diagram, enums]); - - useEffect(() => { - if (debouncedValue) { - try { - const { tables } = fromDBML(debouncedValue); - console.log(tables); - setTables(tables); - } catch (e) { - setIssues((prev) => ({ ...prev, dbml: e.diags.map((x) => x.message) })); - } - } - }, [debouncedValue, setTables, setIssues]); - - return ( - setValue(v)} - height="100%" - /> - ); -} diff --git a/src/components/EditorSidePanel/DBMLEditor/index.jsx b/src/components/EditorSidePanel/DBMLEditor/index.jsx new file mode 100644 index 0000000..14126ae --- /dev/null +++ b/src/components/EditorSidePanel/DBMLEditor/index.jsx @@ -0,0 +1,72 @@ +import { useEffect, useState } from "react"; +import { useDiagram, useEnums, useTransform } from "../../../hooks"; +import { useDebounceValue } from "usehooks-ts"; +import { fromDBML } from "../../../utils/importFrom/dbml"; +import { toDBML } from "../../../utils/exportAs/dbml"; +import CodeEditor from "../../CodeEditor"; + +export default function DBMLEditor({ setIssues }) { + const { tables: currentTables, setTables } = useDiagram(); + const diagram = useDiagram(); + const { enums } = useEnums(); + const { transform } = useTransform(); + const [value, setValue] = useState(() => toDBML({ ...diagram, enums })); + const [debouncedValue] = useDebounceValue(value, 2000); + + useEffect(() => { + const updateDiagram = () => { + try { + const currentDBML = toDBML({ ...diagram, enums }); + + if (debouncedValue && debouncedValue !== currentDBML) { + const { tables: newTables } = fromDBML(debouncedValue); + + const mergedTables = newTables + .map((newTable) => { + const existingTable = currentTables.find( + (t) => t.id === newTable.id || t.name === newTable.name, + ); + + return { + ...newTable, + ...(existingTable + ? { + x: existingTable.x, + y: existingTable.y, + color: existingTable.color, + id: existingTable.id, + } + : { + x: transform.pan.x, + y: transform.pan.y, + }), + }; + }) + .map((x, i) => ({ ...x, id: i })); + + setTables(mergedTables); + } + } catch (e) { + setIssues((prev) => ({ + ...prev, + dbml: e.diags?.map((x) => x.message) || [e.message], + })); + } + }; + + updateDiagram(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [debouncedValue]); + + return ( + + ); +} diff --git a/src/components/EditorSidePanel/DBMLEditor/styles.css b/src/components/EditorSidePanel/DBMLEditor/styles.css deleted file mode 100644 index 50dc37b..0000000 --- a/src/components/EditorSidePanel/DBMLEditor/styles.css +++ /dev/null @@ -1,23 +0,0 @@ -.cm-editor { - font-size: 13px; -} - -.ͼ1o { - background-color: var(--semi-color-bg-0); -} - -.ͼ1o .cm-gutters { - background-color: var(--semi-color-bg-0); -} - -.ͼ1.cm-focused { - outline: none; -} - -.ͼ16 { - background-color: #1e1e1e00; -} - -.ͼ16 .cm-gutters { - background-color: rgba(var(--semi-grey-1), 0.3); -} \ No newline at end of file diff --git a/src/components/EditorSidePanel/SidePanel.jsx b/src/components/EditorSidePanel/SidePanel.jsx index 0707e23..839dd11 100644 --- a/src/components/EditorSidePanel/SidePanel.jsx +++ b/src/components/EditorSidePanel/SidePanel.jsx @@ -21,7 +21,7 @@ import { databases } from "../../data/databases"; import EnumsTab from "./EnumsTab/EnumsTab"; import { isRtl } from "../../i18n/utils/rtl"; import i18n from "../../i18n/i18n"; -import DBMLEditor from "./DBMLEditor/DBMLEditor"; +import DBMLEditor from "./DBMLEditor"; export default function SidePanel({ width, resize, setResize }) { const { layout } = useLayout(); diff --git a/src/utils/dbml/fromDBML.js b/src/utils/dbml/fromDBML.js deleted file mode 100644 index 34eb05e..0000000 --- a/src/utils/dbml/fromDBML.js +++ /dev/null @@ -1,73 +0,0 @@ -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); - } - - console.log(table); - - 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); - } - } - - console.log(ast); - - const diagram = { tables, enums }; - - arrangeTables(diagram); - - return diagram; -} diff --git a/src/utils/dbml/toDBML.js b/src/utils/dbml/toDBML.js deleted file mode 100644 index bd38c49..0000000 --- a/src/utils/dbml/toDBML.js +++ /dev/null @@ -1,102 +0,0 @@ -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/exportAs/dbml.js b/src/utils/exportAs/dbml.js index f1329cf..ec39dd4 100644 --- a/src/utils/exportAs/dbml.js +++ b/src/utils/exportAs/dbml.js @@ -3,7 +3,10 @@ import i18n from "../../i18n/i18n"; import { parseDefault } from "../exportSQL/shared"; function columnDefault(field, database) { - if (!field.default || field.default.trim() === "") { + if ( + !field.default || + (typeof field.default === "string" && field.default.trim() === "") + ) { return ""; } diff --git a/src/utils/exportSQL/shared.js b/src/utils/exportSQL/shared.js index 3cbf21a..c2614c5 100644 --- a/src/utils/exportSQL/shared.js +++ b/src/utils/exportSQL/shared.js @@ -4,7 +4,10 @@ import { DB } from "../../data/constants"; import { dbToTypes } from "../../data/datatypes"; export function parseDefault(field, database = DB.GENERIC) { - if (!field.default || field.default.trim() == "") { + if ( + !field.default || + (typeof field.default === "string" && field.default.trim() == "") + ) { return ""; } diff --git a/src/utils/importFrom/dbml.js b/src/utils/importFrom/dbml.js index 899dfeb..31d7e36 100644 --- a/src/utils/importFrom/dbml.js +++ b/src/utils/importFrom/dbml.js @@ -27,7 +27,7 @@ export function fromDBML(src) { field.id = parsedTable.fields.length; field.name = column.name; field.type = column.type.type_name.toUpperCase(); - field.default = column.dbdefault ?? ""; + field.default = column.dbdefault?.value ?? ""; field.check = ""; field.primary = !!column.pk; field.unique = !!column.pk; diff --git a/src/utils/issues.js b/src/utils/issues.js index 6b3db61..bba5355 100644 --- a/src/utils/issues.js +++ b/src/utils/issues.js @@ -7,7 +7,12 @@ function checkDefault(field, database) { if (isFunction(field.default)) return true; - if (!field.notNull && field.default.toLowerCase() === "null") return true; + if ( + !field.notNull && + typeof field.default === "string" && + field.default.toLowerCase() === "null" + ) + return true; if (!dbToTypes[database][field.type].checkDefault) return true; diff --git a/src/utils/utils.js b/src/utils/utils.js index 553c340..1d93e7a 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -30,7 +30,8 @@ export function strHasQuotes(str) { const keywords = ["CURRENT_TIMESTAMP", "NULL"]; export function isKeyword(str) { - return keywords.includes(str.toUpperCase()); + if (typeof str === "string") return keywords.includes(str.toUpperCase()); + return false; } export function isFunction(str) {