diff --git a/src/components/EditorHeader/ControlPanel.jsx b/src/components/EditorHeader/ControlPanel.jsx index ecf378f..1c266bb 100644 --- a/src/components/EditorHeader/ControlPanel.jsx +++ b/src/components/EditorHeader/ControlPanel.jsx @@ -29,7 +29,7 @@ import { jsonToSQLite, jsonToMariaDB, jsonToSQLServer, -} from "../../utils/toSQL"; +} from "../../utils/exportSQL/generic"; import { ObjectType, Action, @@ -37,6 +37,7 @@ import { State, MODAL, SIDESHEET, + DB, } from "../../data/constants"; import jsPDF from "jspdf"; import { useHotkeys } from "react-hotkeys-hook"; @@ -62,6 +63,7 @@ import LayoutDropdown from "./LayoutDropdown"; import Sidesheet from "./SideSheet/Sidesheet"; import Modal from "./Modal/Modal"; import { useTranslation } from "react-i18next"; +import { exportSQL } from "../../utils/exportSQL"; export default function ControlPanel({ diagramId, @@ -851,89 +853,105 @@ export default function ControlPanel({ function: () => {}, }, export_source: { - children: [ - { - MySQL: () => { - setModal(MODAL.CODE); - const src = jsonToMySQL({ - tables: tables, - references: relationships, - types: types, - database: database, - }); - setExportData((prev) => ({ - ...prev, - data: src, - extension: "sql", - })); + ...(database === DB.GENERIC && { + children: [ + { + MySQL: () => { + setModal(MODAL.CODE); + const src = jsonToMySQL({ + tables: tables, + references: relationships, + types: types, + database: database, + }); + setExportData((prev) => ({ + ...prev, + data: src, + extension: "sql", + })); + }, }, - }, - { - PostgreSQL: () => { - setModal(MODAL.CODE); - const src = jsonToPostgreSQL({ - tables: tables, - references: relationships, - types: types, - database: database, - }); - setExportData((prev) => ({ - ...prev, - data: src, - extension: "sql", - })); + { + PostgreSQL: () => { + setModal(MODAL.CODE); + const src = jsonToPostgreSQL({ + tables: tables, + references: relationships, + types: types, + database: database, + }); + setExportData((prev) => ({ + ...prev, + data: src, + extension: "sql", + })); + }, }, - }, - { - SQLite: () => { - setModal(MODAL.CODE); - const src = jsonToSQLite({ - tables: tables, - references: relationships, - types: types, - database: database, - }); - setExportData((prev) => ({ - ...prev, - data: src, - extension: "sql", - })); + { + SQLite: () => { + setModal(MODAL.CODE); + const src = jsonToSQLite({ + tables: tables, + references: relationships, + types: types, + database: database, + }); + setExportData((prev) => ({ + ...prev, + data: src, + extension: "sql", + })); + }, }, - }, - { - MariaDB: () => { - setModal(MODAL.CODE); - const src = jsonToMariaDB({ - tables: tables, - references: relationships, - types: types, - database: database, - }); - setExportData((prev) => ({ - ...prev, - data: src, - extension: "sql", - })); + { + MariaDB: () => { + setModal(MODAL.CODE); + const src = jsonToMariaDB({ + tables: tables, + references: relationships, + types: types, + database: database, + }); + setExportData((prev) => ({ + ...prev, + data: src, + extension: "sql", + })); + }, }, - }, - { - MSSQL: () => { - setModal(MODAL.CODE); - const src = jsonToSQLServer({ - tables: tables, - references: relationships, - types: types, - database: database, - }); - setExportData((prev) => ({ - ...prev, - data: src, - extension: "sql", - })); + { + MSSQL: () => { + setModal(MODAL.CODE); + const src = jsonToSQLServer({ + tables: tables, + references: relationships, + types: types, + database: database, + }); + setExportData((prev) => ({ + ...prev, + data: src, + extension: "sql", + })); + }, }, - }, - ], - function: () => {}, + ], + }), + function: () => { + if (database === DB.GENERIC) return; + setModal(MODAL.CODE); + const src = exportSQL({ + tables: tables, + references: relationships, + types: types, + database: database, + }); + setExportData((prev) => ({ + ...prev, + data: src, + extension: "sql", + })); + }, }, exit: { function: () => { diff --git a/src/data/datatypes.js b/src/data/datatypes.js index df748a1..7af2d9a 100644 --- a/src/data/datatypes.js +++ b/src/data/datatypes.js @@ -98,6 +98,7 @@ export const defaultTypes = { isSized: true, hasPrecision: false, defaultSize: 1, + hasQuotes: true, }, VARCHAR: { type: "VARCHAR", @@ -111,6 +112,7 @@ export const defaultTypes = { isSized: true, hasPrecision: false, defaultSize: 255, + hasQuotes: true, }, TEXT: { type: "TEXT", @@ -119,16 +121,7 @@ export const defaultTypes = { isSized: true, hasPrecision: false, defaultSize: 65535, - }, - DATE: { - type: "DATE", - checkDefault: (field) => { - return /^\d{4}-\d{2}-\d{2}$/.test(field.default); - }, - hasCheck: false, - isSized: false, - hasPrecision: false, - defaultSize: null, + hasQuotes: true, }, TIME: { type: "TIME", @@ -139,6 +132,7 @@ export const defaultTypes = { isSized: false, hasPrecision: false, defaultSize: null, + hasQuotes: true, }, TIMESTAMP: { type: "TIMESTAMP", @@ -157,6 +151,18 @@ export const defaultTypes = { isSized: false, hasPrecision: false, defaultSize: null, + hasQuotes: true, + }, + DATE: { + type: "DATE", + checkDefault: (field) => { + return /^\d{4}-\d{2}-\d{2}$/.test(field.default); + }, + hasCheck: false, + isSized: false, + hasPrecision: false, + defaultSize: null, + hasQuotes: true, }, DATETIME: { type: "DATETIME", @@ -175,6 +181,7 @@ export const defaultTypes = { isSized: false, hasPrecision: false, defaultSize: null, + hasQuotes: true, }, BOOLEAN: { type: "BOOLEAN", @@ -200,6 +207,7 @@ export const defaultTypes = { isSized: true, hasPrecision: false, defaultSize: 1, + hasQuotes: true, }, VARBINARY: { type: "VARBINARY", @@ -212,6 +220,7 @@ export const defaultTypes = { isSized: true, hasPrecision: false, defaultSize: 255, + hasQuotes: true, }, BLOB: { type: "BLOB", @@ -246,6 +255,7 @@ export const defaultTypes = { isSized: false, hasPrecision: false, defaultSize: null, + hasQuotes: true, }, SET: { type: "SET", @@ -356,16 +366,126 @@ export const postgresTypes = { }; export const sqliteTypes = { - INTEGER: { type: "INTEGER", checkDefault: (field) => {} }, - REAL: { type: "REAL", checkDefault: (field) => {} }, - TEXT: { type: "TEXT", checkDefault: (field) => {} }, - BLOB: { type: "BLOB", checkDefault: (field) => {} }, - NUMERIC: { type: "NUMERIC", checkDefault: (field) => {} }, - BOOLEAN: { type: "BOOLEAN", checkDefault: (field) => {} }, - DATE: { type: "DATE", checkDefault: (field) => {} }, - DATETIME: { type: "DATETIME", checkDefault: (field) => {} }, - TIME: { type: "TIME", checkDefault: (field) => {} }, - TIMESTAMP: { type: "TIMESTAMP", checkDefault: (field) => {} }, + INT: { + type: "INT", + checkDefault: (field) => { + return intRegex.test(field.default); + }, + hasCheck: true, + isSized: false, + hasPrecision: false, + defaultSize: null, + }, + REAL: { + type: "REAL", + checkDefault: (field) => { + return doubleRegex.test(field.default); + }, + hasCheck: true, + isSized: false, + hasPrecision: true, + defaultSize: null, + }, + TEXT: { + type: "TEXT", + checkDefault: (field) => false, + hasCheck: false, + isSized: true, + hasPrecision: false, + defaultSize: 65535, + hasQuotes: true, + }, + BLOB: { + type: "BLOB", + checkDefault: (field) => false, + isSized: false, + hasCheck: false, + hasPrecision: false, + defaultSize: null, + }, + NUMERIC: { + type: "NUMERIC", + checkDefault: (field) => { + return doubleRegex.test(field.default); + }, + hasCheck: true, + isSized: false, + hasPrecision: true, + defaultSize: null, + }, + BOOLEAN: { + type: "BOOLEAN", + checkDefault: (field) => { + return ( + field.default.trim().toLowerCase() === "false" || + field.default.trim().toLowerCase() === "true" + ); + }, + hasCheck: false, + isSized: false, + hasPrecision: false, + defaultSize: null, + }, + TIME: { + type: "TIME", + checkDefault: (field) => { + return /^(?:[01]?\d|2[0-3]):[0-5]?\d:[0-5]?\d$/.test(field.default); + }, + hasCheck: false, + isSized: false, + hasPrecision: false, + defaultSize: null, + hasQuotes: true, + }, + TIMESTAMP: { + type: "TIMESTAMP", + checkDefault: (field) => { + if (field.default.toUpperCase() === "CURRENT_TIMESTAMP") { + return true; + } + if (!/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(field.default)) { + return false; + } + const content = field.default.split(" "); + const date = content[0].split("-"); + return parseInt(date[0]) >= 1970 && parseInt(date[0]) <= 2038; + }, + hasCheck: false, + isSized: false, + hasPrecision: false, + defaultSize: null, + hasQuotes: true, + }, + DATE: { + type: "DATE", + checkDefault: (field) => { + return /^\d{4}-\d{2}-\d{2}$/.test(field.default); + }, + hasCheck: false, + isSized: false, + hasPrecision: false, + defaultSize: null, + hasQuotes: true, + }, + DATETIME: { + type: "DATETIME", + checkDefault: (field) => { + if (field.default.toUpperCase() === "CURRENT_TIMESTAMP") { + return true; + } + if (!/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(field.default)) { + return false; + } + const c = field.default.split(" "); + const d = c[0].split("-"); + return parseInt(d[0]) >= 1000 && parseInt(d[0]) <= 9999; + }, + hasCheck: false, + isSized: false, + hasPrecision: false, + defaultSize: null, + hasQuotes: true, + }, }; export const mariadbTypes = { diff --git a/src/utils/toSQL.js b/src/utils/exportSQL/generic.js similarity index 96% rename from src/utils/toSQL.js rename to src/utils/exportSQL/generic.js index 58acaf1..f330387 100644 --- a/src/utils/toSQL.js +++ b/src/utils/exportSQL/generic.js @@ -1,5 +1,5 @@ -import { dbToTypes, defaultTypes } from "../data/datatypes"; -import { isFunction, isKeyword, strHasQuotes } from "./utils"; +import { dbToTypes, defaultTypes } from "../../data/datatypes"; +import { parseDefault } from "./shared"; export function getJsonType(f) { if (!Object.keys(defaultTypes).includes(f.type)) { @@ -137,33 +137,6 @@ export function getTypeString( } } -export function hasQuotes(type) { - return [ - "CHAR", - "VARCHAR", - "BINARY", - "VARBINARY", - "ENUM", - "DATE", - "TIME", - "TIMESTAMP", - "DATETIME", - ].includes(type); -} - -export function parseDefault(field) { - if ( - strHasQuotes(field.default) || - isFunction(field.default) || - isKeyword(field.default) || - !hasQuotes(field.type) - ) { - return field.default; - } - - return `'${field.default}'`; -} - export function jsonToMySQL(obj) { return `${obj.tables .map( @@ -178,7 +151,7 @@ export function jsonToMySQL(obj) { }\` ${getTypeString(field, obj.database)}${field.notNull ? " NOT NULL" : ""}${ field.increment ? " AUTO_INCREMENT" : "" }${field.unique ? " UNIQUE" : ""}${ - field.default !== "" ? ` DEFAULT ${parseDefault(field)}` : "" + field.default !== "" ? ` DEFAULT ${parseDefault(field, obj.database)}` : "" }${ field.check === "" || !dbToTypes[obj.database][field.type].hasCheck @@ -277,7 +250,7 @@ export function jsonToPostgreSQL(obj) { }" ${getTypeString(field, obj.database, "postgres")}${ field.notNull ? " NOT NULL" : "" }${ - field.default !== "" ? ` DEFAULT ${parseDefault(field)}` : "" + field.default !== "" ? ` DEFAULT ${parseDefault(field, obj.database)}` : "" }${ field.check === "" || !dbToTypes[obj.database][field.type].hasCheck @@ -378,7 +351,7 @@ export function jsonToSQLite(obj) { field.name }" ${getSQLiteType(field)}${field.notNull ? " NOT NULL" : ""}${ field.unique ? " UNIQUE" : "" - }${field.default !== "" ? ` DEFAULT ${parseDefault(field)}` : ""}${ + }${field.default !== "" ? ` DEFAULT ${parseDefault(field, obj.database)}` : ""}${ field.check === "" || !dbToTypes[obj.database][field.type].hasCheck ? "" @@ -424,7 +397,7 @@ export function jsonToMariaDB(obj) { }\` ${getTypeString(field, obj.database)}${field.notNull ? " NOT NULL" : ""}${ field.increment ? " AUTO_INCREMENT" : "" }${field.unique ? " UNIQUE" : ""}${ - field.default !== "" ? ` DEFAULT ${parseDefault(field)}` : "" + field.default !== "" ? ` DEFAULT ${parseDefault(field, obj.database)}` : "" }${ field.check === "" || !dbToTypes[obj.database][field.type].hasCheck @@ -498,7 +471,7 @@ export function jsonToSQLServer(obj) { }${field.increment ? " IDENTITY" : ""}${ field.unique ? " UNIQUE" : "" }${ - field.default !== "" ? ` DEFAULT ${parseDefault(field)}` : "" + field.default !== "" ? ` DEFAULT ${parseDefault(field, obj.database)}` : "" }${ field.check === "" || !dbToTypes[obj.database][field.type].hasCheck diff --git a/src/utils/exportSQL/index.js b/src/utils/exportSQL/index.js new file mode 100644 index 0000000..0782bb2 --- /dev/null +++ b/src/utils/exportSQL/index.js @@ -0,0 +1,19 @@ +import { DB } from "../../data/constants"; +import { toSqlite } from "./sqlite"; + +export function exportSQL(diagram) { + switch (diagram.database) { + case DB.SQLITE: + return toSqlite(diagram); + case DB.MYSQL: + return "hi from mysql"; + case DB.POSTGRES: + return "hi from postgres"; + case DB.MARIADB: + return "hi from mariadb"; + case DB.MSSQL: + return "hi from mssql"; + default: + return ""; + } +} diff --git a/src/utils/exportSQL/shared.js b/src/utils/exportSQL/shared.js new file mode 100644 index 0000000..83c4dce --- /dev/null +++ b/src/utils/exportSQL/shared.js @@ -0,0 +1,16 @@ +import { DB } from "../../data/constants"; +import { dbToTypes } from "../../data/datatypes"; +import { isFunction, isKeyword, strHasQuotes } from "../utils"; + +export function parseDefault(field, database = DB.GENERIC) { + if ( + strHasQuotes(field.default) || + isFunction(field.default) || + isKeyword(field.default) || + !dbToTypes[database][field.type].hasQuotes + ) { + return field.default; + } + + return `'${field.default}'`; +} diff --git a/src/utils/exportSQL/sqlite.js b/src/utils/exportSQL/sqlite.js new file mode 100644 index 0000000..36dbc5c --- /dev/null +++ b/src/utils/exportSQL/sqlite.js @@ -0,0 +1,62 @@ +import { dbToTypes } from "../../data/datatypes"; +import { parseDefault } from "./shared"; + +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) => + `${field.comment === "" ? "" : `\t-- ${field.comment}\n`}\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" : ""}` + : "" + }\t${inlineFK}\n);\n${ + table.indices.length > 0 + ? `${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"); +} + +export function getInlineFK(table, obj) { + let fk = ""; + obj.references.forEach((r) => { + if (fk !== "") return; + if (r.startTableId === table.id) { + fk = `FOREIGN KEY ("${table.fields[r.startFieldId].name}") REFERENCES "${ + obj.tables[r.endTableId].name + }"("${ + obj.tables[r.endTableId].fields[r.endFieldId].name + }")\n\tON UPDATE ${r.updateConstraint.toUpperCase()} ON DELETE ${r.deleteConstraint.toUpperCase()}`; + } + }); + return fk; +}