From b0088a2c3a47610354dc01b292efeeb7973d69c6 Mon Sep 17 00:00:00 2001 From: Ishaan Garg <168962484+Ishaang19@users.noreply.github.com> Date: Wed, 15 Oct 2025 20:52:15 +0530 Subject: [PATCH 1/3] Issue #6 [BUG] Relationship lines overlap fix #6 --- src/components/EditorCanvas/Canvas.jsx | 2625 ++++++++++++------ src/components/EditorCanvas/Relationship.jsx | 793 ++++-- 2 files changed, 2334 insertions(+), 1084 deletions(-) diff --git a/src/components/EditorCanvas/Canvas.jsx b/src/components/EditorCanvas/Canvas.jsx index d4ceecb..778eecf 100644 --- a/src/components/EditorCanvas/Canvas.jsx +++ b/src/components/EditorCanvas/Canvas.jsx @@ -1,864 +1,1761 @@ -import { useRef, useState } from "react"; -import { - Action, - Cardinality, - Constraint, - darkBgTheme, - ObjectType, - gridSize, - gridCircleRadius, - minAreaSize, -} from "../../data/constants"; -import { Toast } from "@douyinfe/semi-ui"; -import Table from "./Table"; -import Area from "./Area"; -import Relationship from "./Relationship"; -import Note from "./Note"; -import { - useCanvas, - useSettings, - useTransform, - useDiagram, - useUndoRedo, - useSelect, - useAreas, - useNotes, - useLayout, - useSaveState, -} from "../../hooks"; -import { useTranslation } from "react-i18next"; -import { useEventListener } from "usehooks-ts"; -import { areFieldsCompatible, getTableHeight } from "../../utils/utils"; -import { getRectFromEndpoints, isInsideRect } from "../../utils/rect"; -import { State, noteWidth } from "../../data/constants"; -import { nanoid } from "nanoid"; - -export default function Canvas() { - const { t } = useTranslation(); - - const canvasRef = useRef(null); - const canvasContextValue = useCanvas(); - const { - canvas: { viewBox }, - pointer, - } = canvasContextValue; - - const { tables, updateTable, relationships, addRelationship, database } = - useDiagram(); - const { setSaveState } = useSaveState(); - const { areas, updateArea } = useAreas(); - const { notes, updateNote } = useNotes(); - const { layout } = useLayout(); - const { settings } = useSettings(); - const { setUndoStack, setRedoStack } = useUndoRedo(); - const { transform, setTransform } = useTransform(); - const { - selectedElement, - setSelectedElement, - bulkSelectedElements, - setBulkSelectedElements, - } = useSelect(); - const notDragging = { - id: -1, - type: ObjectType.NONE, - grabOffset: { x: 0, y: 0 }, - }; - const [dragging, setDragging] = useState(notDragging); - const [linking, setLinking] = useState(false); - const [linkingLine, setLinkingLine] = useState({ - startTableId: -1, - startFieldId: -1, - endTableId: -1, - endFieldId: -1, - startX: 0, - startY: 0, - endX: 0, - endY: 0, - }); - const [hoveredTable, setHoveredTable] = useState({ - tableId: null, - fieldId: null, - }); - const [panning, setPanning] = useState({ - isPanning: false, - panStart: { x: 0, y: 0 }, - cursorStart: { x: 0, y: 0 }, - }); - const [areaResize, setAreaResize] = useState({ id: -1, dir: "none" }); - const [areaInitDimensions, setAreaInitDimensions] = useState({ - x: 0, - y: 0, - width: 0, - height: 0, - }); - const [bulkSelectRect, setBulkSelectRect] = useState({ - x1: 0, - y1: 0, - x2: 0, - y2: 0, - show: false, - ctrlKey: false, - metaKey: false, - }); - // this is used to store the element that is clicked on - // at the moment, and shouldn't be a part of the state - let elementPointerDown = null; - - const isSameElement = (el1, el2) => { - return el1.id === el2.id && el1.type === el2.type; - }; - - const collectSelectedElements = () => { - const rect = getRectFromEndpoints(bulkSelectRect); - const elements = []; - const shouldAddElement = (elementRect, element) => { - // if ctrl key is pressed, only add the elements that are not already selected - // can theoretically be optimized later if the selected elements is - // a map from id to element (after the ids are made unique) - return ( - isInsideRect(elementRect, rect) && - ((!bulkSelectRect.ctrlKey && !bulkSelectRect.metaKey) || - !bulkSelectedElements.some((el) => isSameElement(el, element))) - ); - }; - - tables.forEach((table) => { - if (table.locked) return; - - const element = { - id: table.id, - type: ObjectType.TABLE, - currentCoords: { x: table.x, y: table.y }, - initialCoords: { x: table.x, y: table.y }, - }; - const tableRect = { - x: table.x, - y: table.y, - width: settings.tableWidth, - height: getTableHeight(table), - }; - if (shouldAddElement(tableRect, element)) { - elements.push(element); - } - }); - - areas.forEach((area) => { - if (area.locked) return; - - const element = { - id: area.id, - type: ObjectType.AREA, - currentCoords: { x: area.x, y: area.y }, - initialCoords: { x: area.x, y: area.y }, - }; - const areaRect = { - x: area.x, - y: area.y, - width: area.width, - height: area.height, - }; - if (shouldAddElement(areaRect, element)) { - elements.push(element); - } - }); - - notes.forEach((note) => { - if (note.locked) return; - - const element = { - id: note.id, - type: ObjectType.NOTE, - currentCoords: { x: note.x, y: note.y }, - initialCoords: { x: note.x, y: note.y }, - }; - const noteRect = { - x: note.x, - y: note.y, - width: noteWidth, - height: note.height, - }; - if (shouldAddElement(noteRect, element)) { - elements.push(element); - } - }); - - if (bulkSelectRect.ctrlKey || bulkSelectRect.metaKey) { - setBulkSelectedElements([...bulkSelectedElements, ...elements]); - } else { - setBulkSelectedElements(elements); - } - }; - - const handlePointerDownOnElement = (e, { element, type }) => { - if (selectedElement.open && !layout.sidebar) return; - - if (!e.isPrimary) return; - - if (!element.locked || !(e.ctrlKey || e.metaKey)) { - setSelectedElement((prev) => ({ - ...prev, - element: type, - id: element.id, - open: false, - })); - } - - if (element.locked) { - if (!(e.ctrlKey || e.metaKey)) { - setBulkSelectedElements([]); - } - return; - } - - setBulkSelectRect((prev) => ({ - ...prev, - show: false, - })); - - // this is the object that will be added to the bulk selected elements - // if necessary - const elementInBulk = { - id: element.id, - type, - currentCoords: { x: element.x, y: element.y }, - initialCoords: { x: element.x, y: element.y }, - }; - - const isSelected = bulkSelectedElements.some((el) => - isSameElement(el, elementInBulk), - ); - - if (e.ctrlKey || e.metaKey) { - if (isSelected) { - if (bulkSelectedElements.length > 1) { - setBulkSelectedElements( - bulkSelectedElements.filter( - (el) => !isSameElement(el, elementInBulk), - ), - ); - setSelectedElement({ - ...selectedElement, - element: ObjectType.NONE, - id: -1, - open: false, - }); - } - } else { - setBulkSelectedElements([...bulkSelectedElements, elementInBulk]); - } - setDragging(notDragging); - return; - } - - if (!isSelected) { - setBulkSelectedElements([elementInBulk]); - } - setDragging({ - id: element.id, - type, - grabOffset: { - x: pointer.spaces.diagram.x - element.x, - y: pointer.spaces.diagram.y - element.y, - }, - }); - }; - - const coordinatesAfterSnappingToGrid = ({ x, y }) => { - if (settings.snapToGrid) { - return { - x: Math.round(x / gridSize) * gridSize, - y: Math.round(y / gridSize) * gridSize, - }; - } - return { x, y }; - }; - - /** - * @param {PointerEvent} e - */ - const handlePointerMove = (e) => { - if (selectedElement.open && !layout.sidebar) return; - - if (!e.isPrimary) return; - - if (panning.isPanning) { - setTransform((prev) => ({ - ...prev, - pan: { - x: - panning.panStart.x + - (panning.cursorStart.x - pointer.spaces.screen.x) / transform.zoom, - y: - panning.panStart.y + - (panning.cursorStart.y - pointer.spaces.screen.y) / transform.zoom, - }, - })); - return; - } - - if (layout.readOnly) return; - - if (linking) { - setLinkingLine({ - ...linkingLine, - endX: pointer.spaces.diagram.x, - endY: pointer.spaces.diagram.y, - }); - return; - } - - if (isDragging()) { - const { x: mainElementFinalX, y: mainElementFinalY } = - coordinatesAfterSnappingToGrid({ - x: pointer.spaces.diagram.x - dragging.grabOffset.x, - y: pointer.spaces.diagram.y - dragging.grabOffset.y, - }); - - const { currentCoords } = bulkSelectedElements.find((el) => - isSameElement(el, dragging), - ); - - const deltaX = mainElementFinalX - currentCoords.x; - const deltaY = mainElementFinalY - currentCoords.y; - - const newBulkSelectedElements = []; - bulkSelectedElements.forEach((el) => { - const elementFinalCoords = { - x: el.currentCoords.x + deltaX, - y: el.currentCoords.y + deltaY, - }; - if (el.type === ObjectType.TABLE) { - updateTable(el.id, { ...elementFinalCoords }); - } - if (el.type === ObjectType.AREA) { - updateArea(el.id, { ...elementFinalCoords }); - } - if (el.type === ObjectType.NOTE) { - updateNote(el.id, { ...elementFinalCoords }); - } - newBulkSelectedElements.push({ - ...el, - currentCoords: elementFinalCoords, - }); - }); - - setBulkSelectedElements(newBulkSelectedElements); - return; - } - - if (areaResize.id !== -1) { - if (areaResize.dir === "none") return; - let newDims = { ...areaInitDimensions }; - setPanning((old) => ({ ...old, isPanning: false })); - const { x, y } = coordinatesAfterSnappingToGrid(pointer.spaces.diagram); - - switch (areaResize.dir) { - case "br": - newDims.width = x - areaInitDimensions.x; - newDims.height = y - areaInitDimensions.y; - break; - case "tl": - newDims.x = x; - newDims.y = y; - newDims.width = areaInitDimensions.width - (x - areaInitDimensions.x); - newDims.height = - areaInitDimensions.height - (y - areaInitDimensions.y); - break; - case "tr": - newDims.y = y; - newDims.width = x - areaInitDimensions.x; - newDims.height = - areaInitDimensions.height - (y - areaInitDimensions.y); - break; - case "bl": - newDims.x = x; - newDims.width = areaInitDimensions.width - (x - areaInitDimensions.x); - newDims.height = y - areaInitDimensions.y; - break; - } - - if (newDims.width <= minAreaSize) { - newDims.width = minAreaSize; - if (areaResize.dir === "tl" || areaResize.dir === "bl") { - newDims.x = - areaInitDimensions.x + areaInitDimensions.width - minAreaSize; - } - } - - if (newDims.height <= minAreaSize) { - newDims.height = minAreaSize; - if (areaResize.dir === "tl" || areaResize.dir === "tr") { - newDims.y = - areaInitDimensions.y + areaInitDimensions.height - minAreaSize; - } - } - - updateArea(areaResize.id, { ...newDims }); - return; - } - - if (bulkSelectRect.show) { - setBulkSelectRect((prev) => ({ - ...prev, - x2: pointer.spaces.diagram.x, - y2: pointer.spaces.diagram.y, - })); - } - }; - - /** - * @param {PointerEvent} e - */ - const handlePointerDown = (e) => { - if (!e.isPrimary) return; - - // don't pan if the sidesheet for editing a table is open - if ( - selectedElement.element === ObjectType.TABLE && - selectedElement.open && - !layout.sidebar - ) - return; - - const isMouseLeftButton = e.button === 0; - const isMouseMiddleButton = e.button === 1; - - if (isMouseLeftButton) { - setBulkSelectRect({ - x1: pointer.spaces.diagram.x, - y1: pointer.spaces.diagram.y, - x2: pointer.spaces.diagram.x, - y2: pointer.spaces.diagram.y, - show: elementPointerDown === null || !elementPointerDown.element.locked, - ctrlKey: e.ctrlKey, - metaKey: e.metaKey, - }); - if (elementPointerDown !== null) { - handlePointerDownOnElement(e, elementPointerDown); - } - pointer.setStyle("crosshair"); - } else if (isMouseMiddleButton) { - setPanning({ - isPanning: true, - panStart: transform.pan, - // Diagram space depends on the current panning. - // Use screen space to avoid circular dependencies and undefined behavior. - cursorStart: pointer.spaces.screen, - }); - pointer.setStyle("grabbing"); - } - }; - - const isDragging = () => { - return dragging.type !== ObjectType.NONE && dragging.id !== -1; - }; - - const didDrag = () => { - if (!isDragging()) return false; - // checking any element is sufficient - const { currentCoords, initialCoords } = bulkSelectedElements[0]; - return ( - currentCoords.x !== initialCoords.x || currentCoords.y !== initialCoords.y - ); - }; - - const didResize = (id) => { - return !( - areas[id].x === areaInitDimensions.x && - areas[id].y === areaInitDimensions.y && - areas[id].width === areaInitDimensions.width && - areas[id].height === areaInitDimensions.height - ); - }; - - const didPan = () => - !( - transform.pan.x === panning.panStart.x && - transform.pan.y === panning.panStart.y - ); - - /** - * @param {PointerEvent} e - */ - const handlePointerUp = (e) => { - if (selectedElement.open && !layout.sidebar) return; - - if (!e.isPrimary) return; - - if (didDrag()) { - setUndoStack((prev) => [ - ...prev, - { - action: Action.MOVE, - bulk: true, - message: t("bulk_update"), - elements: bulkSelectedElements.map((el) => ({ - id: el.id, - type: el.type, - undo: el.initialCoords, - redo: el.currentCoords, - })), - }, - ]); - setRedoStack([]); - setBulkSelectedElements((prev) => - prev.map((el) => ({ - ...el, - initialCoords: { ...el.currentCoords }, - })), - ); - } - - if (bulkSelectRect.show) { - setBulkSelectRect((prev) => ({ - ...prev, - x2: pointer.spaces.diagram.x, - y2: pointer.spaces.diagram.y, - show: false, - })); - if (!isDragging()) { - collectSelectedElements(); - } - } - setDragging(notDragging); - - if (panning.isPanning && didPan()) { - setSaveState(State.SAVING); - } - setPanning((old) => ({ ...old, isPanning: false })); - pointer.setStyle("default"); - - if (linking) handleLinking(); - setLinking(false); - - if (areaResize.id !== -1 && didResize(areaResize.id)) { - setUndoStack((prev) => [ - ...prev, - { - action: Action.EDIT, - element: ObjectType.AREA, - aid: areaResize.id, - undo: { - ...areas[areaResize.id], - x: areaInitDimensions.x, - y: areaInitDimensions.y, - width: areaInitDimensions.width, - height: areaInitDimensions.height, - }, - redo: areas[areaResize.id], - message: t("edit_area", { - areaName: areas[areaResize.id].name, - extra: "[resize]", - }), - }, - ]); - setRedoStack([]); - } - setAreaResize({ id: -1, dir: "none" }); - setAreaInitDimensions({ - x: 0, - y: 0, - width: 0, - height: 0, - }); - }; - - const handleGripField = () => { - setPanning((old) => ({ ...old, isPanning: false })); - setDragging(notDragging); - setLinking(true); - }; - - const getCardinality = (startField, endField) => { - const startIsUnique = startField.unique || startField.primary; - const endIsUnique = endField.unique || endField.primary; - - if (startIsUnique && endIsUnique) { - return Cardinality.ONE_TO_ONE; - } - - if (startIsUnique && !endIsUnique) { - return Cardinality.ONE_TO_MANY; - } - - if (!startIsUnique && endIsUnique) { - return Cardinality.MANY_TO_ONE; - } - - return Cardinality.ONE_TO_ONE; - }; - - const handleLinking = () => { - if (hoveredTable.tableId === null) return; - if (hoveredTable.fieldId === null) return; - - const { fields: startTableFields, name: startTableName } = tables.find( - (t) => t.id === linkingLine.startTableId, - ); - const startField = startTableFields.find( - (f) => f.id === linkingLine.startFieldId, - ); - const { fields: endTableFields, name: endTableName } = tables.find( - (t) => t.id === hoveredTable.tableId, - ); - const endField = endTableFields.find((f) => f.id === hoveredTable.fieldId); - - if (!areFieldsCompatible(database, startField.type, endField.type)) { - Toast.info(t("cannot_connect")); - return; - } - if ( - linkingLine.startTableId === hoveredTable.tableId && - linkingLine.startFieldId === hoveredTable.fieldId - ) - return; - - const cardinality = getCardinality(startField, endField); - - const newRelationship = { - ...linkingLine, - cardinality, - endTableId: hoveredTable.tableId, - endFieldId: hoveredTable.fieldId, - updateConstraint: Constraint.NONE, - deleteConstraint: Constraint.NONE, - name: `fk_${startTableName}_${startField.name}_${endTableName}`, - id: nanoid(), - }; - delete newRelationship.startX; - delete newRelationship.startY; - delete newRelationship.endX; - delete newRelationship.endY; - addRelationship(newRelationship); - }; - - useEventListener( - "wheel", - (e) => { - e.preventDefault(); - - if (e.ctrlKey || e.metaKey) { - // How "eager" the viewport is to - // center the cursor's coordinates - const eagernessFactor = 0.05; - setTransform((prev) => ({ - pan: { - x: - prev.pan.x - - (pointer.spaces.diagram.x - prev.pan.x) * - eagernessFactor * - Math.sign(e.deltaY), - y: - prev.pan.y - - (pointer.spaces.diagram.y - prev.pan.y) * - eagernessFactor * - Math.sign(e.deltaY), - }, - zoom: e.deltaY <= 0 ? prev.zoom * 1.05 : prev.zoom / 1.05, - })); - } else if (e.shiftKey) { - setTransform((prev) => ({ - ...prev, - pan: { - ...prev.pan, - x: prev.pan.x + e.deltaY / prev.zoom, - }, - })); - } else { - setTransform((prev) => ({ - ...prev, - pan: { - x: prev.pan.x + e.deltaX / prev.zoom, - y: prev.pan.y + e.deltaY / prev.zoom, - }, - })); - } - }, - canvasRef, - { passive: false }, - ); - - return ( -
-
- - {settings.showGrid && ( - <> - - - - - - - - )} - {areas.map((a) => ( - { - elementPointerDown = { - element: a, - type: ObjectType.AREA, - }; - }} - /> - ))} - {relationships.map((e, i) => ( - - ))} - {tables.map((table) => ( - { - elementPointerDown = { - element: table, - type: ObjectType.TABLE, - }; - }} - /> - ))} - {linking && ( - - )} - {notes.map((n) => ( - { - elementPointerDown = { - element: n, - type: ObjectType.NOTE, - }; - }} - /> - ))} - {bulkSelectRect.show && ( - - )} - - - {settings.showDebugCoordinates && ( -
-
- - - - - - - - - - - - - - - - - -
- {t("transform")} -
pan xpan yscale
{transform.pan.x.toFixed(2)}{transform.pan.y.toFixed(2)}{transform.zoom.toFixed(4)}
- - - - - - - - - - - - - - - - - - - - -
{t("viewbox")}
lefttopwidthheight
{viewBox.left.toFixed(2)}{viewBox.top.toFixed(2)}{viewBox.width.toFixed(2)}{viewBox.height.toFixed(2)}
- - - - - - - - - - - - - - - - - - - - - - - -
{t("cursor_coordinates")}
{t("coordinate_space")}xy
{t("coordinate_space_screen")}{pointer.spaces.screen.x.toFixed(2)}{pointer.spaces.screen.y.toFixed(2)}
{t("coordinate_space_diagram")}{pointer.spaces.diagram.x.toFixed(2)}{pointer.spaces.diagram.y.toFixed(2)}
-
- )} -
- ); -} +import { useRef, useState } from "react"; + +import { + +Action, + +Cardinality, + +Constraint, + +darkBgTheme, + +ObjectType, + +gridSize, + +gridCircleRadius, + +minAreaSize, + +} from "../../data/constants"; + +import { Toast } from "@douyinfe/semi-ui"; + +import Table from "./Table"; + +import Area from "./Area"; + +import Relationship from "./Relationship"; + +import Note from "./Note"; + +import { + +useCanvas, + +useSettings, + +useTransform, + +useDiagram, + +useUndoRedo, + +useSelect, + +useAreas, + +useNotes, + +useLayout, + +useSaveState, + +} from "../../hooks"; + +import { useTranslation } from "react-i18next"; + +import { useEventListener } from "usehooks-ts"; + +import { areFieldsCompatible, getTableHeight } from "../../utils/utils"; + +import { getRectFromEndpoints, isInsideRect } from "../../utils/rect"; + +import { State, noteWidth } from "../../data/constants"; + +import { nanoid } from "nanoid"; + + + +export default function Canvas() { + +const { t } = useTranslation(); + + + +const canvasRef = useRef(null); + +const canvasContextValue = useCanvas(); + +const { + +canvas: { viewBox }, + +pointer, + +} = canvasContextValue; + + + +const { tables, updateTable, relationships, addRelationship, database } = + +useDiagram(); + +const { setSaveState } = useSaveState(); + +const { areas, updateArea } = useAreas(); + +const { notes, updateNote } = useNotes(); + +const { layout } = useLayout(); + +const { settings } = useSettings(); + +const { setUndoStack, setRedoStack } = useUndoRedo(); + +const { transform, setTransform } = useTransform(); + +const { + +selectedElement, + +setSelectedElement, + +bulkSelectedElements, + +setBulkSelectedElements, + +} = useSelect(); + +const notDragging = { + +id: -1, + +type: ObjectType.NONE, + +grabOffset: { x: 0, y: 0 }, + +}; + +const [dragging, setDragging] = useState(notDragging); + +const [linking, setLinking] = useState(false); + +const [linkingLine, setLinkingLine] = useState({ + +startTableId: -1, + +startFieldId: -1, + +endTableId: -1, + +endFieldId: -1, + +startX: 0, + +startY: 0, + +endX: 0, + +endY: 0, + +}); + +const [hoveredTable, setHoveredTable] = useState({ + +tableId: null, + +fieldId: null, + +}); + +const [panning, setPanning] = useState({ + +isPanning: false, + +panStart: { x: 0, y: 0 }, + +cursorStart: { x: 0, y: 0 }, + +}); + +const [areaResize, setAreaResize] = useState({ id: -1, dir: "none" }); + +const [areaInitDimensions, setAreaInitDimensions] = useState({ + +x: 0, + +y: 0, + +width: 0, + +height: 0, + +}); + +const [bulkSelectRect, setBulkSelectRect] = useState({ + +x1: 0, + +y1: 0, + +x2: 0, + +y2: 0, + +show: false, + +ctrlKey: false, + +metaKey: false, + +}); + +// this is used to store the element that is clicked on + +// at the moment, and shouldn't be a part of the state + +let elementPointerDown = null; + + + +const isSameElement = (el1, el2) => { + +return el1.id === el2.id && el1.type === el2.type; + +}; + + + +const collectSelectedElements = () => { + +const rect = getRectFromEndpoints(bulkSelectRect); + +const elements = []; + +const shouldAddElement = (elementRect, element) => { + +// if ctrl key is pressed, only add the elements that are not already selected + +// can theoretically be optimized later if the selected elements is + +// a map from id to element (after the ids are made unique) + +return ( + +isInsideRect(elementRect, rect) && + +((!bulkSelectRect.ctrlKey && !bulkSelectRect.metaKey) || + +!bulkSelectedElements.some((el) => isSameElement(el, element))) + +); + +}; + + + +tables.forEach((table) => { + +if (table.locked) return; + + + +const element = { + +id: table.id, + +type: ObjectType.TABLE, + +currentCoords: { x: table.x, y: table.y }, + +initialCoords: { x: table.x, y: table.y }, + +}; + +const tableRect = { + +x: table.x, + +y: table.y, + +width: settings.tableWidth, + +height: getTableHeight(table), + +}; + +if (shouldAddElement(tableRect, element)) { + +elements.push(element); + +} + +}); + + + +areas.forEach((area) => { + +if (area.locked) return; + + + +const element = { + +id: area.id, + +type: ObjectType.AREA, + +currentCoords: { x: area.x, y: area.y }, + +initialCoords: { x: area.x, y: area.y }, + +}; + +const areaRect = { + +x: area.x, + +y: area.y, + +width: area.width, + +height: area.height, + +}; + +if (shouldAddElement(areaRect, element)) { + +elements.push(element); + +} + +}); + + + +notes.forEach((note) => { + +if (note.locked) return; + + + +const element = { + +id: note.id, + +type: ObjectType.NOTE, + +currentCoords: { x: note.x, y: note.y }, + +initialCoords: { x: note.x, y: note.y }, + +}; + +const noteRect = { + +x: note.x, + +y: note.y, + +width: noteWidth, + +height: note.height, + +}; + +if (shouldAddElement(noteRect, element)) { + +elements.push(element); + +} + +}); + + + +if (bulkSelectRect.ctrlKey || bulkSelectRect.metaKey) { + +setBulkSelectedElements([...bulkSelectedElements, ...elements]); + +} else { + +setBulkSelectedElements(elements); + +} + +}; + + + +const handlePointerDownOnElement = (e, { element, type }) => { + +if (selectedElement.open && !layout.sidebar) return; + + + +if (!e.isPrimary) return; + + + +if (!element.locked || !(e.ctrlKey || e.metaKey)) { + +setSelectedElement((prev) => ({ + +...prev, + +element: type, + +id: element.id, + +open: false, + +})); + +} + + + +if (element.locked) { + +if (!(e.ctrlKey || e.metaKey)) { + +setBulkSelectedElements([]); + +} + +return; + +} + + + +setBulkSelectRect((prev) => ({ + +...prev, + +show: false, + +})); + + + +// this is the object that will be added to the bulk selected elements + +// if necessary + +const elementInBulk = { + +id: element.id, + +type, + +currentCoords: { x: element.x, y: element.y }, + +initialCoords: { x: element.x, y: element.y }, + +}; + + + +const isSelected = bulkSelectedElements.some((el) => + +isSameElement(el, elementInBulk), + +); + + + +if (e.ctrlKey || e.metaKey) { + +if (isSelected) { + +if (bulkSelectedElements.length > 1) { + +setBulkSelectedElements( + +bulkSelectedElements.filter( + +(el) => !isSameElement(el, elementInBulk), + +), + +); + +setSelectedElement({ + +...selectedElement, + +element: ObjectType.NONE, + +id: -1, + +open: false, + +}); + +} + +} else { + +setBulkSelectedElements([...bulkSelectedElements, elementInBulk]); + +} + +setDragging(notDragging); + +return; + +} + + + +if (!isSelected) { + +setBulkSelectedElements([elementInBulk]); + +} + +setDragging({ + +id: element.id, + +type, + +grabOffset: { + +x: pointer.spaces.diagram.x - element.x, + +y: pointer.spaces.diagram.y - element.y, + +}, + +}); + +}; + + + +const coordinatesAfterSnappingToGrid = ({ x, y }) => { + +if (settings.snapToGrid) { + +return { + +x: Math.round(x / gridSize) * gridSize, + +y: Math.round(y / gridSize) * gridSize, + +}; + +} + +return { x, y }; + +}; + + + +/** + +* @param {PointerEvent} e + +*/ + +const handlePointerMove = (e) => { + +if (selectedElement.open && !layout.sidebar) return; + + + +if (!e.isPrimary) return; + + + +if (panning.isPanning) { + +setTransform((prev) => ({ + +...prev, + +pan: { + +x: + +panning.panStart.x + + +(panning.cursorStart.x - pointer.spaces.screen.x) / transform.zoom, + +y: + +panning.panStart.y + + +(panning.cursorStart.y - pointer.spaces.screen.y) / transform.zoom, + +}, + +})); + +return; + +} + + + +if (layout.readOnly) return; + + + +if (linking) { + +setLinkingLine({ + +...linkingLine, + +endX: pointer.spaces.diagram.x, + +endY: pointer.spaces.diagram.y, + +}); + +return; + +} + + + +if (isDragging()) { + +const { x: mainElementFinalX, y: mainElementFinalY } = + +coordinatesAfterSnappingToGrid({ + +x: pointer.spaces.diagram.x - dragging.grabOffset.x, + +y: pointer.spaces.diagram.y - dragging.grabOffset.y, + +}); + + + +const { currentCoords } = bulkSelectedElements.find((el) => + +isSameElement(el, dragging), + +); + + + +const deltaX = mainElementFinalX - currentCoords.x; + +const deltaY = mainElementFinalY - currentCoords.y; + + + +const newBulkSelectedElements = []; + +bulkSelectedElements.forEach((el) => { + +const elementFinalCoords = { + +x: el.currentCoords.x + deltaX, + +y: el.currentCoords.y + deltaY, + +}; + +if (el.type === ObjectType.TABLE) { + +updateTable(el.id, { ...elementFinalCoords }); + +} + +if (el.type === ObjectType.AREA) { + +updateArea(el.id, { ...elementFinalCoords }); + +} + +if (el.type === ObjectType.NOTE) { + +updateNote(el.id, { ...elementFinalCoords }); + +} + +newBulkSelectedElements.push({ + +...el, + +currentCoords: elementFinalCoords, + +}); + +}); + + + +setBulkSelectedElements(newBulkSelectedElements); + +return; + +} + + + +if (areaResize.id !== -1) { + +if (areaResize.dir === "none") return; + +let newDims = { ...areaInitDimensions }; + +setPanning((old) => ({ ...old, isPanning: false })); + +const { x, y } = coordinatesAfterSnappingToGrid(pointer.spaces.diagram); + + + +switch (areaResize.dir) { + +case "br": + +newDims.width = x - areaInitDimensions.x; + +newDims.height = y - areaInitDimensions.y; + +break; + +case "tl": + +newDims.x = x; + +newDims.y = y; + +newDims.width = areaInitDimensions.width - (x - areaInitDimensions.x); + +newDims.height = + +areaInitDimensions.height - (y - areaInitDimensions.y); + +break; + +case "tr": + +newDims.y = y; + +newDims.width = x - areaInitDimensions.x; + +newDims.height = + +areaInitDimensions.height - (y - areaInitDimensions.y); + +break; + +case "bl": + +newDims.x = x; + +newDims.width = areaInitDimensions.width - (x - areaInitDimensions.x); + +newDims.height = y - areaInitDimensions.y; + +break; + +} + + + +if (newDims.width <= minAreaSize) { + +newDims.width = minAreaSize; + +if (areaResize.dir === "tl" || areaResize.dir === "bl") { + +newDims.x = + +areaInitDimensions.x + areaInitDimensions.width - minAreaSize; + +} + +} + + + +if (newDims.height <= minAreaSize) { + +newDims.height = minAreaSize; + +if (areaResize.dir === "tl" || areaResize.dir === "tr") { + +newDims.y = + +areaInitDimensions.y + areaInitDimensions.height - minAreaSize; + +} + +} + + + +updateArea(areaResize.id, { ...newDims }); + +return; + +} + + + +if (bulkSelectRect.show) { + +setBulkSelectRect((prev) => ({ + +...prev, + +x2: pointer.spaces.diagram.x, + +y2: pointer.spaces.diagram.y, + +})); + +} + +}; + + + +/** + +* @param {PointerEvent} e + +*/ + +const handlePointerDown = (e) => { + +if (!e.isPrimary) return; + + + +// don't pan if the sidesheet for editing a table is open + +if ( + +selectedElement.element === ObjectType.TABLE && + +selectedElement.open && + +!layout.sidebar + +) + +return; + + + +const isMouseLeftButton = e.button === 0; + +const isMouseMiddleButton = e.button === 1; + + + +if (isMouseLeftButton) { + +setBulkSelectRect({ + +x1: pointer.spaces.diagram.x, + +y1: pointer.spaces.diagram.y, + +x2: pointer.spaces.diagram.x, + +y2: pointer.spaces.diagram.y, + +show: elementPointerDown === null || !elementPointerDown.element.locked, + +ctrlKey: e.ctrlKey, + +metaKey: e.metaKey, + +}); + +if (elementPointerDown !== null) { + +handlePointerDownOnElement(e, elementPointerDown); + +} + +pointer.setStyle("crosshair"); + +} else if (isMouseMiddleButton) { + +setPanning({ + +isPanning: true, + +panStart: transform.pan, + +// Diagram space depends on the current panning. + +// Use screen space to avoid circular dependencies and undefined behavior. + +cursorStart: pointer.spaces.screen, + +}); + +pointer.setStyle("grabbing"); + +} + +}; + + + +const isDragging = () => { + +return dragging.type !== ObjectType.NONE && dragging.id !== -1; + +}; + + + +const didDrag = () => { + +if (!isDragging()) return false; + +// checking any element is sufficient + +const { currentCoords, initialCoords } = bulkSelectedElements[0]; + +return ( + +currentCoords.x !== initialCoords.x || currentCoords.y !== initialCoords.y + +); + +}; + + + +const didResize = (id) => { + +return !( + +areas[id].x === areaInitDimensions.x && + +areas[id].y === areaInitDimensions.y && + +areas[id].width === areaInitDimensions.width && + +areas[id].height === areaInitDimensions.height + +); + +}; + + + +const didPan = () => + +!( + +transform.pan.x === panning.panStart.x && + +transform.pan.y === panning.panStart.y + +); + + + +/** + +* @param {PointerEvent} e + +*/ + +const handlePointerUp = (e) => { + +if (selectedElement.open && !layout.sidebar) return; + + + +if (!e.isPrimary) return; + + + +if (didDrag()) { + +setUndoStack((prev) => [ + +...prev, + +{ + +action: Action.MOVE, + +bulk: true, + +message: t("bulk_update"), + +elements: bulkSelectedElements.map((el) => ({ + +id: el.id, + +type: el.type, + +undo: el.initialCoords, + +redo: el.currentCoords, + +})), + +}, + +]); + +setRedoStack([]); + +setBulkSelectedElements((prev) => + +prev.map((el) => ({ + +...el, + +initialCoords: { ...el.currentCoords }, + +})), + +); + +} + + + +if (bulkSelectRect.show) { + +setBulkSelectRect((prev) => ({ + +...prev, + +x2: pointer.spaces.diagram.x, + +y2: pointer.spaces.diagram.y, + +show: false, + +})); + +if (!isDragging()) { + +collectSelectedElements(); + +} + +} + +setDragging(notDragging); + + + +if (panning.isPanning && didPan()) { + +setSaveState(State.SAVING); + +} + +setPanning((old) => ({ ...old, isPanning: false })); + +pointer.setStyle("default"); + + + +if (linking) handleLinking(); + +setLinking(false); + + + +if (areaResize.id !== -1 && didResize(areaResize.id)) { + +setUndoStack((prev) => [ + +...prev, + +{ + +action: Action.EDIT, + +element: ObjectType.AREA, + +aid: areaResize.id, + +undo: { + +...areas[areaResize.id], + +x: areaInitDimensions.x, + +y: areaInitDimensions.y, + +width: areaInitDimensions.width, + +height: areaInitDimensions.height, + +}, + +redo: areas[areaResize.id], + +message: t("edit_area", { + +areaName: areas[areaResize.id].name, + +extra: "[resize]", + +}), + +}, + +]); + +setRedoStack([]); + +} + +setAreaResize({ id: -1, dir: "none" }); + +setAreaInitDimensions({ + +x: 0, + +y: 0, + +width: 0, + +height: 0, + +}); + +}; + + + +const handleGripField = () => { + +setPanning((old) => ({ ...old, isPanning: false })); + +setDragging(notDragging); + +setLinking(true); + +}; + + + +const getCardinality = (startField, endField) => { + +const startIsUnique = startField.unique || startField.primary; + +const endIsUnique = endField.unique || endField.primary; + + + +if (startIsUnique && endIsUnique) { + +return Cardinality.ONE_TO_ONE; + +} + + + +if (startIsUnique && !endIsUnique) { + +return Cardinality.ONE_TO_MANY; + +} + + + +if (!startIsUnique && endIsUnique) { + +return Cardinality.MANY_TO_ONE; + +} + + + +return Cardinality.ONE_TO_ONE; + +}; + + + +const handleLinking = () => { + +if (hoveredTable.tableId === null) return; + +if (hoveredTable.fieldId === null) return; + + + +const { fields: startTableFields, name: startTableName } = tables.find( + +(t) => t.id === linkingLine.startTableId, + +); + +const startField = startTableFields.find( + +(f) => f.id === linkingLine.startFieldId, + +); + +const { fields: endTableFields, name: endTableName } = tables.find( + +(t) => t.id === hoveredTable.tableId, + +); + +const endField = endTableFields.find((f) => f.id === hoveredTable.fieldId); + + + +if (!areFieldsCompatible(database, startField.type, endField.type)) { + +Toast.info(t("cannot_connect")); + +return; + +} + +if ( + +linkingLine.startTableId === hoveredTable.tableId && + +linkingLine.startFieldId === hoveredTable.fieldId + +) + +return; + + + +const cardinality = getCardinality(startField, endField); + + + +const newRelationship = { + +...linkingLine, + +cardinality, + +endTableId: hoveredTable.tableId, + +endFieldId: hoveredTable.fieldId, + +updateConstraint: Constraint.NONE, + +deleteConstraint: Constraint.NONE, + +name: `fk_${startTableName}_${startField.name}_${endTableName}`, + +id: nanoid(), + +}; + +delete newRelationship.startX; + +delete newRelationship.startY; + +delete newRelationship.endX; + +delete newRelationship.endY; + +addRelationship(newRelationship); + +}; + + + +useEventListener( + +"wheel", + +(e) => { + +e.preventDefault(); + + + +if (e.ctrlKey || e.metaKey) { + +// How "eager" the viewport is to + +// center the cursor's coordinates + +const eagernessFactor = 0.05; + +setTransform((prev) => ({ + +pan: { + +x: + +prev.pan.x - + +(pointer.spaces.diagram.x - prev.pan.x) * + +eagernessFactor * + +Math.sign(e.deltaY), + +y: + +prev.pan.y - + +(pointer.spaces.diagram.y - prev.pan.y) * + +eagernessFactor * + +Math.sign(e.deltaY), + +}, + +zoom: e.deltaY <= 0 ? prev.zoom * 1.05 : prev.zoom / 1.05, + +})); + +} else if (e.shiftKey) { + +setTransform((prev) => ({ + +...prev, + +pan: { + +...prev.pan, + +x: prev.pan.x + e.deltaY / prev.zoom, + +}, + +})); + +} else { + +setTransform((prev) => ({ + +...prev, + +pan: { + +x: prev.pan.x + e.deltaX / prev.zoom, + +y: prev.pan.y + e.deltaY / prev.zoom, + +}, + +})); + +} + +}, + +canvasRef, + +{ passive: false }, + +); + + + +return ( + +
+ +
+ + + +{settings.showGrid && ( + +<> + + + + + + + + + + + + + + + +)} + +{areas.map((a) => ( + + { + +elementPointerDown = { + +element: a, + +type: ObjectType.AREA, + +}; + +}} + +/> + +))} + +{relationships.map((e, i) => { + +// Calculate relationships sharing the same start or end field + +const relatedRelationships = relationships.filter( + +(r) => + +(r.startTableId === e.startTableId && r.startFieldId === e.startFieldId) || + +(r.endTableId === e.endTableId && r.endFieldId === e.endFieldId) + +); + +const relationshipIndex = relatedRelationships.findIndex((r) => r.id === e.id); + +const totalRelated = relatedRelationships.length; + + + +return ( + + + +); + +})} + +{tables.map((table) => ( + + { + +elementPointerDown = { + +element: table, + +type: ObjectType.TABLE, + +}; + +}} + +/> + +))} + +{linking && ( + + + +)} + +{notes.map((n) => ( + + { + +elementPointerDown = { + +element: n, + +type: ObjectType.NOTE, + +}; + +}} + +/> + +))} + +{bulkSelectRect.show && ( + + + +)} + + + + + +{settings.showDebugCoordinates && ( + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +{t("transform")} + +
pan xpan yscale
{transform.pan.x.toFixed(2)}{transform.pan.y.toFixed(2)}{transform.zoom.toFixed(4)}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{t("viewbox")}
lefttopwidthheight
{viewBox.left.toFixed(2)}{viewBox.top.toFixed(2)}{viewBox.width.toFixed(2)}{viewBox.height.toFixed(2)}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{t("cursor_coordinates")}
{t("coordinate_space")}xy
{t("coordinate_space_screen")}{pointer.spaces.screen.x.toFixed(2)}{pointer.spaces.screen.y.toFixed(2)}
{t("coordinate_space_diagram")}{pointer.spaces.diagram.x.toFixed(2)}{pointer.spaces.diagram.y.toFixed(2)}
+ +
+ +)} + +
+ +); + +} + + \ No newline at end of file diff --git a/src/components/EditorCanvas/Relationship.jsx b/src/components/EditorCanvas/Relationship.jsx index 72457de..0e7b581 100644 --- a/src/components/EditorCanvas/Relationship.jsx +++ b/src/components/EditorCanvas/Relationship.jsx @@ -1,220 +1,573 @@ -import { useMemo, useRef, useState, useEffect } from "react"; -import { Cardinality, ObjectType, Tab } from "../../data/constants"; -import { calcPath } from "../../utils/calcPath"; -import { useDiagram, useSettings, useLayout, useSelect } from "../../hooks"; -import { useTranslation } from "react-i18next"; -import { SideSheet } from "@douyinfe/semi-ui"; -import RelationshipInfo from "../EditorSidePanel/RelationshipsTab/RelationshipInfo"; - -const labelFontSize = 16; - -export default function Relationship({ data }) { - const { settings } = useSettings(); - const { tables } = useDiagram(); - const { layout } = useLayout(); - const { selectedElement, setSelectedElement } = useSelect(); - const { t } = useTranslation(); - - const pathValues = useMemo(() => { - const startTable = tables.find((t) => t.id === data.startTableId); - const endTable = tables.find((t) => t.id === data.endTableId); - - if (!startTable || !endTable) return null; - - return { - startFieldIndex: startTable.fields.findIndex( - (f) => f.id === data.startFieldId, - ), - endFieldIndex: endTable.fields.findIndex((f) => f.id === data.endFieldId), - startTable: { x: startTable.x, y: startTable.y }, - endTable: { x: endTable.x, y: endTable.y }, - }; - }, [tables, data]); - - const pathRef = useRef(); - const labelRef = useRef(); - - let cardinalityStart = "1"; - let cardinalityEnd = "1"; - - switch (data.cardinality) { - // the translated values are to ensure backwards compatibility - case t(Cardinality.MANY_TO_ONE): - case Cardinality.MANY_TO_ONE: - cardinalityStart = data.manyLabel || "n"; - cardinalityEnd = "1"; - break; - case t(Cardinality.ONE_TO_MANY): - case Cardinality.ONE_TO_MANY: - cardinalityStart = "1"; - cardinalityEnd = data.manyLabel || "n"; - break; - case t(Cardinality.ONE_TO_ONE): - case Cardinality.ONE_TO_ONE: - cardinalityStart = "1"; - cardinalityEnd = "1"; - break; - default: - break; - } - - let cardinalityStartX = 0; - let cardinalityEndX = 0; - let cardinalityStartY = 0; - let cardinalityEndY = 0; - let labelX = 0; - let labelY = 0; - - let labelWidth = labelRef.current?.getBBox().width ?? 0; - let labelHeight = labelRef.current?.getBBox().height ?? 0; - - const cardinalityOffset = 28; - - if (pathRef.current) { - const pathLength = pathRef.current.getTotalLength(); - - const labelPoint = pathRef.current.getPointAtLength(pathLength / 2); - labelX = labelPoint.x - (labelWidth ?? 0) / 2; - labelY = labelPoint.y + (labelHeight ?? 0) / 2; - - const point1 = pathRef.current.getPointAtLength(cardinalityOffset); - cardinalityStartX = point1.x; - cardinalityStartY = point1.y; - const point2 = pathRef.current.getPointAtLength( - pathLength - cardinalityOffset, - ); - cardinalityEndX = point2.x; - cardinalityEndY = point2.y; - } - - const edit = () => { - if (!layout.sidebar) { - setSelectedElement((prev) => ({ - ...prev, - element: ObjectType.RELATIONSHIP, - id: data.id, - open: true, - })); - } else { - setSelectedElement((prev) => ({ - ...prev, - currentTab: Tab.RELATIONSHIPS, - element: ObjectType.RELATIONSHIP, - id: data.id, - open: true, - })); - if (selectedElement.currentTab !== Tab.RELATIONSHIPS) return; - document - .getElementById(`scroll_ref_${data.id}`) - .scrollIntoView({ behavior: "smooth" }); - } - }; - - return ( - <> - - {/* invisible wider path for better hover ux */} - - - {settings.showRelationshipLabels && ( - - {data.name} - - )} - {pathRef.current && settings.showCardinality && ( - <> - - - - )} - - { - setSelectedElement((prev) => ({ - ...prev, - open: false, - })); - }} - style={{ paddingBottom: "16px" }} - > -
- -
-
- - ); -} - -function CardinalityLabel({ x, y, text, r = 12, padding = 14 }) { - const [textWidth, setTextWidth] = useState(0); - const textRef = useRef(null); - - useEffect(() => { - if (textRef.current) { - const bbox = textRef.current.getBBox(); - setTextWidth(bbox.width); - } - }, [text]); - - return ( - - - - {text} - - - ); -} +import { useMemo, useRef, useState, useEffect } from "react"; + +import { Cardinality, ObjectType, Tab } from "../../data/constants"; + +import { calcPath } from "../../utils/calcPath"; + +import { useDiagram, useSettings, useLayout, useSelect } from "../../hooks"; + +import { useTranslation } from "react-i18next"; + +import { SideSheet } from "@douyinfe/semi-ui"; + +import RelationshipInfo from "../EditorSidePanel/RelationshipsTab/RelationshipInfo"; + + + +const labelFontSize = 16; + + + +export default function Relationship({ data, relationshipIndex = 0, totalRelated = 1 }) { + +const { settings } = useSettings(); + +const { tables } = useDiagram(); + +const { layout } = useLayout(); + +const { selectedElement, setSelectedElement } = useSelect(); + +const { t } = useTranslation(); + +const [isHovered, setIsHovered] = useState(false); + + + +const pathValues = useMemo(() => { + +const startTable = tables.find((t) => t.id === data.startTableId); + +const endTable = tables.find((t) => t.id === data.endTableId); + + + +if (!startTable || !endTable) return null; + + + +return { + +startFieldIndex: startTable.fields.findIndex( + +(f) => f.id === data.startFieldId, + +), + +endFieldIndex: endTable.fields.findIndex((f) => f.id === data.endFieldId), + +startTable: { x: startTable.x, y: startTable.y }, + +endTable: { x: endTable.x, y: endTable.y }, + +}; + +}, [tables, data]); + + + +const pathRef = useRef(); + +const labelRef = useRef(); + + + +let cardinalityStart = "1"; + +let cardinalityEnd = "1"; + + + +switch (data.cardinality) { + +// the translated values are to ensure backwards compatibility + +case t(Cardinality.MANY_TO_ONE): + +case Cardinality.MANY_TO_ONE: + +cardinalityStart = data.manyLabel || "n"; + +cardinalityEnd = "1"; + +break; + +case t(Cardinality.ONE_TO_MANY): + +case Cardinality.ONE_TO_MANY: + +cardinalityStart = "1"; + +cardinalityEnd = data.manyLabel || "n"; + +break; + +case t(Cardinality.ONE_TO_ONE): + +case Cardinality.ONE_TO_ONE: + +cardinalityStart = "1"; + +cardinalityEnd = "1"; + +break; + +default: + +break; + +} + + + +let cardinalityStartX = 0; + +let cardinalityEndX = 0; + +let cardinalityStartY = 0; + +let cardinalityEndY = 0; + +let labelX = 0; + +let labelY = 0; + + + +let labelWidth = labelRef.current?.getBBox().width ?? 0; + +let labelHeight = labelRef.current?.getBBox().height ?? 0; + + + +const cardinalityOffset = 28; + +// Calculate perpendicular offset for multiple relationships on same field + +const lateralOffset = totalRelated > 1 ? (relationshipIndex - (totalRelated - 1) / 2) * 20 : 0; + + + +if (pathRef.current) { + +const pathLength = pathRef.current.getTotalLength(); + + + +const labelPoint = pathRef.current.getPointAtLength(pathLength / 2); + +labelX = labelPoint.x - (labelWidth ?? 0) / 2; + +labelY = labelPoint.y + (labelHeight ?? 0) / 2; + + + +const point1 = pathRef.current.getPointAtLength(cardinalityOffset); + +const point2 = pathRef.current.getPointAtLength( + +pathLength - cardinalityOffset, + +); + + + +// Calculate perpendicular offset for start cardinality + +const startTangent = pathRef.current.getPointAtLength(cardinalityOffset + 1); + +const startAngle = Math.atan2(startTangent.y - point1.y, startTangent.x - point1.x); + +const startPerpAngle = startAngle + Math.PI / 2; + + + +cardinalityStartX = point1.x + Math.cos(startPerpAngle) * lateralOffset; + +cardinalityStartY = point1.y + Math.sin(startPerpAngle) * lateralOffset; + + + +// Calculate perpendicular offset for end cardinality + +const endTangent = pathRef.current.getPointAtLength(pathLength - cardinalityOffset - 1); + +const endAngle = Math.atan2(point2.y - endTangent.y, point2.x - endTangent.x); + +const endPerpAngle = endAngle + Math.PI / 2; + + + +cardinalityEndX = point2.x + Math.cos(endPerpAngle) * lateralOffset; + +cardinalityEndY = point2.y + Math.sin(endPerpAngle) * lateralOffset; + +} + + + +const edit = () => { + +if (!layout.sidebar) { + +setSelectedElement((prev) => ({ + +...prev, + +element: ObjectType.RELATIONSHIP, + +id: data.id, + +open: true, + +})); + +} else { + +setSelectedElement((prev) => ({ + +...prev, + +currentTab: Tab.RELATIONSHIPS, + +element: ObjectType.RELATIONSHIP, + +id: data.id, + +open: true, + +})); + +if (selectedElement.currentTab !== Tab.RELATIONSHIPS) return; + +document + +.getElementById(`scroll_ref_${data.id}`) + +.scrollIntoView({ behavior: "smooth" }); + +} + +}; + + + +return ( + +<> + + setIsHovered(true)} + +onMouseLeave={() => setIsHovered(false)} + +> + +{/* invisible wider path for better hover ux */} + + + + + +{settings.showRelationshipLabels && ( + + + +{data.name} + + + +)} + +{pathRef.current && settings.showCardinality && ( + +<> + + + + + + + +)} + +{/* Tooltip on hover */} + +{isHovered && ( + + + + + + + +{data.name} + + + + + +{data.cardinality} + + + + + +)} + + + + { + +setSelectedElement((prev) => ({ + +...prev, + +open: false, + +})); + +}} + +style={{ paddingBottom: "16px" }} + +> + +
+ + + +
+ +
+ + + +); + +} + + + +function CardinalityLabel({ x, y, text, r = 12, padding = 14, isHovered = false }) { + +const [textWidth, setTextWidth] = useState(0); + +const textRef = useRef(null); + + + +useEffect(() => { + +if (textRef.current) { + +const bbox = textRef.current.getBBox(); + +setTextWidth(bbox.width); + +} + +}, [text]); + + + +return ( + + + + + + + +{text} + + + + + +); + +} + + \ No newline at end of file From 5fd2ec5c03f20b7ea7a8361e61222eb734d7e922 Mon Sep 17 00:00:00 2001 From: Ishaan Garg <168962484+Ishaang19@users.noreply.github.com> Date: Wed, 15 Oct 2025 20:54:12 +0530 Subject: [PATCH 2/3] Issue #2 [BUG]: Show error for circular type references fix #2 --- src/i18n/locales/en.js | 858 +++++++++++++++++++++++++++-------------- src/utils/issues.js | 771 +++++++++++++++++++++++++----------- 2 files changed, 1110 insertions(+), 519 deletions(-) diff --git a/src/i18n/locales/en.js b/src/i18n/locales/en.js index a961c05..39eaffb 100644 --- a/src/i18n/locales/en.js +++ b/src/i18n/locales/en.js @@ -1,285 +1,573 @@ -const english = { - name: "English", - native_name: "English", - code: "en", -}; - -const en = { - translation: { - report_bug: "Report a bug", - import: "Import", - inherits: "Inherits", - merging_column_w_inherited_definition: - "Column '{{fieldName}}' in table '{{tableName}}' with inherited definition will be merged", - import_from: "Import from", - file: "File", - new: "New", - new_window: "New window", - no_saved_diagrams: "You have no saved diagrams", - open: "Open", - open_recent: "Open Recent", - save: "Save", - save_as: "Save as", - save_as_template: "Save as template", - template_saved: "Template saved!", - rename: "Rename", - delete_diagram: "Delete diagram", - are_you_sure_delete_diagram: - "Are you sure you want to delete this diagram? This operation is irreversible.", - oops_smth_went_wrong: "Oops! Something went wrong.", - import_diagram: "Import diagram", - import_from_source: "Import from SQL", - export_as: "Export as", - export_source: "Export SQL", - models: "Models", - exit: "Exit", - edit: "Edit", - undo: "Undo", - redo: "Redo", - clear: "Clear", - are_you_sure_clear: - "Are you sure you want to clear the diagram? This is irreversible.", - cut: "Cut", - copy: "Copy", - paste: "Paste", - duplicate: "Duplicate", - delete: "Delete", - copy_as_image: "Copy as image", - view: "View", - header: "Menubar", - sidebar: "Sidebar", - issues: "Issues", - presentation_mode: "Presentation mode", - strict_mode: "Strict mode", - field_details: "Field details", - reset_view: "Reset view", - show_grid: "Show grid", - snap_to_grid: "Snap to grid", - show_datatype: "Show datatype", - show_cardinality: "Show cardinality", - theme: "Theme", - light: "Light", - dark: "Dark", - zoom_in: "Zoom in", - zoom_out: "Zoom out", - fullscreen: "Fullscreen", - settings: "Settings", - show_timeline: "Show timeline", - autosave: "Autosave", - panning: "Panning", - show_debug_coordinates: "Show debug coordinates", - transform: "Transform", - viewbox: "View Box", - cursor_coordinates: "Cursor Coordinates", - coordinate_space: "Space", - coordinate_space_screen: "Screen", - coordinate_space_diagram: "Diagram", - table_width: "Table width", - language: "Language", - flush_storage: "Flush storage", - are_you_sure_flush_storage: - "Are you sure you want to flush the storage? This will irreversibly delete all your diagrams and custom templates.", - storage_flushed: "Storage flushed", - help: "Help", - shortcuts: "Shortcuts", - ask_on_discord: "Ask us on Discord", - feedback: "Feedback", - no_changes: "No changes", - loading: "Loading...", - last_saved: "Last saved", - saving: "Saving...", - failed_to_save: "Failed to save", - fit_window_reset: "Fit window / Reset", - zoom: "Zoom", - add_table: "Add table", - add_area: "Add area", - add_note: "Add note", - add_type: "Add type", - to_do: "To-do", - tables: "Tables", - relationships: "Relationships", - subject_areas: "Subject areas", - notes: "Notes", - types: "Types", - search: "Search...", - no_tables: "No tables", - no_tables_text: "Start building your diagram!", - no_relationships: "No relationships", - no_relationships_text: "Drag to connect fields and form relationships!", - no_subject_areas: "No subject areas", - no_subject_areas_text: "Add subject areas to group tables!", - no_notes: "No notes", - no_notes_text: "Use notes to record extra info", - no_types: "No types", - no_types_text: "Make your own custom data types", - no_issues: "No issues were detected.", - strict_mode_is_on_no_issues: - "Strict mode is off so no issues will be displayed.", - name: "Name", - type: "Type", - null: "Null", - not_null: "Not null", - nullable: "Nullable", - primary: "Primary", - unique: "Unique", - autoincrement: "Autoincrement", - default_value: "Default", - check: "Check expression", - this_will_appear_as_is: "*This will appear in the generated script as is.", - comment: "Comment", - add_field: "Add field", - values: "Values", - size: "Size", - precision: "Precision", - set_precision: "Set precision: 'size, digits'", - use_for_batch_input: "Use , for batch input", - indices: "Indices", - add_index: "Add index", - select_fields: "Select fields", - title: "Title", - not_set: "Not set", - foreign: "Foreign", - cardinality: "Cardinality", - on_update: "On update", - on_delete: "On delete", - swap: "Swap", - one_to_one: "One to one", - one_to_many: "One to many", - many_to_one: "Many to one", - content: "Content", - types_info: - "This feature is meant for object-relational DBMSs like PostgreSQL.\nIf used for MySQL or MariaDB a JSON type will be generated with the corresponding json validation check.\nIf used for SQLite it will be translated to a BLOB.\nIf used for MSSQL a type alias to the first field will be generated.", - table_deleted: "Table deleted", - area_deleted: "Area deleted", - note_deleted: "Note deleted", - relationship_deleted: "Relationship deleted", - type_deleted: "Type deleted", - cannot_connect: "Cannot connect, the columns have different types", - copied_to_clipboard: "Copied to clipboard", - create_new_diagram: "Create new diagram", - cancel: "Cancel", - open_diagram: "Open diagram", - rename_diagram: "Rename diagram", - export: "Export", - export_image: "Export image", - create: "Create", - confirm: "Confirm", - last_modified: "Last modified", - drag_and_drop_files: "Drag and drop the file here or click to upload.", - upload_sql_to_generate_diagrams: - "Upload an sql file to autogenerate your tables and columns.", - overwrite_existing_diagram: "Overwrite existing diagram", - only_mysql_supported: - "*For the time being loading only MySQL scripts is supported.", - blank: "Blank", - filename: "Filename", - table_w_no_name: "Declared a table with no name", - duplicate_table_by_name: "Duplicate table by the name '{{tableName}}'", - empty_field_name: "Empty field `name` in table '{{tableName}}'", - empty_field_type: "Empty field `type` in table '{{tableName}}'", - no_values_for_field: - "'{{fieldName}}' field of table '{{tableName}}' is of type `{{type}}` but no values have been specified", - default_doesnt_match_type: - "Default value for field '{{fieldName}}' in table '{{tableName}}' does not match its type", - not_null_is_null: - "'{{fieldName}}' field of table '{{tableName}}' is NOT NULL but has default NULL", - duplicate_fields: - "Duplicate table fields by name '{{fieldName}}' in table '{{tableName}}'", - duplicate_index: - "Duplicate index by name '{{indexName}}' in table '{{tableName}}'", - empty_index: "Index in table '{{tableName}}' indexes no columns", - no_primary_key: "Table '{{tableName}}' has no primary key", - type_with_no_name: "Declared a type with no name", - duplicate_types: "Duplicate types by the name '{{typeName}}'", - type_w_no_fields: "Declared an empty type '{{typeName}}' with no fields", - empty_type_field_name: "Empty field `name` in type '{{typeName}}'", - empty_type_field_type: "Empty field `type` in type '{{typeName}}'", - no_values_for_type_field: - "'{{fieldName}}' field of type '{{typeName}}' is of type `{{type}}` but no values have been specified", - duplicate_type_fields: - "Duplicate type fields by name '{{fieldName}}' in type '{{typeName}}'", - duplicate_reference: "Duplicate reference by the name '{{refName}}'", - circular_dependency: "Circular dependency involving table '{{refName}}'", - timeline: "Timeline", - priority: "Priority", - none: "None", - low: "Low", - medium: "Medium", - high: "High", - sort_by: "Sort by", - my_order: "My order", - completed: "Completed", - alphabetically: "Alphabetically", - add_task: "Add task", - details: "Details", - no_tasks: "You have no tasks yet.", - no_activity: "You have no activity yet.", - move_element: "Move {{name}} to {{coords}}", - edit_area: "{{extra}} Edit area {{areaName}}", - delete_area: "Delete area {{areaName}}", - edit_note: "{{extra}} Edit note {{noteTitle}}", - delete_note: "Delete note {{noteTitle}}", - edit_table: "{{extra}} Edit table {{tableName}}", - delete_table: "Delete table {{tableName}}", - edit_type: "{{extra}} Edit type {{typeName}}", - delete_type: "Delete type {{typeName}}", - add_relationship: "Add relationship", - edit_relationship: "{{extra}} Edit relationship {{refName}}", - delete_relationship: "Delete relationship {{refName}}", - not_found: "Not found", - pick_db: "Choose a database", - generic: "Generic", - generic_description: - "Generic diagrams can be exported to any SQL flavor but support few data types.", - enums: "Enums", - add_enum: "Add enum", - edit_enum: "{{extra}} Edit enum {{enumName}}", - delete_enum: "Delete enum", - enum_w_no_name: "Found enum with no name", - enum_w_no_values: "Found enum '{{enumName}}' with no values", - duplicate_enums: "Duplicate enums with the name '{{enumName}}'", - no_enums: "No enums", - no_enums_text: "Define enums here", - declare_array: "Declare array", - empty_index_name: "Declared an index with no name in table '{{tableName}}'", - didnt_find_diagram: "Oops! Didn't find the diagram.", - unsigned: "Unsigned", - share: "Share", - unshare: "Unshare", - copy_link: "Copy link", - readme: "README", - failed_to_load: "Failed to load. Make sure the link is correct.", - share_info: - "* Sharing this link will not create a live real-time collaboration session.", - show_relationship_labels: "Show relationship labels", - docs: "Docs", - supported_types: "Supported file types:", - bulk_update: "Bulk update", - multiselect: "Multiselect", - export_saved_data: "Export saved data", - dbml_view: "DBML view", - tab_view: "Tab view", - label: "Label", - many_side_label: "Many(n) side label", - version: "Version", - versions: "Versions", - no_saved_versions: "No saved versions", - record_version: "Record version", - commited_at: "Commited at", - read_only: "Read only", - continue: "Continue", - restore_version: "Restore version", - restore_warning: "Loading another version will overwrite any changes.", - return_to_current: "Return to diagram", - no_changes_to_record: "No changes to record", - click_to_view: "Click to view", - load_more: "Load more", - clear_cache: "Clear cache", - cache_cleared: "Cache cleared", - failed_to_record_version: "Failed to record version", - failed_to_load_diagram: "Failed to load diagram", - see_all: "See all", - }, -}; - -export { en, english }; +const english = { + +name: "English", + +native_name: "English", + +code: "en", + +}; + + + +const en = { + +translation: { + +report_bug: "Report a bug", + +import: "Import", + +inherits: "Inherits", + +merging_column_w_inherited_definition: + +"Column '{{fieldName}}' in table '{{tableName}}' with inherited definition will be merged", + +import_from: "Import from", + +file: "File", + +new: "New", + +new_window: "New window", + +no_saved_diagrams: "You have no saved diagrams", + +open: "Open", + +open_recent: "Open Recent", + +save: "Save", + +save_as: "Save as", + +save_as_template: "Save as template", + +template_saved: "Template saved!", + +rename: "Rename", + +delete_diagram: "Delete diagram", + +are_you_sure_delete_diagram: + +"Are you sure you want to delete this diagram? This operation is irreversible.", + +oops_smth_went_wrong: "Oops! Something went wrong.", + +import_diagram: "Import diagram", + +import_from_source: "Import from SQL", + +export_as: "Export as", + +export_source: "Export SQL", + +models: "Models", + +exit: "Exit", + +edit: "Edit", + +undo: "Undo", + +redo: "Redo", + +clear: "Clear", + +are_you_sure_clear: + +"Are you sure you want to clear the diagram? This is irreversible.", + +cut: "Cut", + +copy: "Copy", + +paste: "Paste", + +duplicate: "Duplicate", + +delete: "Delete", + +copy_as_image: "Copy as image", + +view: "View", + +header: "Menubar", + +sidebar: "Sidebar", + +issues: "Issues", + +presentation_mode: "Presentation mode", + +strict_mode: "Strict mode", + +field_details: "Field details", + +reset_view: "Reset view", + +show_grid: "Show grid", + +snap_to_grid: "Snap to grid", + +show_datatype: "Show datatype", + +show_cardinality: "Show cardinality", + +theme: "Theme", + +light: "Light", + +dark: "Dark", + +zoom_in: "Zoom in", + +zoom_out: "Zoom out", + +fullscreen: "Fullscreen", + +settings: "Settings", + +show_timeline: "Show timeline", + +autosave: "Autosave", + +panning: "Panning", + +show_debug_coordinates: "Show debug coordinates", + +transform: "Transform", + +viewbox: "View Box", + +cursor_coordinates: "Cursor Coordinates", + +coordinate_space: "Space", + +coordinate_space_screen: "Screen", + +coordinate_space_diagram: "Diagram", + +table_width: "Table width", + +language: "Language", + +flush_storage: "Flush storage", + +are_you_sure_flush_storage: + +"Are you sure you want to flush the storage? This will irreversibly delete all your diagrams and custom templates.", + +storage_flushed: "Storage flushed", + +help: "Help", + +shortcuts: "Shortcuts", + +ask_on_discord: "Ask us on Discord", + +feedback: "Feedback", + +no_changes: "No changes", + +loading: "Loading...", + +last_saved: "Last saved", + +saving: "Saving...", + +failed_to_save: "Failed to save", + +fit_window_reset: "Fit window / Reset", + +zoom: "Zoom", + +add_table: "Add table", + +add_area: "Add area", + +add_note: "Add note", + +add_type: "Add type", + +to_do: "To-do", + +tables: "Tables", + +relationships: "Relationships", + +subject_areas: "Subject areas", + +notes: "Notes", + +types: "Types", + +search: "Search...", + +no_tables: "No tables", + +no_tables_text: "Start building your diagram!", + +no_relationships: "No relationships", + +no_relationships_text: "Drag to connect fields and form relationships!", + +no_subject_areas: "No subject areas", + +no_subject_areas_text: "Add subject areas to group tables!", + +no_notes: "No notes", + +no_notes_text: "Use notes to record extra info", + +no_types: "No types", + +no_types_text: "Make your own custom data types", + +no_issues: "No issues were detected.", + +strict_mode_is_on_no_issues: + +"Strict mode is off so no issues will be displayed.", + +name: "Name", + +type: "Type", + +null: "Null", + +not_null: "Not null", + +nullable: "Nullable", + +primary: "Primary", + +unique: "Unique", + +autoincrement: "Autoincrement", + +default_value: "Default", + +check: "Check expression", + +this_will_appear_as_is: "*This will appear in the generated script as is.", + +comment: "Comment", + +add_field: "Add field", + +values: "Values", + +size: "Size", + +precision: "Precision", + +set_precision: "Set precision: 'size, digits'", + +use_for_batch_input: "Use , for batch input", + +indices: "Indices", + +add_index: "Add index", + +select_fields: "Select fields", + +title: "Title", + +not_set: "Not set", + +foreign: "Foreign", + +cardinality: "Cardinality", + +on_update: "On update", + +on_delete: "On delete", + +swap: "Swap", + +one_to_one: "One to one", + +one_to_many: "One to many", + +many_to_one: "Many to one", + +content: "Content", + +types_info: + +"This feature is meant for object-relational DBMSs like PostgreSQL.\nIf used for MySQL or MariaDB a JSON type will be generated with the corresponding json validation check.\nIf used for SQLite it will be translated to a BLOB.\nIf used for MSSQL a type alias to the first field will be generated.", + +table_deleted: "Table deleted", + +area_deleted: "Area deleted", + +note_deleted: "Note deleted", + +relationship_deleted: "Relationship deleted", + +type_deleted: "Type deleted", + +cannot_connect: "Cannot connect, the columns have different types", + +copied_to_clipboard: "Copied to clipboard", + +create_new_diagram: "Create new diagram", + +cancel: "Cancel", + +open_diagram: "Open diagram", + +rename_diagram: "Rename diagram", + +export: "Export", + +export_image: "Export image", + +create: "Create", + +confirm: "Confirm", + +last_modified: "Last modified", + +drag_and_drop_files: "Drag and drop the file here or click to upload.", + +upload_sql_to_generate_diagrams: + +"Upload an sql file to autogenerate your tables and columns.", + +overwrite_existing_diagram: "Overwrite existing diagram", + +only_mysql_supported: + +"*For the time being loading only MySQL scripts is supported.", + +blank: "Blank", + +filename: "Filename", + +table_w_no_name: "Declared a table with no name", + +duplicate_table_by_name: "Duplicate table by the name '{{tableName}}'", + +empty_field_name: "Empty field `name` in table '{{tableName}}'", + +empty_field_type: "Empty field `type` in table '{{tableName}}'", + +no_values_for_field: + +"'{{fieldName}}' field of table '{{tableName}}' is of type `{{type}}` but no values have been specified", + +default_doesnt_match_type: + +"Default value for field '{{fieldName}}' in table '{{tableName}}' does not match its type", + +not_null_is_null: + +"'{{fieldName}}' field of table '{{tableName}}' is NOT NULL but has default NULL", + +duplicate_fields: + +"Duplicate table fields by name '{{fieldName}}' in table '{{tableName}}'", + +duplicate_index: + +"Duplicate index by name '{{indexName}}' in table '{{tableName}}'", + +empty_index: "Index in table '{{tableName}}' indexes no columns", + +no_primary_key: "Table '{{tableName}}' has no primary key", + +type_with_no_name: "Declared a type with no name", + +duplicate_types: "Duplicate types by the name '{{typeName}}'", + +type_w_no_fields: "Declared an empty type '{{typeName}}' with no fields", + +empty_type_field_name: "Empty field `name` in type '{{typeName}}'", + +empty_type_field_type: "Empty field `type` in type '{{typeName}}'", + +no_values_for_type_field: + +"'{{fieldName}}' field of type '{{typeName}}' is of type `{{type}}` but no values have been specified", + +duplicate_type_fields: + +"Duplicate type fields by name '{{fieldName}}' in type '{{typeName}}'", + +duplicate_reference: "Duplicate reference by the name '{{refName}}'", + +circular_dependency: "Circular dependency involving table '{{refName}}'", + +circular_type_reference: "Circular type reference involving type '{{typeName}}'", + +timeline: "Timeline", + +priority: "Priority", + +none: "None", + +low: "Low", + +medium: "Medium", + +high: "High", + +sort_by: "Sort by", + +my_order: "My order", + +completed: "Completed", + +alphabetically: "Alphabetically", + +add_task: "Add task", + +details: "Details", + +no_tasks: "You have no tasks yet.", + +no_activity: "You have no activity yet.", + +move_element: "Move {{name}} to {{coords}}", + +edit_area: "{{extra}} Edit area {{areaName}}", + +delete_area: "Delete area {{areaName}}", + +edit_note: "{{extra}} Edit note {{noteTitle}}", + +delete_note: "Delete note {{noteTitle}}", + +edit_table: "{{extra}} Edit table {{tableName}}", + +delete_table: "Delete table {{tableName}}", + +edit_type: "{{extra}} Edit type {{typeName}}", + +delete_type: "Delete type {{typeName}}", + +add_relationship: "Add relationship", + +edit_relationship: "{{extra}} Edit relationship {{refName}}", + +delete_relationship: "Delete relationship {{refName}}", + +not_found: "Not found", + +pick_db: "Choose a database", + +generic: "Generic", + +generic_description: + +"Generic diagrams can be exported to any SQL flavor but support few data types.", + +enums: "Enums", + +add_enum: "Add enum", + +edit_enum: "{{extra}} Edit enum {{enumName}}", + +delete_enum: "Delete enum", + +enum_w_no_name: "Found enum with no name", + +enum_w_no_values: "Found enum '{{enumName}}' with no values", + +duplicate_enums: "Duplicate enums with the name '{{enumName}}'", + +no_enums: "No enums", + +no_enums_text: "Define enums here", + +declare_array: "Declare array", + +empty_index_name: "Declared an index with no name in table '{{tableName}}'", + +didnt_find_diagram: "Oops! Didn't find the diagram.", + +unsigned: "Unsigned", + +share: "Share", + +unshare: "Unshare", + +copy_link: "Copy link", + +readme: "README", + +failed_to_load: "Failed to load. Make sure the link is correct.", + +share_info: + +"* Sharing this link will not create a live real-time collaboration session.", + +show_relationship_labels: "Show relationship labels", + +docs: "Docs", + +supported_types: "Supported file types:", + +bulk_update: "Bulk update", + +multiselect: "Multiselect", + +export_saved_data: "Export saved data", + +dbml_view: "DBML view", + +tab_view: "Tab view", + +label: "Label", + +many_side_label: "Many(n) side label", + +version: "Version", + +versions: "Versions", + +no_saved_versions: "No saved versions", + +record_version: "Record version", + +commited_at: "Commited at", + +read_only: "Read only", + +continue: "Continue", + +restore_version: "Restore version", + +restore_warning: "Loading another version will overwrite any changes.", + +return_to_current: "Return to diagram", + +no_changes_to_record: "No changes to record", + +click_to_view: "Click to view", + +load_more: "Load more", + +clear_cache: "Clear cache", + +cache_cleared: "Cache cleared", + +failed_to_record_version: "Failed to record version", + +failed_to_load_diagram: "Failed to load diagram", + +see_all: "See all", + +}, + +}; + + + +export { en, english }; + + \ No newline at end of file diff --git a/src/utils/issues.js b/src/utils/issues.js index 0553bb4..4d33da4 100644 --- a/src/utils/issues.js +++ b/src/utils/issues.js @@ -1,234 +1,537 @@ -import { dbToTypes } from "../data/datatypes"; -import i18n from "../i18n/i18n"; -import { isFunction } from "./utils"; - -function checkDefault(field, database) { - if (field.default === "") return true; - if (isFunction(field.default)) return true; - if (!field.notNull && field.default.toLowerCase() === "null") return true; - if (!dbToTypes[database][field.type].checkDefault) return true; - - return dbToTypes[database][field.type].checkDefault(field); -} - -export function getIssues(diagram) { - const issues = []; - const duplicateTableNames = {}; - - diagram.tables.forEach((table) => { - if (table.name === "") { - issues.push(i18n.t("table_w_no_name")); - } - - if (duplicateTableNames[table.name]) { - issues.push(i18n.t("duplicate_table_by_name", { tableName: table.name })); - } else { - duplicateTableNames[table.name] = true; - } - - const duplicateFieldNames = {}; - let hasPrimaryKey = false; - - const inheritedFields = - table.inherits - ?.map((parentName) => { - const parent = diagram.tables.find((t) => t.name === parentName); - return parent ? parent.fields.map((f) => f.name) : []; - }) - .flat() || []; - - table.fields.forEach((field) => { - if (field.primary) hasPrimaryKey = true; - - if (field.name === "") { - issues.push(i18n.t("empty_field_name", { tableName: table.name })); - } - - if (field.type === "") { - issues.push(i18n.t("empty_field_type", { tableName: table.name })); - } else if (field.type === "ENUM" || field.type === "SET") { - if (!field.values || field.values.length === 0) { - issues.push( - i18n.t("no_values_for_field", { - tableName: table.name, - fieldName: field.name, - type: field.type, - }), - ); - } - } - - if (!checkDefault(field, diagram.database)) { - issues.push( - i18n.t("default_doesnt_match_type", { - tableName: table.name, - fieldName: field.name, - }), - ); - } - - if (field.notNull && field.default.toLowerCase() === "null") { - issues.push( - i18n.t("not_null_is_null", { - tableName: table.name, - fieldName: field.name, - }), - ); - } - - if (duplicateFieldNames[field.name]) { - issues.push( - i18n.t("duplicate_fields", { - tableName: table.name, - fieldName: field.name, - }), - ); - } else { - duplicateFieldNames[field.name] = true; - } - - if (inheritedFields.includes(field.name)) { - issues.push( - i18n.t("merging_column_w_inherited_definition", { - fieldName: field.name, - tableName: table.name, - }), - ); - } - }); - - const duplicateIndices = {}; - table.indices.forEach((index) => { - if (duplicateIndices[index.name]) { - issues.push( - i18n.t("duplicate_index", { - tableName: table.name, - indexName: index.name, - }), - ); - } else { - duplicateIndices[index.name] = true; - } - }); - - table.indices.forEach((index) => { - if (index.name.trim() === "") { - issues.push(i18n.t("empty_index_name", { tableName: table.name })); - } - if (index.fields.length === 0) { - issues.push(i18n.t("empty_index", { tableName: table.name })); - } - }); - - if (!hasPrimaryKey) { - issues.push(i18n.t("no_primary_key", { tableName: table.name })); - } - }); - - const duplicateTypeNames = {}; - diagram.types.forEach((type) => { - if (type.name === "") { - issues.push(i18n.t("type_with_no_name")); - } - - if (duplicateTypeNames[type.name]) { - issues.push(i18n.t("duplicate_types", { typeName: type.name })); - } else { - duplicateTypeNames[type.name] = true; - } - - if (type.fields.length === 0) { - issues.push(i18n.t("type_w_no_fields", { typeName: type.name })); - return; - } - - const duplicateFieldNames = {}; - type.fields.forEach((field) => { - if (field.name === "") { - issues.push(i18n.t("empty_type_field_name", { typeName: type.name })); - } - - if (field.type === "") { - issues.push(i18n.t("empty_type_field_type", { typeName: type.name })); - } else if (field.type === "ENUM" || field.type === "SET") { - if (!field.values || field.values.length === 0) { - issues.push( - i18n.t("no_values_for_type_field", { - typeName: type.name, - fieldName: field.name, - type: field.type, - }), - ); - } - } - - if (duplicateFieldNames[field.name]) { - issues.push( - i18n.t("duplicate_type_fields", { - typeName: type.name, - fieldName: field.name, - }), - ); - } else { - duplicateFieldNames[field.name] = true; - } - }); - }); - - const duplicateEnumNames = {}; - diagram.enums.forEach((e) => { - if (e.name === "") { - issues.push(i18n.t("enum_w_no_name")); - } - - if (duplicateEnumNames[e.name]) { - issues.push(i18n.t("duplicate_enums", { enumName: e.name })); - } else { - duplicateEnumNames[e.name] = true; - } - - if (e.values.length === 0) { - issues.push(i18n.t("enum_w_no_values", { enumName: e.name })); - return; - } - }); - - const duplicateFKName = {}; - diagram.relationships.forEach((r) => { - if (duplicateFKName[r.name]) { - issues.push(i18n.t("duplicate_reference", { refName: r.name })); - } else { - duplicateFKName[r.name] = true; - } - }); - - const visitedTables = new Set(); - - function checkCircularRelationships(tableId, visited = []) { - if (visited.includes(tableId)) { - issues.push( - i18n.t("circular_dependency", { - refName: diagram.tables.find((t) => t.id === tableId)?.name, - }), - ); - return; - } - - visited.push(tableId); - visitedTables.add(tableId); - - diagram.relationships.forEach((r) => { - if (r.startTableId === tableId && r.startTableId !== r.endTableId) { - checkCircularRelationships(r.endTableId, [...visited]); - } - }); - } - - diagram.tables.forEach((table) => { - if (!visitedTables.has(table.id)) { - checkCircularRelationships(table.id); - } - }); - - return issues; -} +import { dbToTypes } from "../data/datatypes"; + +import i18n from "../i18n/i18n"; + +import { isFunction } from "./utils"; + + + +function checkDefault(field, database) { + +if (field.default === "") return true; + +if (isFunction(field.default)) return true; + +if (!field.notNull && field.default.toLowerCase() === "null") return true; + +if (!dbToTypes[database][field.type].checkDefault) return true; + + + +return dbToTypes[database][field.type].checkDefault(field); + +} + + + +export function getIssues(diagram) { + +const issues = []; + +const duplicateTableNames = {}; + + + +diagram.tables.forEach((table) => { + +if (table.name === "") { + +issues.push(i18n.t("table_w_no_name")); + +} + + + +if (duplicateTableNames[table.name]) { + +issues.push(i18n.t("duplicate_table_by_name", { tableName: table.name })); + +} else { + +duplicateTableNames[table.name] = true; + +} + + + +const duplicateFieldNames = {}; + +let hasPrimaryKey = false; + + + +const inheritedFields = + +table.inherits + +?.map((parentName) => { + +const parent = diagram.tables.find((t) => t.name === parentName); + +return parent ? parent.fields.map((f) => f.name) : []; + +}) + +.flat() || []; + + + +table.fields.forEach((field) => { + +if (field.primary) hasPrimaryKey = true; + + + +if (field.name === "") { + +issues.push(i18n.t("empty_field_name", { tableName: table.name })); + +} + + + +if (field.type === "") { + +issues.push(i18n.t("empty_field_type", { tableName: table.name })); + +} else if (field.type === "ENUM" || field.type === "SET") { + +if (!field.values || field.values.length === 0) { + +issues.push( + +i18n.t("no_values_for_field", { + +tableName: table.name, + +fieldName: field.name, + +type: field.type, + +}), + +); + +} + +} + + + +if (!checkDefault(field, diagram.database)) { + +issues.push( + +i18n.t("default_doesnt_match_type", { + +tableName: table.name, + +fieldName: field.name, + +}), + +); + +} + + + +if (field.notNull && field.default.toLowerCase() === "null") { + +issues.push( + +i18n.t("not_null_is_null", { + +tableName: table.name, + +fieldName: field.name, + +}), + +); + +} + + + +if (duplicateFieldNames[field.name]) { + +issues.push( + +i18n.t("duplicate_fields", { + +tableName: table.name, + +fieldName: field.name, + +}), + +); + +} else { + +duplicateFieldNames[field.name] = true; + +} + + + +if (inheritedFields.includes(field.name)) { + +issues.push( + +i18n.t("merging_column_w_inherited_definition", { + +fieldName: field.name, + +tableName: table.name, + +}), + +); + +} + +}); + + + +const duplicateIndices = {}; + +table.indices.forEach((index) => { + +if (duplicateIndices[index.name]) { + +issues.push( + +i18n.t("duplicate_index", { + +tableName: table.name, + +indexName: index.name, + +}), + +); + +} else { + +duplicateIndices[index.name] = true; + +} + +}); + + + +table.indices.forEach((index) => { + +if (index.name.trim() === "") { + +issues.push(i18n.t("empty_index_name", { tableName: table.name })); + +} + +if (index.fields.length === 0) { + +issues.push(i18n.t("empty_index", { tableName: table.name })); + +} + +}); + + + +if (!hasPrimaryKey) { + +issues.push(i18n.t("no_primary_key", { tableName: table.name })); + +} + +}); + + + +const duplicateTypeNames = {}; + +diagram.types.forEach((type) => { + +if (type.name === "") { + +issues.push(i18n.t("type_with_no_name")); + +} + + + +if (duplicateTypeNames[type.name]) { + +issues.push(i18n.t("duplicate_types", { typeName: type.name })); + +} else { + +duplicateTypeNames[type.name] = true; + +} + + + +if (type.fields.length === 0) { + +issues.push(i18n.t("type_w_no_fields", { typeName: type.name })); + +return; + +} + + + +const duplicateFieldNames = {}; + +type.fields.forEach((field) => { + +if (field.name === "") { + +issues.push(i18n.t("empty_type_field_name", { typeName: type.name })); + +} + + + +if (field.type === "") { + +issues.push(i18n.t("empty_type_field_type", { typeName: type.name })); + +} else if (field.type === "ENUM" || field.type === "SET") { + +if (!field.values || field.values.length === 0) { + +issues.push( + +i18n.t("no_values_for_type_field", { + +typeName: type.name, + +fieldName: field.name, + +type: field.type, + +}), + +); + +} + +} + + + +if (duplicateFieldNames[field.name]) { + +issues.push( + +i18n.t("duplicate_type_fields", { + +typeName: type.name, + +fieldName: field.name, + +}), + +); + +} else { + +duplicateFieldNames[field.name] = true; + +} + +}); + +}); + + + +const duplicateEnumNames = {}; + +diagram.enums.forEach((e) => { + +if (e.name === "") { + +issues.push(i18n.t("enum_w_no_name")); + +} + + + +if (duplicateEnumNames[e.name]) { + +issues.push(i18n.t("duplicate_enums", { enumName: e.name })); + +} else { + +duplicateEnumNames[e.name] = true; + +} + + + +if (e.values.length === 0) { + +issues.push(i18n.t("enum_w_no_values", { enumName: e.name })); + +return; + +} + +}); + + + +const duplicateFKName = {}; + +diagram.relationships.forEach((r) => { + +if (duplicateFKName[r.name]) { + +issues.push(i18n.t("duplicate_reference", { refName: r.name })); + +} else { + +duplicateFKName[r.name] = true; + +} + +}); + + + +const visitedTables = new Set(); + + + +function checkCircularRelationships(tableId, visited = []) { + +if (visited.includes(tableId)) { + +issues.push( + +i18n.t("circular_dependency", { + +refName: diagram.tables.find((t) => t.id === tableId)?.name, + +}), + +); + +return; + +} + + + +visited.push(tableId); + +visitedTables.add(tableId); + + + +diagram.relationships.forEach((r) => { + +if (r.startTableId === tableId && r.startTableId !== r.endTableId) { + +checkCircularRelationships(r.endTableId, [...visited]); + +} + +}); + +} + + + +diagram.tables.forEach((table) => { + +if (!visitedTables.has(table.id)) { + +checkCircularRelationships(table.id); + +} + +}); + + + +// Check for circular type references + +const visitedTypes = new Set(); + + + +function checkCircularTypeReferences(typeName, visited = []) { + +if (visited.includes(typeName)) { + +issues.push( + +i18n.t("circular_type_reference", { + +typeName: typeName, + +}), + +); + +return; + +} + + + +visited.push(typeName); + +visitedTypes.add(typeName); + + + +const currentType = diagram.types.find((t) => t.name === typeName); + +if (!currentType) return; + + + +currentType.fields.forEach((field) => { + +// Check if the field type references another custom type + +const referencedType = diagram.types.find((t) => t.name === field.type); + +if (referencedType && field.type !== typeName) { + +checkCircularTypeReferences(field.type, [...visited]); + +} + +}); + +} + + + +diagram.types.forEach((type) => { + +if (!visitedTypes.has(type.name)) { + +checkCircularTypeReferences(type.name); + +} + +}); + + + +return issues; + +} + + \ No newline at end of file From 469dbbbfc633fe26659dea56bc9ed434d109378f Mon Sep 17 00:00:00 2001 From: Ishaan Garg <168962484+Ishaang19@users.noreply.github.com> Date: Wed, 15 Oct 2025 21:00:43 +0530 Subject: [PATCH 3/3] Issue #9 [Proposal] Add an option whether to create foreign keys fix #9 --- src/components/EditorHeader/ControlPanel.jsx | 1 + src/context/SettingsContext.jsx | 132 ++++--- src/utils/exportSQL/index.js | 77 ++-- src/utils/exportSQL/mariadb.js | 223 ++++++++---- src/utils/exportSQL/mssql.js | 349 +++++++++++++------ src/utils/exportSQL/mysql.js | 244 ++++++++----- src/utils/exportSQL/oraclesql.js | 190 ++++++---- src/utils/exportSQL/postgres.js | 343 ++++++++++++------ src/utils/exportSQL/shared.js | 152 +++++--- src/utils/exportSQL/sqlite.js | 131 ++++--- 10 files changed, 1232 insertions(+), 610 deletions(-) diff --git a/src/components/EditorHeader/ControlPanel.jsx b/src/components/EditorHeader/ControlPanel.jsx index 99635ae..2953ae6 100644 --- a/src/components/EditorHeader/ControlPanel.jsx +++ b/src/components/EditorHeader/ControlPanel.jsx @@ -2044,3 +2044,4 @@ export default function ControlPanel({ ); } } + \ No newline at end of file diff --git a/src/context/SettingsContext.jsx b/src/context/SettingsContext.jsx index 9c50bf8..33cc89e 100644 --- a/src/context/SettingsContext.jsx +++ b/src/context/SettingsContext.jsx @@ -1,43 +1,89 @@ -import { createContext, useEffect, useState } from "react"; -import { tableWidth } from "../data/constants"; - -const defaultSettings = { - strictMode: false, - showFieldSummary: true, - showGrid: true, - snapToGrid: false, - showDataTypes: true, - mode: "light", - autosave: true, - showCardinality: true, - showRelationshipLabels: true, - tableWidth: tableWidth, - showDebugCoordinates: false, -}; - -export const SettingsContext = createContext(defaultSettings); - -export default function SettingsContextProvider({ children }) { - const [settings, setSettings] = useState(defaultSettings); - - useEffect(() => { - const settings = localStorage.getItem("settings"); - if (settings) { - setSettings(JSON.parse(settings)); - } - }, []); - - useEffect(() => { - document.body.setAttribute("theme-mode", settings.mode); - }, [settings.mode]); - - useEffect(() => { - localStorage.setItem("settings", JSON.stringify(settings)); - }, [settings]); - - return ( - - {children} - - ); -} +import { createContext, useEffect, useState } from "react"; + +import { tableWidth } from "../data/constants"; + + + +const defaultSettings = { + +strictMode: false, + +showFieldSummary: true, + +showGrid: true, + +snapToGrid: false, + +showDataTypes: true, + +mode: "light", + +autosave: true, + +showCardinality: true, + +showRelationshipLabels: true, + +tableWidth: tableWidth, + +showDebugCoordinates: false, + +generateForeignKeys: true, + +}; + + + +export const SettingsContext = createContext(defaultSettings); + + + +export default function SettingsContextProvider({ children }) { + +const [settings, setSettings] = useState(defaultSettings); + + + +useEffect(() => { + +const settings = localStorage.getItem("settings"); + +if (settings) { + +setSettings(JSON.parse(settings)); + +} + +}, []); + + + +useEffect(() => { + +document.body.setAttribute("theme-mode", settings.mode); + +}, [settings.mode]); + + + +useEffect(() => { + +localStorage.setItem("settings", JSON.stringify(settings)); + +}, [settings]); + + + +return ( + + + +{children} + + + +); + +} + + \ No newline at end of file diff --git a/src/utils/exportSQL/index.js b/src/utils/exportSQL/index.js index 987fb26..b990055 100644 --- a/src/utils/exportSQL/index.js +++ b/src/utils/exportSQL/index.js @@ -1,26 +1,51 @@ -import { DB } from "../../data/constants"; -import { toMariaDB } from "./mariadb"; -import { toMSSQL } from "./mssql"; -import { toMySQL } from "./mysql"; -import { toOracleSQL } from "./oraclesql"; -import { toPostgres } from "./postgres"; -import { toSqlite } from "./sqlite"; - -export function exportSQL(diagram) { - switch (diagram.database) { - case DB.SQLITE: - return toSqlite(diagram); - case DB.MYSQL: - return toMySQL(diagram); - case DB.POSTGRES: - return toPostgres(diagram); - case DB.MARIADB: - return toMariaDB(diagram); - case DB.MSSQL: - return toMSSQL(diagram); - case DB.ORACLESQL: - return toOracleSQL(diagram); - default: - return ""; - } -} +import { DB } from "../../data/constants"; + +import { toMariaDB } from "./mariadb"; + +import { toMSSQL } from "./mssql"; + +import { toMySQL } from "./mysql"; + +import { toOracleSQL } from "./oraclesql"; + +import { toPostgres } from "./postgres"; + +import { toSqlite } from "./sqlite"; + + + +export function exportSQL(diagram, generateForeignKeys = true) { + +switch (diagram.database) { + +case DB.SQLITE: + +return toSqlite(diagram, generateForeignKeys); + +case DB.MYSQL: + +return toMySQL(diagram, generateForeignKeys); + +case DB.POSTGRES: + +return toPostgres(diagram, generateForeignKeys); + +case DB.MARIADB: + +return toMariaDB(diagram, generateForeignKeys); + +case DB.MSSQL: + +return toMSSQL(diagram, generateForeignKeys); + +case DB.ORACLESQL: + +return toOracleSQL(diagram, generateForeignKeys); + +default: + +return ""; + +} + +} \ No newline at end of file diff --git a/src/utils/exportSQL/mariadb.js b/src/utils/exportSQL/mariadb.js index 8bab3bc..21b51c6 100644 --- a/src/utils/exportSQL/mariadb.js +++ b/src/utils/exportSQL/mariadb.js @@ -1,74 +1,149 @@ -import { escapeQuotes, parseDefault } from "./shared"; - -import { dbToTypes } from "../../data/datatypes"; -import { DB } from "../../data/constants"; - -function parseType(field) { - let res = field.type; - - if (field.type === "SET" || field.type === "ENUM") { - res += `${field.values ? "(" + field.values.map((value) => "'" + value + "'").join(", ") + ")" : ""}`; - } - - if (dbToTypes[DB.MARIADB][field.type].isSized) { - res += `${field.size && field.size !== "" ? "(" + field.size + ")" : ""}`; - } - - return res; -} - -export function toMariaDB(diagram) { - return `${diagram.tables - .map( - (table) => - `CREATE OR REPLACE TABLE \`${table.name}\` (\n${table.fields - .map( - (field) => - `\t\`${field.name}\` ${parseType(field)}${field.unsigned ? " UNSIGNED" : ""}${field.notNull ? " NOT NULL" : ""}${ - field.increment ? " AUTO_INCREMENT" : "" - }${field.unique ? " UNIQUE" : ""}${ - field.default !== "" - ? ` DEFAULT ${parseDefault(field, diagram.database)}` - : "" - }${ - field.check === "" || - !dbToTypes[diagram.database][field.type].hasCheck - ? "" - : ` CHECK(${field.check})` - }${field.comment ? ` COMMENT '${escapeQuotes(field.comment)}'` : ""}`, - ) - .join(",\n")}${ - table.fields.filter((f) => f.primary).length > 0 - ? `,\n\tPRIMARY KEY(${table.fields - .filter((f) => f.primary) - .map((f) => `\`${f.name}\``) - .join(", ")})` - : "" - }\n)${table.comment ? ` COMMENT='${escapeQuotes(table.comment)}'` : ""};${`\n${table.indices - .map( - (i) => - `\nCREATE ${i.unique ? "UNIQUE " : ""}INDEX \`${ - i.name - }\`\nON \`${table.name}\` (${i.fields - .map((f) => `\`${f}\``) - .join(", ")});`, - ) - .join("")}`}`, - ) - .join("\n")}\n${diagram.references - .map((r) => { - const { name: startName, fields: startFields } = diagram.tables.find( - (t) => t.id === r.startTableId, - ); - - const { name: endName, fields: endFields } = diagram.tables.find( - (t) => t.id === r.endTableId, - ); - return `ALTER TABLE \`${startName}\`\nADD FOREIGN KEY(\`${ - startFields.find((f) => f.id === r.startFieldId).name - }\`) REFERENCES \`${endName}\`(\`${ - endFields.find((f) => f.id === r.endFieldId).name - }\`)\nON UPDATE ${r.updateConstraint.toUpperCase()} ON DELETE ${r.deleteConstraint.toUpperCase()};`; - }) - .join("\n")}`; -} +import { escapeQuotes, parseDefault } from "./shared"; + + + +import { dbToTypes } from "../../data/datatypes"; + +import { DB } from "../../data/constants"; + + + +function parseType(field) { + +let res = field.type; + + + +if (field.type === "SET" || field.type === "ENUM") { + +res += `${field.values ? "(" + field.values.map((value) => "'" + value + "'").join(", ") + ")" : ""}`; + +} + + + +if (dbToTypes[DB.MARIADB][field.type].isSized) { + +res += `${field.size && field.size !== "" ? "(" + field.size + ")" : ""}`; + +} + + + +return res; + +} + + + +export function toMariaDB(diagram, generateForeignKeys = true) { + +return `${diagram.tables + +.map( + +(table) => + +`CREATE OR REPLACE TABLE \`${table.name}\` (\n${table.fields + +.map( + +(field) => + +`\t\`${field.name}\` ${parseType(field)}${field.unsigned ? " UNSIGNED" : ""}${field.notNull ? " NOT NULL" : ""}${ + +field.increment ? " AUTO_INCREMENT" : "" + +}${field.unique ? " UNIQUE" : ""}${ + +field.default !== "" + +? ` DEFAULT ${parseDefault(field, diagram.database)}` + +: "" + +}${ + +field.check === "" || + +!dbToTypes[diagram.database][field.type].hasCheck + +? "" + +: ` CHECK(${field.check})` + +}${field.comment ? ` COMMENT '${escapeQuotes(field.comment)}'` : ""}`, + +) + +.join(",\n")}${ + +table.fields.filter((f) => f.primary).length > 0 + +? `,\n\tPRIMARY KEY(${table.fields + +.filter((f) => f.primary) + +.map((f) => `\`${f.name}\``) + +.join(", ")})` + +: "" + +}\n)${table.comment ? ` COMMENT='${escapeQuotes(table.comment)}'` : ""};${`\n${table.indices + +.map( + +(i) => + +`\nCREATE ${i.unique ? "UNIQUE " : ""}INDEX \`${ + +i.name + +}\`\nON \`${table.name}\` (${i.fields + +.map((f) => `\`${f}\``) + +.join(", ")});`, + +) + +.join("")}`}`, + +) + +.join("\n")}${generateForeignKeys ? `\n${diagram.references + +.map((r) => { + +const { name: startName, fields: startFields } = diagram.tables.find( + +(t) => t.id === r.startTableId, + +); + + + +const { name: endName, fields: endFields } = diagram.tables.find( + +(t) => t.id === r.endTableId, + +); + +return `ALTER TABLE \`${startName}\`\nADD FOREIGN KEY(\`${ + +startFields.find((f) => f.id === r.startFieldId).name + +}\`) REFERENCES \`${endName}\`(\`${ + +endFields.find((f) => f.id === r.endFieldId).name + +}\`)\nON UPDATE ${r.updateConstraint.toUpperCase()} ON DELETE ${r.deleteConstraint.toUpperCase()};`; + +}) + +.join("\n")}` : ""}`; + +} + + \ No newline at end of file diff --git a/src/utils/exportSQL/mssql.js b/src/utils/exportSQL/mssql.js index a2354d3..007664a 100644 --- a/src/utils/exportSQL/mssql.js +++ b/src/utils/exportSQL/mssql.js @@ -1,116 +1,233 @@ -import { parseDefault, escapeQuotes } from "./shared"; - -import { dbToTypes } from "../../data/datatypes"; -import { DB } from "../../data/constants"; - -function generateAddExtendedPropertySQL(value, level1name, level2name = null) { - if (!value || value.trim() === "") { - return ""; - } - const escapedValue = escapeQuotes(value.replace(/\n/g, " ")); - const escapedTableName = escapeQuotes(level1name); - - if (level2name) { - const escapedColumnName = escapeQuotes(level2name); - return ` -EXEC sys.sp_addextendedproperty - @name=N'MS_Description', @value=N'${escapedValue}', - @level0type=N'SCHEMA',@level0name=N'dbo', - @level1type=N'TABLE',@level1name=N'${escapedTableName}', - @level2type=N'COLUMN',@level2name=N'${escapedColumnName}'; -GO -`; - } else { - return ` -EXEC sys.sp_addextendedproperty - @name=N'MS_Description', @value=N'${escapedValue}', - @level0type=N'SCHEMA',@level0name=N'dbo', - @level1type=N'TABLE',@level1name=N'${escapedTableName}'; -GO -`; - } -} - -export function toMSSQL(diagram) { - const tablesSql = diagram.tables - .map((table) => { - const fieldsSql = table.fields - .map((field) => { - const typeMetaData = dbToTypes[DB.MSSQL][field.type.toUpperCase()]; - const isSized = typeMetaData.isSized || typeMetaData.hasPrecision; - - return `\t[${field.name}] ${field.type}${field.size && isSized ? `(${field.size})` : ""}${ - field.notNull ? " NOT NULL" : "" - }${field.increment ? " IDENTITY" : ""}${ - field.unique ? " UNIQUE" : "" - }${ - field.default !== "" - ? ` DEFAULT ${parseDefault(field, diagram.database)}` - : "" - }${ - field.check === "" || - !dbToTypes[diagram.database][field.type].hasCheck - ? "" - : ` CHECK(${field.check})` - }`; - }) - .join(",\n"); - - const primaryKeys = table.fields.filter((f) => f.primary); - const primaryKeySql = - primaryKeys.length > 0 - ? `,\n\tPRIMARY KEY(${primaryKeys - .map((f) => `[${f.name}]`) - .join(", ")})` - : ""; - - const createTableSql = `CREATE TABLE [${table.name}] (\n${fieldsSql}${primaryKeySql}\n);\nGO\n`; - - const tableCommentSql = generateAddExtendedPropertySQL( - table.comment, - table.name, - ); - - const columnCommentsSql = table.fields - .map((field) => - generateAddExtendedPropertySQL(field.comment, table.name, field.name), - ) - .join(""); - - const indicesSql = table.indices - .map( - (i) => - `\nCREATE ${i.unique ? "UNIQUE " : ""}INDEX [${ - i.name - }]\nON [${table.name}] (${i.fields - .map((f) => `[${f}]`) - .join(", ")});\nGO\n`, - ) - .join(""); - - return `${createTableSql}${tableCommentSql}${columnCommentsSql}${indicesSql}`; - }) - .join("\n"); - - const referencesSql = diagram.references - .map((r) => { - const startTable = diagram.tables.find((t) => t.id === r.startTableId); - const endTable = diagram.tables.find((t) => t.id === r.endTableId); - - if (!startTable || !endTable) return ""; - - const startField = startTable.fields.find((f) => f.id === r.startFieldId); - const endField = endTable.fields.find((f) => f.id === r.endFieldId); - - if (!startField || !endField) return ""; - - return `\nALTER TABLE [${startTable.name}] -ADD FOREIGN KEY([${startField.name}]) -REFERENCES [${endTable.name}]([${endField.name}]) -ON UPDATE ${r.updateConstraint.toUpperCase()} ON DELETE ${r.deleteConstraint.toUpperCase()}; -GO`; - }) - .join(""); - - return `${tablesSql}\n${referencesSql}`; -} +import { parseDefault, escapeQuotes } from "./shared"; + + + +import { dbToTypes } from "../../data/datatypes"; + +import { DB } from "../../data/constants"; + + + +function generateAddExtendedPropertySQL(value, level1name, level2name = null) { + +if (!value || value.trim() === "") { + +return ""; + +} + +const escapedValue = escapeQuotes(value.replace(/\n/g, " ")); + +const escapedTableName = escapeQuotes(level1name); + + + +if (level2name) { + +const escapedColumnName = escapeQuotes(level2name); + +return ` + +EXEC sys.sp_addextendedproperty + +@name=N'MS_Description', @value=N'${escapedValue}', + +@level0type=N'SCHEMA',@level0name=N'dbo', + +@level1type=N'TABLE',@level1name=N'${escapedTableName}', + +@level2type=N'COLUMN',@level2name=N'${escapedColumnName}'; + +GO + +`; + +} else { + +return ` + +EXEC sys.sp_addextendedproperty + +@name=N'MS_Description', @value=N'${escapedValue}', + +@level0type=N'SCHEMA',@level0name=N'dbo', + +@level1type=N'TABLE',@level1name=N'${escapedTableName}'; + +GO + +`; + +} + +} + + + +export function toMSSQL(diagram, generateForeignKeys = true) { + +const tablesSql = diagram.tables + +.map((table) => { + +const fieldsSql = table.fields + +.map((field) => { + +const typeMetaData = dbToTypes[DB.MSSQL][field.type.toUpperCase()]; + +const isSized = typeMetaData.isSized || typeMetaData.hasPrecision; + + + +return `\t[${field.name}] ${field.type}${field.size && isSized ? `(${field.size})` : ""}${ + +field.notNull ? " NOT NULL" : "" + +}${field.increment ? " IDENTITY" : ""}${ + +field.unique ? " UNIQUE" : "" + +}${ + +field.default !== "" + +? ` DEFAULT ${parseDefault(field, diagram.database)}` + +: "" + +}${ + +field.check === "" || + +!dbToTypes[diagram.database][field.type].hasCheck + +? "" + +: ` CHECK(${field.check})` + +}`; + +}) + +.join(",\n"); + + + +const primaryKeys = table.fields.filter((f) => f.primary); + +const primaryKeySql = + +primaryKeys.length > 0 + +? `,\n\tPRIMARY KEY(${primaryKeys + +.map((f) => `[${f.name}]`) + +.join(", ")})` + +: ""; + + + +const createTableSql = `CREATE TABLE [${table.name}] (\n${fieldsSql}${primaryKeySql}\n);\nGO\n`; + + + +const tableCommentSql = generateAddExtendedPropertySQL( + +table.comment, + +table.name, + +); + + + +const columnCommentsSql = table.fields + +.map((field) => + +generateAddExtendedPropertySQL(field.comment, table.name, field.name), + +) + +.join(""); + + + +const indicesSql = table.indices + +.map( + +(i) => + +`\nCREATE ${i.unique ? "UNIQUE " : ""}INDEX [${ + +i.name + +}]\nON [${table.name}] (${i.fields + +.map((f) => `[${f}]`) + +.join(", ")});\nGO\n`, + +) + +.join(""); + + + +return `${createTableSql}${tableCommentSql}${columnCommentsSql}${indicesSql}`; + +}) + +.join("\n"); + + + +const referencesSql = generateForeignKeys ? diagram.references + +.map((r) => { + +const startTable = diagram.tables.find((t) => t.id === r.startTableId); + +const endTable = diagram.tables.find((t) => t.id === r.endTableId); + + + +if (!startTable || !endTable) return ""; + + + +const startField = startTable.fields.find((f) => f.id === r.startFieldId); + +const endField = endTable.fields.find((f) => f.id === r.endFieldId); + + + +if (!startField || !endField) return ""; + + + +return `\nALTER TABLE [${startTable.name}] + +ADD FOREIGN KEY([${startField.name}]) + +REFERENCES [${endTable.name}]([${endField.name}]) + +ON UPDATE ${r.updateConstraint.toUpperCase()} ON DELETE ${r.deleteConstraint.toUpperCase()}; + +GO`; + +}) + +.join("") : ""; + + + +return `${tablesSql}${referencesSql ? `\n${referencesSql}` : ""}`; + +} + + \ No newline at end of file diff --git a/src/utils/exportSQL/mysql.js b/src/utils/exportSQL/mysql.js index f35dbda..c75db38 100644 --- a/src/utils/exportSQL/mysql.js +++ b/src/utils/exportSQL/mysql.js @@ -1,81 +1,163 @@ -import { escapeQuotes, parseDefault } from "./shared"; - -import { dbToTypes } from "../../data/datatypes"; -import { DB } from "../../data/constants"; - -function parseType(field) { - let res = field.type; - - if (field.type === "SET" || field.type === "ENUM") { - res += `${field.values ? "(" + field.values.map((value) => "'" + value + "'").join(", ") + ")" : ""}`; - } - - if ( - dbToTypes[DB.MYSQL][field.type].isSized || - dbToTypes[DB.MYSQL][field.type].hasPrecision - ) { - res += `${field.size && field.size !== "" ? "(" + field.size + ")" : ""}`; - } - - return res; -} - -export function toMySQL(diagram) { - return `${diagram.tables - .map( - (table) => - `CREATE TABLE IF NOT EXISTS \`${table.name}\` (\n${table.fields - .map( - (field) => - `\t\`${field.name}\` ${parseType(field)}${ - dbToTypes[DB.MYSQL][field.type]?.signed && field.unsigned - ? " UNSIGNED" - : "" - }${field.notNull ? " NOT NULL" : ""}${ - field.increment ? " AUTO_INCREMENT" : "" - }${field.unique ? " UNIQUE" : ""}${ - field.default !== "" - ? ` DEFAULT ${parseDefault(field, diagram.database)}` - : "" - }${ - field.check === "" || - !dbToTypes[diagram.database][field.type].hasCheck - ? "" - : ` CHECK(${field.check})` - }${field.comment ? ` COMMENT '${escapeQuotes(field.comment)}'` : ""}`, - ) - .join(",\n")}${ - table.fields.filter((f) => f.primary).length > 0 - ? `,\n\tPRIMARY KEY(${table.fields - .filter((f) => f.primary) - .map((f) => `\`${f.name}\``) - .join(", ")})` - : "" - }\n)${table.comment ? ` COMMENT='${escapeQuotes(table.comment)}'` : ""};\n${`\n${table.indices - .map( - (i) => - `\nCREATE ${i.unique ? "UNIQUE " : ""}INDEX \`${ - i.name - }\`\nON \`${table.name}\` (${i.fields - .map((f) => `\`${f}\``) - .join(", ")});`, - ) - .join("")}`}`, - ) - .join("\n")}\n${diagram.references - .map((r) => { - const { name: startName, fields: startFields } = diagram.tables.find( - (t) => t.id === r.startTableId, - ); - - const { name: endName, fields: endFields } = diagram.tables.find( - (t) => t.id === r.endTableId, - ); - return `ALTER TABLE \`${startName}\`\nADD FOREIGN KEY(\`${ - startFields.find((f) => f.id === r.startFieldId).name - }\`) REFERENCES \`${endName}\`(\`${ - endFields.find((f) => f.id === r.endFieldId).name - }\`)\nON UPDATE ${r.updateConstraint.toUpperCase()} ON DELETE ${r.deleteConstraint.toUpperCase()};`; - }) - .join("\n")}`; -} +import { escapeQuotes, parseDefault } from "./shared"; + + + +import { dbToTypes } from "../../data/datatypes"; + +import { DB } from "../../data/constants"; + + + +function parseType(field) { + +let res = field.type; + + + +if (field.type === "SET" || field.type === "ENUM") { + +res += `${field.values ? "(" + field.values.map((value) => "'" + value + "'").join(", ") + ")" : ""}`; + +} + + + +if ( + +dbToTypes[DB.MYSQL][field.type].isSized || + +dbToTypes[DB.MYSQL][field.type].hasPrecision + +) { + +res += `${field.size && field.size !== "" ? "(" + field.size + ")" : ""}`; + +} + + + +return res; + +} + + + +export function toMySQL(diagram, generateForeignKeys = true) { + +return `${diagram.tables + +.map( + +(table) => + +`CREATE TABLE IF NOT EXISTS \`${table.name}\` (\n${table.fields + +.map( + +(field) => + +`\t\`${field.name}\` ${parseType(field)}${ + +dbToTypes[DB.MYSQL][field.type]?.signed && field.unsigned + +? " UNSIGNED" + +: "" + +}${field.notNull ? " NOT NULL" : ""}${ + +field.increment ? " AUTO_INCREMENT" : "" + +}${field.unique ? " UNIQUE" : ""}${ + +field.default !== "" + +? ` DEFAULT ${parseDefault(field, diagram.database)}` + +: "" + +}${ + +field.check === "" || + +!dbToTypes[diagram.database][field.type].hasCheck + +? "" + +: ` CHECK(${field.check})` + +}${field.comment ? ` COMMENT '${escapeQuotes(field.comment)}'` : ""}`, + +) + +.join(",\n")}${ + +table.fields.filter((f) => f.primary).length > 0 + +? `,\n\tPRIMARY KEY(${table.fields + +.filter((f) => f.primary) + +.map((f) => `\`${f.name}\``) + +.join(", ")})` + +: "" + +}\n)${table.comment ? ` COMMENT='${escapeQuotes(table.comment)}'` : ""};\n${`\n${table.indices + +.map( + +(i) => + +`\nCREATE ${i.unique ? "UNIQUE " : ""}INDEX \`${ + +i.name + +}\`\nON \`${table.name}\` (${i.fields + +.map((f) => `\`${f}\``) + +.join(", ")});`, + +) + +.join("")}`}`, + +) + +.join("\n")}${generateForeignKeys ? `\n${diagram.references + +.map((r) => { + +const { name: startName, fields: startFields } = diagram.tables.find( + +(t) => t.id === r.startTableId, + +); + + + +const { name: endName, fields: endFields } = diagram.tables.find( + +(t) => t.id === r.endTableId, + +); + +return `ALTER TABLE \`${startName}\`\nADD FOREIGN KEY(\`${ + +startFields.find((f) => f.id === r.startFieldId).name + +}\`) REFERENCES \`${endName}\`(\`${ + +endFields.find((f) => f.id === r.endFieldId).name + +}\`)\nON UPDATE ${r.updateConstraint.toUpperCase()} ON DELETE ${r.deleteConstraint.toUpperCase()};`; + +}) + +.join("\n")}` : ""}`; + +} + + \ No newline at end of file diff --git a/src/utils/exportSQL/oraclesql.js b/src/utils/exportSQL/oraclesql.js index 0a44c56..a1e1cf6 100644 --- a/src/utils/exportSQL/oraclesql.js +++ b/src/utils/exportSQL/oraclesql.js @@ -1,63 +1,127 @@ -import { dbToTypes } from "../../data/datatypes"; -import { parseDefault } from "./shared"; - -export function toOracleSQL(diagram) { - return `${diagram.tables - .map( - (table) => - `${ - table.comment === "" ? "" : `/* ${table.comment} */\n` - }CREATE TABLE "${table.name}" (\n${table.fields - .map( - (field) => - `${field.comment === "" ? "" : `\t-- ${field.comment}\n`}\t"${ - field.name - }" ${field.type}${ - field.size !== undefined && field.size !== "" - ? "(" + field.size + ")" - : "" - }${field.notNull ? " NOT NULL" : ""}${ - field.increment ? " GENERATED ALWAYS AS IDENTITY" : "" - }${field.unique ? " UNIQUE" : ""}${ - field.default !== "" - ? ` DEFAULT ${parseDefault(field, diagram.database)}` - : "" - }${ - field.check === "" || - !dbToTypes[diagram.database][field.type].hasCheck - ? "" - : ` CHECK(${field.check})` - }${field.comment ? ` -- ${field.comment}` : ""}`, - ) - .join(",\n")}${ - table.fields.filter((f) => f.primary).length > 0 - ? `,\n\tPRIMARY KEY(${table.fields - .filter((f) => f.primary) - .map((f) => `"${f.name}"`) - .join(", ")})` - : "" - }\n)${table.comment ? ` -- ${table.comment}` : ""};\n${`\n${table.indices - .map( - (i) => - `\nCREATE ${i.unique ? "UNIQUE " : ""}INDEX "${i.name}"\nON "${table.name}" (${i.fields - .map((f) => `"${f}"`) - .join(", ")});`, - ) - .join("")}`}`, - ) - .join("\n")}\n${diagram.references - .map((r) => { - const { name: startName, fields: startFields } = diagram.tables.find( - (t) => t.id === r.startTableId, - ); - const { name: endName, fields: endFields } = diagram.tables.find( - (t) => t.id === r.endTableId, - ); - return `ALTER TABLE "${startName}"\nADD CONSTRAINT "${r.name}" FOREIGN KEY ("${ - startFields.find((f) => f.id === r.startFieldId).name - }") REFERENCES "${endName}" ("${ - endFields.find((f) => f.id === r.endFieldId).name - }")\nON UPDATE ${r.updateConstraint.toUpperCase()} ON DELETE ${r.deleteConstraint.toUpperCase()};`; - }) - .join("\n")}`; -} +import { dbToTypes } from "../../data/datatypes"; + +import { parseDefault } from "./shared"; + + + +export function toOracleSQL(diagram, generateForeignKeys = true) { + +return `${diagram.tables + +.map( + +(table) => + +`${ + +table.comment === "" ? "" : `/* ${table.comment} */\n` + +}CREATE TABLE "${table.name}" (\n${table.fields + +.map( + +(field) => + +`${field.comment === "" ? "" : `\t-- ${field.comment}\n`}\t"${ + +field.name + +}" ${field.type}${ + +field.size !== undefined && field.size !== "" + +? "(" + field.size + ")" + +: "" + +}${field.notNull ? " NOT NULL" : ""}${ + +field.increment ? " GENERATED ALWAYS AS IDENTITY" : "" + +}${field.unique ? " UNIQUE" : ""}${ + +field.default !== "" + +? ` DEFAULT ${parseDefault(field, diagram.database)}` + +: "" + +}${ + +field.check === "" || + +!dbToTypes[diagram.database][field.type].hasCheck + +? "" + +: ` CHECK(${field.check})` + +}${field.comment ? ` -- ${field.comment}` : ""}`, + +) + +.join(",\n")}${ + +table.fields.filter((f) => f.primary).length > 0 + +? `,\n\tPRIMARY KEY(${table.fields + +.filter((f) => f.primary) + +.map((f) => `"${f.name}"`) + +.join(", ")})` + +: "" + +}\n)${table.comment ? ` -- ${table.comment}` : ""};\n${`\n${table.indices + +.map( + +(i) => + +`\nCREATE ${i.unique ? "UNIQUE " : ""}INDEX "${i.name}"\nON "${table.name}" (${i.fields + +.map((f) => `"${f}"`) + +.join(", ")});`, + +) + +.join("")}`}`, + +) + +.join("\n")}${generateForeignKeys ? `\n${diagram.references + +.map((r) => { + +const { name: startName, fields: startFields } = diagram.tables.find( + +(t) => t.id === r.startTableId, + +); + +const { name: endName, fields: endFields } = diagram.tables.find( + +(t) => t.id === r.endTableId, + +); + +return `ALTER TABLE "${startName}"\nADD CONSTRAINT "${r.name}" FOREIGN KEY ("${ + +startFields.find((f) => f.id === r.startFieldId).name + +}") REFERENCES "${endName}" ("${ + +endFields.find((f) => f.id === r.endFieldId).name + +}")\nON UPDATE ${r.updateConstraint.toUpperCase()} ON DELETE ${r.deleteConstraint.toUpperCase()};`; + +}) + +.join("\n")}` : ""}`; + +} + + \ No newline at end of file diff --git a/src/utils/exportSQL/postgres.js b/src/utils/exportSQL/postgres.js index 3dc6c0e..5070637 100644 --- a/src/utils/exportSQL/postgres.js +++ b/src/utils/exportSQL/postgres.js @@ -1,114 +1,229 @@ -import { escapeQuotes, exportFieldComment, parseDefault } from "./shared"; -import { dbToTypes } from "../../data/datatypes"; - -export function toPostgres(diagram) { - const enumStatements = diagram.enums - .map( - (e) => - `CREATE TYPE "${e.name}" AS ENUM (\n${e.values - .map((v) => `\t'${v}'`) - .join(",\n")}\n);\n`, - ) - .join("\n"); - - const typeStatements = diagram.types - .map( - (type) => - `CREATE TYPE ${type.name} AS (\n${type.fields - .map((f) => `\t${f.name} ${f.type}`) - .join(",\n")}\n);\n\n${ - type.comment?.trim() - ? `COMMENT ON TYPE "${type.name}" IS '${escapeQuotes(type.comment)}';\n` - : "" - }`, - ) - .join("\n"); - - const tableStatements = diagram.tables - .map((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, diagram.database)}` - : "" - }${ - field.check && dbToTypes[diagram.database][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 IF NOT EXISTS "${table.name}" (\n${fieldDefinitions}${primaryKeyClause}${inheritsClause};\n\n${commentStatements}\n${indexStatements}`; - }) - .join("\n\n"); - - const foreignKeyStatements = diagram.references - .map((r) => { - const startTable = diagram.tables.find((t) => t.id === r.startTableId); - const endTable = diagram.tables.find((t) => t.id === r.endTableId); - const startField = startTable?.fields.find( - (f) => f.id === r.startFieldId, - ); - const endField = endTable?.fields.find((f) => f.id === r.endFieldId); - - if (!startTable || !endTable || !startField || !endField) return ""; - - return `ALTER TABLE "${startTable.name}"\nADD FOREIGN KEY("${startField.name}") REFERENCES "${endTable.name}"("${endField.name}")\nON UPDATE ${r.updateConstraint.toUpperCase()} ON DELETE ${r.deleteConstraint.toUpperCase()};`; - }) - .filter(Boolean) - .join("\n"); - - return [ - enumStatements, - enumStatements.trim() && typeStatements - ? "\n" + typeStatements - : typeStatements, - tableStatements, - foreignKeyStatements, - ] - .filter(Boolean) - .join("\n"); -} +import { escapeQuotes, exportFieldComment, parseDefault } from "./shared"; + +import { dbToTypes } from "../../data/datatypes"; + + + +export function toPostgres(diagram, generateForeignKeys = true) { + +const enumStatements = diagram.enums + +.map( + +(e) => + +`CREATE TYPE "${e.name}" AS ENUM (\n${e.values + +.map((v) => `\t'${v}'`) + +.join(",\n")}\n);\n`, + +) + +.join("\n"); + + + +const typeStatements = diagram.types + +.map( + +(type) => + +`CREATE TYPE ${type.name} AS (\n${type.fields + +.map((f) => `\t${f.name} ${f.type}`) + +.join(",\n")}\n);\n\n${ + +type.comment?.trim() + +? `COMMENT ON TYPE "${type.name}" IS '${escapeQuotes(type.comment)}';\n` + +: "" + +}`, + +) + +.join("\n"); + + + +const tableStatements = diagram.tables + +.map((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, diagram.database)}` + +: "" + +}${ + +field.check && dbToTypes[diagram.database][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 IF NOT EXISTS "${table.name}" (\n${fieldDefinitions}${primaryKeyClause}${inheritsClause};\n\n${commentStatements}\n${indexStatements}`; + +}) + +.join("\n\n"); + + + +const foreignKeyStatements = generateForeignKeys ? diagram.references + +.map((r) => { + +const startTable = diagram.tables.find((t) => t.id === r.startTableId); + +const endTable = diagram.tables.find((t) => t.id === r.endTableId); + +const startField = startTable?.fields.find( + +(f) => f.id === r.startFieldId, + +); + +const endField = endTable?.fields.find((f) => f.id === r.endFieldId); + + + +if (!startTable || !endTable || !startField || !endField) return ""; + + + +return `ALTER TABLE "${startTable.name}"\nADD FOREIGN KEY("${startField.name}") REFERENCES "${endTable.name}"("${endField.name}")\nON UPDATE ${r.updateConstraint.toUpperCase()} ON DELETE ${r.deleteConstraint.toUpperCase()};`; + +}) + +.filter(Boolean) + +.join("\n") : ""; + + + +return [ + +enumStatements, + +enumStatements.trim() && typeStatements + +? "\n" + typeStatements + +: typeStatements, + +tableStatements, + +foreignKeyStatements, + +] + +.filter(Boolean) + +.join("\n"); + +} + + \ No newline at end of file diff --git a/src/utils/exportSQL/shared.js b/src/utils/exportSQL/shared.js index 64100bc..e7e1666 100644 --- a/src/utils/exportSQL/shared.js +++ b/src/utils/exportSQL/shared.js @@ -1,49 +1,103 @@ -import { isFunction, isKeyword } from "../utils"; - -import { DB } from "../../data/constants"; -import { dbToTypes } from "../../data/datatypes"; - -export function parseDefault(field, database = DB.GENERIC) { - if ( - isFunction(field.default) || - isKeyword(field.default) || - !dbToTypes[database][field.type].hasQuotes - ) { - return field.default; - } - - return `'${escapeQuotes(field.default)}'`; -} - -export function escapeQuotes(str) { - return str.replace(/[']/g, "'$&"); -} - -export function exportFieldComment(comment) { - if (comment === "") { - return ""; - } - - return comment - .split("\n") - .map((commentLine) => `\t-- ${commentLine}\n`) - .join(""); -} - -export function getInlineFK(table, obj) { - let fks = []; - obj.references.forEach((r) => { - if (r.startTableId === table.id) { - fks.push( - `\tFOREIGN KEY ("${table.fields.find((f) => f.id === r.startFieldId)?.name}") REFERENCES "${ - obj.tables.find((t) => t.id === r.endTableId)?.name - }"("${ - obj.tables - .find((t) => t.id === r.endTableId) - .fields.find((f) => f.id === r.endFieldId)?.name - }")\n\tON UPDATE ${r.updateConstraint.toUpperCase()} ON DELETE ${r.deleteConstraint.toUpperCase()}`, - ); - } - }); - return fks.join(",\n"); -} +import { isFunction, isKeyword } from "../utils"; + + + +import { DB } from "../../data/constants"; + +import { dbToTypes } from "../../data/datatypes"; + + + +export function parseDefault(field, database = DB.GENERIC) { + +if ( + +isFunction(field.default) || + +isKeyword(field.default) || + +!dbToTypes[database][field.type].hasQuotes + +) { + +return field.default; + +} + + + +return `'${escapeQuotes(field.default)}'`; + +} + + + +export function escapeQuotes(str) { + +return str.replace(/[']/g, "'$&"); + +} + + + +export function exportFieldComment(comment) { + +if (comment === "") { + +return ""; + +} + + + +return comment + +.split("\n") + +.map((commentLine) => `\t-- ${commentLine}\n`) + +.join(""); + +} + + + +export function getInlineFK(table, obj, generateForeignKeys = true) { + +if (!generateForeignKeys) return ""; + + + +let fks = []; + +obj.references.forEach((r) => { + +if (r.startTableId === table.id) { + +fks.push( + +`\tFOREIGN KEY ("${table.fields.find((f) => f.id === r.startFieldId)?.name}") REFERENCES "${ + +obj.tables.find((t) => t.id === r.endTableId)?.name + +}"("${ + +obj.tables + +.find((t) => t.id === r.endTableId) + +.fields.find((f) => f.id === r.endFieldId)?.name + +}")\n\tON UPDATE ${r.updateConstraint.toUpperCase()} ON DELETE ${r.deleteConstraint.toUpperCase()}`, + +); + +} + +}); + +return fks.join(",\n"); + +} + + \ No newline at end of file diff --git a/src/utils/exportSQL/sqlite.js b/src/utils/exportSQL/sqlite.js index 25ae186..99f39a5 100644 --- a/src/utils/exportSQL/sqlite.js +++ b/src/utils/exportSQL/sqlite.js @@ -1,44 +1,87 @@ -import { exportFieldComment, getInlineFK, parseDefault } from "./shared"; - -import { dbToTypes } from "../../data/datatypes"; - -export function toSqlite(diagram) { - return diagram.tables - .map((table) => { - const inlineFK = getInlineFK(table, diagram); - return `${ - table.comment === "" ? "" : `/* ${table.comment} */\n` - }CREATE TABLE IF NOT EXISTS "${table.name}" (\n${table.fields - .map( - (field) => - `${exportFieldComment(field.comment)}\t"${ - field.name - }" ${field.type}${field.notNull ? " NOT NULL" : ""}${ - field.unique ? " UNIQUE" : "" - }${field.default !== "" ? ` DEFAULT ${parseDefault(field, diagram.database)}` : ""}${ - field.check === "" || - !dbToTypes[diagram.database][field.type].hasCheck - ? "" - : ` CHECK(${field.check})` - }`, - ) - .join(",\n")}${ - table.fields.filter((f) => f.primary).length > 0 - ? `,\n\tPRIMARY KEY(${table.fields - .filter((f) => f.primary) - .map((f) => `"${f.name}"`) - .join(", ")})${inlineFK !== "" ? ",\n" : ""}` - : "" - }${inlineFK}\n);\n${table.indices - .map( - (i) => - `\nCREATE ${i.unique ? "UNIQUE " : ""}INDEX IF NOT EXISTS "${ - i.name - }"\nON "${table.name}" (${i.fields - .map((f) => `"${f}"`) - .join(", ")});`, - ) - .join("\n")}`; - }) - .join("\n"); -} \ No newline at end of file +import { exportFieldComment, getInlineFK, parseDefault } from "./shared"; + + + +import { dbToTypes } from "../../data/datatypes"; + + + +export function toSqlite(diagram, generateForeignKeys = true) { + +return diagram.tables + +.map((table) => { + +const inlineFK = getInlineFK(table, diagram, generateForeignKeys); + +return `${ + +table.comment === "" ? "" : `/* ${table.comment} */\n` + +}CREATE TABLE IF NOT EXISTS "${table.name}" (\n${table.fields + +.map( + +(field) => + +`${exportFieldComment(field.comment)}\t"${ + +field.name + +}" ${field.type}${field.notNull ? " NOT NULL" : ""}${ + +field.unique ? " UNIQUE" : "" + +}${field.default !== "" ? ` DEFAULT ${parseDefault(field, diagram.database)}` : ""}${ + +field.check === "" || + +!dbToTypes[diagram.database][field.type].hasCheck + +? "" + +: ` CHECK(${field.check})` + +}`, + +) + +.join(",\n")}${ + +table.fields.filter((f) => f.primary).length > 0 + +? `,\n\tPRIMARY KEY(${table.fields + +.filter((f) => f.primary) + +.map((f) => `"${f.name}"`) + +.join(", ")})${inlineFK !== "" ? ",\n" : ""}` + +: "" + +}${inlineFK}\n);\n${table.indices + +.map( + +(i) => + +`\nCREATE ${i.unique ? "UNIQUE " : ""}INDEX IF NOT EXISTS "${ + +i.name + +}"\nON "${table.name}" (${i.fields + +.map((f) => `"${f}"`) + +.join(", ")});`, + +) + +.join("\n")}`; + +}) + +.join("\n"); + +} \ No newline at end of file