diff --git a/src/components/EditorHeader/SideSheet/Migration.jsx b/src/components/EditorHeader/SideSheet/Migration.jsx index a57759c..ff986a6 100644 --- a/src/components/EditorHeader/SideSheet/Migration.jsx +++ b/src/components/EditorHeader/SideSheet/Migration.jsx @@ -1,11 +1,77 @@ import { useCallback, useState } from "react"; -import { Tabs, TabPane, Modal } from "@douyinfe/semi-ui"; +import { Tabs, TabPane, Modal, Input } from "@douyinfe/semi-ui"; import { DiffEditor } from "@monaco-editor/react"; import { useEffect } from "react"; import { useTranslation } from "react-i18next"; -import { useSettings } from "../../../hooks"; +import { useDiagram, useSettings } from "../../../hooks"; import { compare, VERSION_FILENAME } from "../../../api/gists"; +import { deepDiff } from "../../../utils/diff"; +import { DateTime } from "luxon"; import CodeEditor from "../../CodeEditor"; +import { + escapeQuotes, + exportFieldComment, + parseDefault, +} from "../../../utils/exportSQL/shared"; +import { dbToTypes } from "../../../data/datatypes"; +import { DB } from "../../../data/constants"; + +const toTable = (table) => { + const inheritsClause = + Array.isArray(table.inherits) && table.inherits.length > 0 + ? `\n) INHERITS (${table.inherits.map((parent) => `"${parent}"`).join(", ")})` + : "\n)"; + + const fieldDefinitions = table.fields + .map( + (field) => + `${exportFieldComment(field.comment)}\t"${field.name}" ${field.type}${ + field.size ? `(${field.size})` : "" + }${field.isArray ? " ARRAY" : ""}${field.notNull ? " NOT NULL" : ""}${ + field.unique ? " UNIQUE" : "" + }${field.increment ? " GENERATED BY DEFAULT AS IDENTITY" : ""}${ + field.default?.trim() + ? ` DEFAULT ${parseDefault(field, DB.POSTGRES)}` + : "" + }${ + field.check && dbToTypes[DB.POSTGRES][field.type]?.hasCheck + ? ` CHECK(${field.check})` + : "" + }`, + ) + .join(",\n"); + + const primaryKeyClause = table.fields.some((f) => f.primary) + ? `,\n\tPRIMARY KEY(${table.fields + .filter((f) => f.primary) + .map((f) => `"${f.name}"`) + .join(", ")})` + : ""; + + const commentStatements = [ + table.comment?.trim() + ? `COMMENT ON TABLE "${table.name}" IS '${escapeQuotes(table.comment)}';` + : "", + ...table.fields + .map((field) => + field.comment?.trim() + ? `COMMENT ON COLUMN "${table.name}"."${field.name}" IS '${escapeQuotes(field.comment)}';` + : "", + ) + .filter(Boolean), + ].join("\n"); + + const indexStatements = table.indices + .map( + (i) => + `CREATE ${i.unique ? "UNIQUE " : ""}INDEX "${i.name}"\nON "${table.name}" (${i.fields + .map((f) => `"${f}"`) + .join(", ")});`, + ) + .join("\n"); + + return `CREATE TABLE "${table.name}" (\n${fieldDefinitions}${primaryKeyClause}${inheritsClause};\n\n${commentStatements}\n${indexStatements}`; +}; export default function Migration({ gistId, @@ -15,46 +81,76 @@ export default function Migration({ }) { const { t } = useTranslation(); const { settings } = useSettings(); + // const { tables } = useDiagram(); const [contentA, setContentA] = useState(""); const [contentB, setContentB] = useState(""); + const [filename, setFilename] = useState( + `${DateTime.now().toFormat("yyyyMMddHHmmss")}-migration`, + ); + const [migrationSQL, setMigrationSQL] = useState({ + up: "", + down: "", + }); - const getDiff = useCallback(async () => { - const acc = {}; - const { data } = await compare( - gistId, - VERSION_FILENAME, - selectedVersion, - versionToCompareTo, - ); - setContentA(JSON.stringify(JSON.parse(data.contentA), null, 2)); - setContentB(JSON.stringify(JSON.parse(data.contentB), null, 2)); + const generateMigrationSQL = (diff) => { + const keysToIgnore = ["x", "y", "id", "width", "height", "locked", "color"]; + const elementsToIgnore = ["notes", "areas"]; - deepDiff(contentA, contentB, acc); + let up = []; + let down = []; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [gistId, selectedVersion, versionToCompareTo]); + for (const [path, change] of Object.entries(diff)) { + const keys = path.split("."); - const deepDiff = (original, modified, acc, path = "") => { - for (const key of new Set([ - ...Object.keys(original), - ...Object.keys(modified), - ])) { - const newPath = path ? `${path}.${key}` : key; + const targetField = keys[keys.length - 1]; + if (keysToIgnore.includes(targetField)) continue; - if ( - typeof original[key] === "object" && - typeof modified[key] === "object" - ) { - deepDiff(original[key], modified[key], acc, newPath); - } else if (original[key] !== modified[key]) { - acc[newPath] = { - from: original[key] || null, - to: modified[key] || null, - }; + const element = keys[0]; + if (elementsToIgnore.includes(element)) continue; + + let next = 1; + if (element === "tables") { + const tableIndex = keys[next]; + next++; + if (isNaN(tableIndex)) continue; + + if (keys.length === next) { + if (!change.from) { + up.push(toTable(change.to)); + down.push(`DROP TABLE "${change.to.name}";`); + } + + if (!change.to) { + up.push(`DROP TABLE "${change.from.name}";`); + down.push(toTable(change.from)); + } + } } } + + return { up: up.join("\n"), down: down.join("\n") }; }; + const getDiff = useCallback(async () => { + try { + const diff = {}; + const { data } = await compare( + gistId, + VERSION_FILENAME, + selectedVersion, + versionToCompareTo, + ); + setContentA(JSON.stringify(JSON.parse(data.contentA), null, 2)); + setContentB(JSON.stringify(JSON.parse(data.contentB), null, 2)); + + deepDiff(JSON.parse(data.contentB), JSON.parse(data.contentA), diff); + setMigrationSQL(generateMigrationSQL(diff)); + console.log(diff); + } catch (error) { + console.error(error); + } + }, [gistId, selectedVersion, versionToCompareTo]); + useEffect(() => { if (!gistId || !selectedVersion || !versionToCompareTo) return; getDiff(); @@ -72,8 +168,21 @@ export default function Migration({ > - - + + +
{t("filename")}:
+ .zip} + onChange={(value) => setFilename(value)} + field="filename" + /> ); } diff --git a/src/utils/diff.js b/src/utils/diff.js new file mode 100644 index 0000000..86397c8 --- /dev/null +++ b/src/utils/diff.js @@ -0,0 +1,20 @@ +export const deepDiff = (original, modified, acc, path = "") => { + for (const key of new Set([ + ...Object.keys(original), + ...Object.keys(modified), + ])) { + const newPath = path ? `${path}.${key}` : key; + + if ( + typeof original[key] === "object" && + typeof modified[key] === "object" // doesnt handle removes well, searate cases for arrays and objs + ) { + deepDiff(original[key], modified[key], acc, newPath); + } else if (original[key] !== modified[key]) { + acc[newPath] = { + from: original[key] || null, + to: modified[key] || null, + }; + } + } +};