This commit is contained in:
1ilit
2025-09-30 23:08:42 +04:00
parent c364c5ef0f
commit c766d4abab
2 changed files with 170 additions and 33 deletions
@@ -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({
>
<Tabs lazyRender keepDOM={false} className="h-[26rem] -mt-3">
<TabPane tab={t("scripts")} itemKey="1">
<CodeEditor language="sql" height="9rem" filename="hello.sql" />
<CodeEditor language="sql" height="9rem" filename="hello.sql" className="mt-2" />
<CodeEditor
language="sql"
height="9rem"
filename={`${filename}.up.sql`}
value={migrationSQL.up}
options={{ readOnly: true }}
/>
<CodeEditor
language="sql"
height="9rem"
filename={`${filename}.down.sql`}
value={migrationSQL.down}
className="mt-2"
options={{ readOnly: true }}
/>
</TabPane>
<TabPane tab={t("json_diff")} itemKey="2">
<DiffEditor
@@ -86,6 +195,14 @@ export default function Migration({
/>
</TabPane>
</Tabs>
<div className="text-sm font-semibold mt-2">{t("filename")}:</div>
<Input
value={filename}
placeholder={t("filename")}
suffix={<div className="p-2">.zip</div>}
onChange={(value) => setFilename(value)}
field="filename"
/>
</Modal>
);
}
+20
View File
@@ -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,
};
}
}
};