add basic dbml editor

This commit is contained in:
1ilit 2025-04-13 02:02:14 +04:00
parent 894ab774b3
commit 1e06914fe0
11 changed files with 90 additions and 241 deletions

View File

@ -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 (
<CodeEditor
value={value}
language="dbml"
onChange={(v) => setValue(v)}
height="100%"
/>
);
}

View File

@ -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 (
<CodeEditor
value={value}
language="dbml"
onChange={setValue}
height="100%"
options={{
minimap: { enabled: false },
}}
/>
);
}

View File

@ -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);
}

View File

@ -21,7 +21,7 @@ import { databases } from "../../data/databases";
import EnumsTab from "./EnumsTab/EnumsTab"; import EnumsTab from "./EnumsTab/EnumsTab";
import { isRtl } from "../../i18n/utils/rtl"; import { isRtl } from "../../i18n/utils/rtl";
import i18n from "../../i18n/i18n"; import i18n from "../../i18n/i18n";
import DBMLEditor from "./DBMLEditor/DBMLEditor"; import DBMLEditor from "./DBMLEditor";
export default function SidePanel({ width, resize, setResize }) { export default function SidePanel({ width, resize, setResize }) {
const { layout } = useLayout(); const { layout } = useLayout();

View File

@ -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;
}

View File

@ -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")}`;
}

View File

@ -3,7 +3,10 @@ import i18n from "../../i18n/i18n";
import { parseDefault } from "../exportSQL/shared"; import { parseDefault } from "../exportSQL/shared";
function columnDefault(field, database) { function columnDefault(field, database) {
if (!field.default || field.default.trim() === "") { if (
!field.default ||
(typeof field.default === "string" && field.default.trim() === "")
) {
return ""; return "";
} }

View File

@ -4,7 +4,10 @@ import { DB } from "../../data/constants";
import { dbToTypes } from "../../data/datatypes"; import { dbToTypes } from "../../data/datatypes";
export function parseDefault(field, database = DB.GENERIC) { export function parseDefault(field, database = DB.GENERIC) {
if (!field.default || field.default.trim() == "") { if (
!field.default ||
(typeof field.default === "string" && field.default.trim() == "")
) {
return ""; return "";
} }

View File

@ -27,7 +27,7 @@ export function fromDBML(src) {
field.id = parsedTable.fields.length; field.id = parsedTable.fields.length;
field.name = column.name; field.name = column.name;
field.type = column.type.type_name.toUpperCase(); field.type = column.type.type_name.toUpperCase();
field.default = column.dbdefault ?? ""; field.default = column.dbdefault?.value ?? "";
field.check = ""; field.check = "";
field.primary = !!column.pk; field.primary = !!column.pk;
field.unique = !!column.pk; field.unique = !!column.pk;

View File

@ -7,7 +7,12 @@ function checkDefault(field, database) {
if (isFunction(field.default)) return true; 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; if (!dbToTypes[database][field.type].checkDefault) return true;

View File

@ -30,7 +30,8 @@ export function strHasQuotes(str) {
const keywords = ["CURRENT_TIMESTAMP", "NULL"]; const keywords = ["CURRENT_TIMESTAMP", "NULL"];
export function isKeyword(str) { export function isKeyword(str) {
return keywords.includes(str.toUpperCase()); if (typeof str === "string") return keywords.includes(str.toUpperCase());
return false;
} }
export function isFunction(str) { export function isFunction(str) {