Field Modifiers integrated , extra options added to the field settings based on the data type of that field

This commit is contained in:
KarimTamani
2025-06-12 21:29:54 +01:00
parent 071bd5eecc
commit 12d2ac693e
33 changed files with 1157 additions and 335 deletions
+1
View File
@@ -37,6 +37,7 @@
"react-dom": "18.3.1",
"react-i18next": "^15.5.1",
"react-router-dom": "6.23.0",
"react-tag-input": "^6.10.6",
"sql-formatter": "^15.6.3",
"tailwind-merge": "^3.2.0",
"tailwind-variants": "0.3.0",
Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

+1 -2
View File
@@ -8,7 +8,7 @@ import { SyncProvider } from "./providers/sync-provider/sync-provider";
import DatabaseProvider from "./providers/database-provider/database-provider";
import DiagramProvider from "./providers/diagram-provider/diagram-provider";
import { ToastProvider } from "@heroui/react";
import { ToastProvider } from "@heroui/react";
import { ModalProvider } from "./providers/modal-provider/modal-provider";
@@ -19,7 +19,6 @@ function App() {
return (
<>
<ToastProvider placement="bottom-right" />
<SyncProvider>
<ReactFlowProvider>
<DatabaseProvider>
+4 -3
View File
@@ -1,6 +1,7 @@
import Menu from "../menu/menu";
import { useDatabase } from "@/providers/database-provider/database-provider";
import Menu from "../menu/menu";
import ConnectionStatus from "./connection-status";
import RenameDatabase from "./rename-database";
@@ -10,8 +11,8 @@ interface Props {
const Navbar: React.FC<Props> = ({ }) => {
const { database } = useDatabase();
return (
<nav className="h-12 fixed z-50 bg-background w-full flex items-center p-4 border-b border-divider ">
@@ -28,7 +29,7 @@ const Navbar: React.FC<Props> = ({ }) => {
<Menu />
<div className=" w-full h-full flex items-center justify-center">
<RenameDatabase/>
{database && <RenameDatabase database={database} />}
</div>
<ConnectionStatus />
+9 -6
View File
@@ -3,13 +3,16 @@ import React, { useEffect, useState } from "react";
import { Tooltip, TooltipContent, TooltipTrigger } from "../tooltip/tooltip";
import { useTranslation } from "react-i18next";
import { Button, Divider, Image, Input } from "@heroui/react";
import { getDatabaseByDialect } from "@/lib/database";
import { DatabaseDialect, getDatabaseByDialect } from "@/lib/database";
import { Save } from "lucide-react";
import { DatabaseType } from "@/lib/schemas/database-schema";
interface RenameDatabaseProps {
database : DatabaseType
}
const RenameDatabase: React.FC = ({ }) => {
const { database } = useDatabase();
const RenameDatabase: React.FC<RenameDatabaseProps> = ({ database}) => {
const { editDatabase } = useDatabaseOperations();
const [editMode, setEditMode] = useState<boolean>(false);
const [dbName, setDbName] = useState<string>(database.name);
@@ -44,7 +47,7 @@ const RenameDatabase: React.FC = ({ }) => {
onDoubleClick={() => setEditMode(true)}
>
<Image
src={getDatabaseByDialect(database.dialect).logo}
src={getDatabaseByDialect(database.dialect as DatabaseDialect).logo}
width={22}
className=" rounded-none "
/>
@@ -62,7 +65,7 @@ const RenameDatabase: React.FC = ({ }) => {
editMode &&
<div className="flex justify-center items-center gap-4">
<Image
src={getDatabaseByDialect(database.dialect).logo}
src={getDatabaseByDialect(database.dialect as DatabaseDialect).logo}
width={32}
className=" rounded-none "
/>
+68
View File
@@ -0,0 +1,68 @@
import React, { useEffect, useState } from "react"
import { useTranslation } from "react-i18next";
import { WithContext as ReactTagInput, Tag } from 'react-tag-input';
interface TagInputProps {
defaultItems?: string[];
onItemsChange? : (items: string[]) => void
}
const KeyCodes = {
comma: 188,
enter: 13,
};
const delimiters = [KeyCodes.comma, KeyCodes.enter];
const TagInput: React.FC<TagInputProps> = ({ defaultItems = [], onItemsChange }) => {
const { t } = useTranslation();
const [tags, setTags] = useState<Tag[]>(defaultItems.map((item: string) => ({
id: item.toLowerCase(),
text: item,
className: ""
}) as Tag));
const handleDelete = (i: number) => {
setTags(tags.filter((_, index) => index !== i));
};
const handleAddition = (tag: Tag) => {
setTags([...tags, tag]);
};
const handleDrag = (tag: Tag, currPos: number, newPos: number) => {
const newTags = tags.slice();
newTags.splice(currPos, 1);
newTags.splice(newPos, 0, tag);
setTags(newTags);
};
useEffect(() => onItemsChange && onItemsChange(tags.map((tag: Tag) => tag.text)), [tags])
return (
<div>
<ReactTagInput
tags={tags}
classNames={{
tag: "font-normal inline-block m-0.5 border-1 border-default rounded-full px-2 py-1 flex-row ",
remove: "bg-green-500 bg-default-900 rounded-full text-xs text-center ml-1 min-w-[15px] max-w-[15px] min-h-[15px] max-h-[15px] transition-colors duration-300 hover:bg-black",
tagInputField: "relative w-full inline-flex flex-row items-center bg-default-100 border-1 border-divider hover:border-primary focus-within:border-default-400 h-8 min-h-8 px-2 rounded-small transition-background !duration-150 transition-colors outline-none dark:bg-default placeholder:text-foreground-500 mt-2" ,
tags : "max-h-[256px] overflow-auto "
}}
handleDelete={handleDelete}
autoFocus={false}
handleAddition={handleAddition}
handleDrag={handleDrag}
delimiters={delimiters}
placeholder={t("db_controller.field_settings.type_enter")}
/>
</div>
);
}
export default React.memo(TagInput)
+1
View File
@@ -33,6 +33,7 @@ const useHighlightedEdges = (nodes: Node[], relationships: RelationshipType[], e
* This avoids recomputation unless `selectedNodeIds` or `relationships` change.
*/
const selectedEdgeIds: string[] = useMemo(() => {
return relationships
.filter((relationship: RelationshipType) =>
selectedNodeIds.includes(relationship.sourceTableId) ||
+5 -5
View File
@@ -7,7 +7,7 @@ import { useEffect, useState } from "react";
import { Parser } from "node-sql-parser";
import { DatabaseType } from "@/lib/schemas/database-schema";
import { format } from 'sql-formatter';
import { getDatabaseByDialect } from "@/lib/database";
import { DatabaseDialect, getDatabaseByDialect } from "@/lib/database";
const parser = new Parser();
@@ -17,11 +17,11 @@ export const useRenderSql = (database: DatabaseType) => {
const { data_types } = useDatabaseOperations();
useEffect(() => {
const dbAst: any = DatabaseToAst(database, data_types);
const dbAst: any = DatabaseToAst(database, data_types);
const formattedSqlCode = format(parser.sqlify(dbAst , {
database : "MySQL" // getDatabaseByDialect(database.dialect).name
}), { language: 'sql' });
const formattedSqlCode = format(parser.sqlify(dbAst, {
database: getDatabaseByDialect(database.dialect ).name
}), { language: 'sql' });
setSql(
formattedSqlCode
);
+40 -20
View File
@@ -12,9 +12,9 @@ export const en = {
color_picker: {
default_color: "Default color"
},
navbar : {
rename_db : "Rename database" ,
} ,
navbar: {
rename_db: "Rename database",
},
db_controller: {
filter: "Filter",
add_table: "Add Table",
@@ -34,9 +34,6 @@ export const en = {
unique: "Unique",
table_note: "Table note",
collapse: "Collapse All",
primary_key: "Primary Key",
foreign_key: "Foreign Key",
@@ -55,14 +52,37 @@ export const en = {
many_to_many: "Many to Many",
},
field_settings: {
title: "Field Setting",
unique: "Unique",
unsigned: "Unsigned",
numeric_setting : "Numeric Setting" ,
decimal_setting : "Decimal Setting" ,
zeroFill: "Zero Fill",
autoIncrement: "Auto Increment",
note: "Note",
delete_field: "Delete Field",
field_note: "Field note",
precision : "Precision" ,
text_setting : "Text Setting" ,
charset : "Charset" ,
collation : "Collation" ,
scale : "Scale" ,
max_length : "Max length" ,
default_value : "Default value" ,
value : "Value" ,
length : "Length" ,
values : "Values" ,
type_enter : "Type and press enter"
},
delete: "Delete",
field_setting: "Field Setting",
index_setting: "Index Setting",
table_actions: "Table Actions",
actions: "Actions",
field_note: "Field note",
delete_field: "Delete Field",
delete_index: "Delete Index",
index_name: "Index name",
@@ -124,18 +144,18 @@ export const en = {
modals: {
close: "Close",
create: "Create" ,
create: "Create",
pick_database: "Pick your Database.",
create_database_header: "Every database offers distinct features and functionalities." ,
db_name : "Database name" ,
db_name_error : "Please provide a Database name" ,
continue : "Continue" ,
open : "Open" ,
open_database : "Open Database" ,
open_database_header : "Open a database by selecting one from the list." ,
delete_database : "Delete Database" ,
delete_database_content : "This action is irreversible and will permanently remove the diagram." ,
delete : "Delete"
create_database_header: "Every database offers distinct features and functionalities.",
db_name: "Database name",
db_name_error: "Please provide a Database name",
continue: "Continue",
open: "Open",
open_database: "Open Database",
open_database_header: "Open a database by selecting one from the list.",
delete_database: "Delete Database",
delete_database_content: "This action is irreversible and will permanently remove the diagram.",
delete: "Delete"
}
}
+57 -2
View File
@@ -1,7 +1,9 @@
import { tags as t } from '@lezer/highlight';
import { EditorView } from '@uiw/react-codemirror';
export const colorOptions = [
"#ff6363", // Red
"#FF7A6A", // Red-Orange
"#ff9f74", // Orange
@@ -19,4 +21,57 @@ export const colorOptions = [
export const randomColor = () => {
return colorOptions[Math.floor(Math.random() * colorOptions.length)];
};
};
export const overrideDarkTheme = EditorView.theme({
'.cm-content': {
backgroundColor: "#20252c"
},
".cm-gutter": {
backgroundColor: "#20252c",
},
".cm-gutterElement": {
color: "#4b515a"
},
".ͼp": {
color: "#A994FF"
},
".cm-line .ͼq": {
color : "#ff6363"
} ,
".ͼu" : {
color : "#B6E672"
} ,
".ͼv": {
color : "#6cdcc4"
}
}, { dark: true });
export const overrideLightTheme = EditorView.theme({
".ͼb": {
color: "#2A1D66"
},
".cm-gutterElement": {
color: "#a2a4a8"
},
".cm-gutter": {
backgroundColor: "white",
},
".cm-gutters": {
borderColor: "#f2f4f6"
},
".cm-line": {
color: "#333639"
},
});
+18 -4
View File
@@ -8,25 +8,39 @@ export interface DatabaseType {
logo: string
}
export enum DatabaseDialect {
MYSQL = "mysql",
POSTGRES = "postgres",
SQLITE = "sqlite",
MARIADB = "mariadb"
};
export const DBTypes: DatabaseType[] = [
{
name: "Postgresql",
dialect: "postgres",
dialect: DatabaseDialect.POSTGRES,
logo: "/postgresql_logo.png"
}, {
name: "MySQL",
dialect: "mysql",
dialect: DatabaseDialect.MYSQL,
logo: "/mysql_logo.png"
},
{
name: "Sqlite",
dialect: "sqlite",
dialect: DatabaseDialect.SQLITE,
logo: "/sqlite_logo.png"
},
{
name: "MariaDB",
dialect: DatabaseDialect.MARIADB,
logo: "/mariadb_logo.png"
},
]
export const getDatabaseByDialect = (dialect: string): DatabaseType => {
export const getDatabaseByDialect = (dialect: DatabaseDialect): DatabaseType => {
const dbType : DatabaseType | undefined = DBTypes.find((dbType : DatabaseType) => dbType.dialect == dialect) ;
return dbType ? dbType : DBTypes[0] ;
}
+76
View File
@@ -0,0 +1,76 @@
export enum Modifiers {
LENGTH = "length",
UNSIGNED = "unsigned",
ZEROFILL = "zerofill",
AUTO_INCREMENT = "auto_increment",
PRECISION = "precision",
SCALE = "scale",
CHARSET = "charset",
COLLATE = "collate",
VALUES = "values" ,
}
export enum MySQLCharset {
Utf8mb4 = 'utf8mb4',
Utf8mb3 = 'utf8mb3',
Latin1 = 'latin1',
Ascii = 'ascii',
Binary = 'binary',
Ucs2 = 'ucs2',
Utf16 = 'utf16',
Utf32 = 'utf32',
Big5 = 'big5',
Gb2312 = 'gb2312',
Gbk = 'gbk',
Sjis = 'sjis',
Euckr = 'euckr',
}
export enum MySQLCollation {
Utf8mb4GeneralCi = 'utf8mb4_general_ci',
Utf8mb4UnicodeCi = 'utf8mb4_unicode_ci',
Utf8mb4Bin = 'utf8mb4_bin',
Utf8mb40900AiCi = 'utf8mb4_0900_ai_ci',
Latin1SwedishCi = 'latin1_swedish_ci',
Latin1GeneralCi = 'latin1_general_ci',
AsciiGeneralCi = 'ascii_general_ci',
Binary = 'binary',
}
export enum PostgreSQLCharset {
Utf8 = 'UTF8',
Latin1 = 'LATIN1',
Latin9 = 'LATIN9',
Win1250 = 'WIN1250',
Win1252 = 'WIN1252',
SqlAscii = 'SQL_ASCII',
EUCJp = 'EUC_JP',
EUCKr = 'EUC_KR',
MULEInternal = 'MULE_INTERNAL',
SJIS = 'SJIS',
}
export enum PostgreSQLCollation {
EnUsUtf8 = 'en_US.UTF-8',
C = 'C',
Posix = 'POSIX',
DeDe = 'de_DE.UTF-8',
FrFr = 'fr_FR.UTF-8',
JaJp = 'ja_JP.UTF-8',
CustomICU = 'und-x-icu', // ICU-based collation example
}
export enum SQLiteCharset {
Utf8 = 'UTF-8',
Utf16le = 'UTF-16le',
Utf16be = 'UTF-16be',
}
export enum SQLiteCollation {
Binary = 'BINARY',
NoCase = 'NOCASE',
RTrim = 'RTRIM',
}
+9 -2
View File
@@ -3,13 +3,16 @@ import { sqliteTable, text } from 'drizzle-orm/sqlite-core';
import { fields } from './field-schema';
export const data_types = sqliteTable('data_types', {
id: text("id"),
name: text('name'),
dialect: text("dialect", {
enum: ["postgres", "mysql", "sqlite", "mariadb"],
}).notNull().default("postgres"),
type: text('type').notNull()
type: text('type').notNull(),
modifiers: text("modifiers")
});
@@ -19,4 +22,8 @@ export const dataTypeRelations = relations(fields, ({ many }) => ({
}));
export interface DataType extends InferSelectModel<typeof data_types> { };
export interface DataType extends InferSelectModel<typeof data_types> {
modifiers: string | null | any ;
};
+6 -2
View File
@@ -2,6 +2,7 @@ import { InferInsertModel, InferSelectModel, relations } from 'drizzle-orm';
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
import { tables, TableType } from './table-schema';
import { relationships, RelationshipType } from './relationship-schema';
import { DatabaseDialect } from '../database';
export const databases = sqliteTable('databases', {
@@ -9,9 +10,11 @@ export const databases = sqliteTable('databases', {
.primaryKey()
.notNull()
.unique(),
name: text('name').notNull(),
dialect: text("dialect", {
enum: ["postgres", "mysql", "sqlite" , "mariadb"],
enum: ["postgres", "mysql", "sqlite", "mariadb"],
}).notNull().default("postgres"),
numOfTables: integer("numOfTables")
@@ -28,6 +31,7 @@ export const databaseRelations = relations(databases, ({ many }) => ({
export interface DatabaseType extends InferSelectModel<typeof databases> {
tables: TableType[],
relationships: RelationshipType[]
relationships: RelationshipType[];
dialect: DatabaseDialect
};
export interface DatabaseInsertType extends InferInsertModel<typeof databases> { };
+10
View File
@@ -21,6 +21,16 @@ export const fields = sqliteTable("fields", {
note: text("note"),
typeId: text("typeId").references(() => data_types.id, { onDelete: "cascade" }),
sequence: integer("sequence").default(0),
maxLength: integer('maxLength') , // or `.nullable()` if you're using drizzle-kit latest
unsigned: integer('unsigned' , {mode : "boolean"}).default(false),
isForeign: integer('isForeign', {mode : "boolean"}).default(false),
zeroFill: integer('zeroFill', {mode : "boolean"}).default(false),
autoIncrement: integer('autoIncrement', {mode : "boolean"}).default(false),
precision: integer('precision'),
scale: integer('scale'),
charset: text('charset'),
collate: text('collate'),
values: text('values'),
});
export const fieldsRelations = relations(fields, ({ one, many }) => ({
+41
View File
@@ -0,0 +1,41 @@
import { getForeignRelationships } from "@/utils/relationship";
import { DatabaseType } from "./schemas/database-schema";
import { RelationshipType } from "./schemas/relationship-schema";
import { TableType } from "./schemas/table-schema";
export interface SortableTable {
tableId: string;
relationships: string[];
}
export interface RenderableTable extends TableType {
foreignRelationships: RelationshipType[]
}
export const toSortableTable = (table: RenderableTable): SortableTable => {
return {
tableId: table.id,
relationships: table.foreignRelationships.filter(
(relationship : RelationshipType) => !(relationship.targetTable.id == relationship.sourceTable.id && relationship.sourceTable.id == table.id)
).map((relationship: RelationshipType) => relationship.sourceTable.id)
}
}
export const toRenderableTable = (table: TableType, database: DatabaseType): RenderableTable => {
let renderableTable: RenderableTable | TableType = {
...table,
sourceRelations: database.relationships.filter((relationship: RelationshipType) =>
relationship.sourceTableId == table.id),
targetRelations: database.relationships.filter((relationship: RelationshipType) =>
relationship.targetTableId == table.id),
} ;
(renderableTable as RenderableTable).foreignRelationships = getForeignRelationships(renderableTable) ;
return renderableTable as RenderableTable ;
}
-2
View File
@@ -13,9 +13,7 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<BrowserRouter>
<Provider>
<App />
</Provider>
</BrowserRouter>
</React.StrictMode>,
+10 -7
View File
@@ -37,27 +37,30 @@ import useHighlightedEdges from "@/hooks/use-highlighted-edges";
import { useTranslation } from "react-i18next";
import useOverlappingTables from "@/hooks/use-overlapping-tables";
import { useTheme } from "next-themes";
import { Parser } from "node-sql-parser";
import { getRelationshipSourceAndTarget } from "@/utils/relationship";
const parser = new Parser();
import { getRelationshipSourceAndTarget } from "@/utils/relationship";
import { useModal } from "@/providers/modal-provider/modal-provider";
const DatabasePage: React.FC = () => {
const { open } = useModal();
const { t } = useTranslation();
const { resolvedTheme } = useTheme();
// Extract database state and operations
const { database, getField } = useDatabase();
const { updateTablePositions, deleteMultiTables, deleteMultiRelationships, createRelationship } = useDatabaseOperations();
// Node and edge state hooks
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
// Diagram-related state (e.g. connection in progress)
const { setIsConnectionInProgress } = useDiagramOps();
// Destructure tables and relationships from database
const { tables, relationships } = database;
const { tables, relationships } = database || { tables: [], relationships: [] };
// Hook to allow zooming and centering the diagram
const { fitView } = useReactFlow();
@@ -75,7 +78,7 @@ const DatabasePage: React.FC = () => {
const targetField: FieldType = getField(connection.target, targetId) as FieldType;
const { sourceTableId, targetTableId, sourceFieldId, targetFieldId } = getRelationshipSourceAndTarget(connection.source, sourceField, connection.target, targetField);
const { sourceTableId, targetTableId, sourceFieldId, targetFieldId } = getRelationshipSourceAndTarget(connection.source, sourceField, connection.target, targetField);
// Check if both fields have the same type (valid relationship)
if (sourceField?.typeId == targetField?.typeId) {
@@ -85,7 +88,7 @@ const DatabasePage: React.FC = () => {
targetTableId,
sourceFieldId,
targetFieldId,
} as RelationshipInsertType);
// Add edge to the diagram
@@ -26,7 +26,7 @@ const RelationshipController: React.FC = ({ }) => {
const { open } = useModal();
const { database } = useDatabase();
const { relationships: allRelationships } = database;
const { relationships: allRelationships } = database || { relationships : []};
const [relationships, setRelationships] = useState<RelationshipType[]>(allRelationships);
const nameRef: Ref<HTMLInputElement> = useRef<HTMLInputElement>(null);
@@ -108,7 +108,7 @@ const RelationshipController: React.FC = ({ }) => {
className="h-8 w-full focus-visible:ring-0 shadow-none "
onKeyUp={searchRelationships}
classNames={{
inputWrapper: "dark:bg-default border-divider group-hover:border-primary ",
inputWrapper: "dark:bg-default group-hover:border-primary ",
}}
/>
@@ -2,12 +2,12 @@ import { useRenderSql } from "@/hooks/user-render-sql";
import { useDatabase } from "@/providers/database-provider/database-provider";
import React, { useEffect, useMemo } from "react";
import CodeMirror from '@uiw/react-codemirror';
import CodeMirror, { EditorView } from '@uiw/react-codemirror';
import { sql } from '@codemirror/lang-sql';
import { oneDark } from '@codemirror/theme-one-dark';
import { useTheme } from "next-themes";
import { useTheme } from "next-themes";
import { Parser } from "node-sql-parser";
import { overrideDarkTheme, overrideLightTheme } from "@/lib/colors";
const parser = new Parser();
const code = `
@@ -37,31 +37,30 @@ CREATE TABLE \`addresses\` (
const SqlPreview: React.FC = ({ }) => {
const { database } = useDatabase();
const sqlCode = useRenderSql(database);
const {resolvedTheme} = useTheme();
const { resolvedTheme } = useTheme();
useEffect(() => {
const ast = parser.astify(code , {
database : "Mysql"
} )
console.log (ast)
const ast = parser.astify(code, {
database: "Mysql"
})
console.log(ast)
}, [])
return (
<div className="flex w-full h-full ">
{
<CodeMirror
value={sqlCode}
className="flex flex-1 w-full"
extensions={[sql()]}
theme={resolvedTheme == "light" ? undefined : oneDark}
/>
<CodeMirror
value={sqlCode}
className="flex flex-1 w-full"
extensions={[sql()]}
theme={resolvedTheme == "light" ? overrideLightTheme : [oneDark , overrideDarkTheme] }
/>
}
</div>
)
@@ -2,14 +2,15 @@
import { useSortable } from "@dnd-kit/sortable";
import { Button, Input, Popover, PopoverContent, PopoverTrigger, Switch, Textarea } from "@heroui/react";
import { EllipsisVertical, GripVertical, KeyRound, Trash2 } from "lucide-react";
import { Ellipsis, EllipsisVertical, GripVertical, KeyRound, Settings, Settings2 } from "lucide-react";
import { useTranslation } from "react-i18next";
import { CSS } from "@dnd-kit/utilities";
import { FieldInsertType, FieldType } from "@/lib/schemas/field-schema";
import { Key, useEffect, useState } from "react";
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
import Autocomplete from "@/components/auto-complete/auto-complete";
import ToggleButton from "@/components/toggle/toggle";
import FieldSetting from "./field-setting";
interface Props {
field: FieldType
}
@@ -21,10 +22,9 @@ const FieldItem: React.FC<Props> = ({ field }) => {
const [fieldName, setFieldName] = useState<string>(field.name);
const [popOverOpen, setPopOverOpen] = useState<boolean>(false);
const { data_types , grouped_data_types } = useDatabaseOperations();
const { deleteField, editField } = useDatabaseOperations();
const { grouped_data_types } = useDatabaseOperations();
const { editField } = useDatabaseOperations();
const [note, setNote] = useState<string | undefined>(field.note as string | undefined);
const [selectedType, setSelectedType] = useState<string | undefined>(field.typeId as string | undefined);
const { t } = useTranslation();
@@ -42,24 +42,7 @@ const FieldItem: React.FC<Props> = ({ field }) => {
setSelectedType(field.typeId as string | undefined);
}, [field.typeId])
const removeField = () => {
setPopOverOpen(false);
deleteField(field.id)
}
const updateFieldNote = () => {
editField({
id: field.id,
note: note,
} as FieldInsertType)
}
const toggleUnqiue = (value: boolean) => {
editField({
id: field.id,
unique: value
} as FieldInsertType)
}
const saveFieldName = () => {
editField({
id: field.id,
@@ -136,7 +119,7 @@ const FieldItem: React.FC<Props> = ({ field }) => {
<KeyRound className="size-4" />
</ToggleButton>
<Popover placement="bottom" radius="sm" shadow="sm" showArrow isOpen={popOverOpen} onOpenChange={setPopOverOpen}>
<Popover placement="right-start" radius="sm" shadow="sm" isOpen={popOverOpen} onOpenChange={setPopOverOpen} >
<PopoverTrigger>
<Button
size="sm"
@@ -147,52 +130,8 @@ const FieldItem: React.FC<Props> = ({ field }) => {
<EllipsisVertical className="size-4 " />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[210px] " >
<div className="w-full flex flex-col gap-2 p-2 ">
<h3 className="font-semibold text-sm text-font/90">
{t("db_controller.field_setting")}
</h3>
<hr className="border-divider" />
<div className="flex w-full justify-between">
<span className="text-sm text-icon font-medium dark:text-font/90">
{t("db_controller.unique")}
</span>
<Switch size="sm" defaultSelected={field.unique as boolean} onValueChange={toggleUnqiue}>
</Switch>
</div>
<label className="text-sm font-medium text-icon dark:text-font/90">
{t("db_controller.note")}
</label>
<Textarea
variant="bordered"
className="w-full"
label={t("db_controller.field_note")}
value={note}
disableAutosize
disableAnimation
onValueChange={setNote}
onBlur={updateFieldNote}
classNames={{
inputWrapper: "bg-default border-divider ",
base: "max-w-xs",
input: "resize-y min-h-[60px] max-h-[180px]",
}} />
<hr className="border-divider" />
<Button
className="bg-default dark:bg-danger dark:border-none dark:text-white"
radius="sm"
variant="faded"
color="danger"
size="sm"
onPressEnd={removeField}>
<span className="font-medium text-sm ">
{t("db_controller.delete_field")}
</span>
<Trash2 className="mr-1 size-3.5 text-danger dark:text-white" />
</Button>
</div>
<PopoverContent >
<FieldSetting field={field}/>
</PopoverContent>
</Popover>
</div>
@@ -0,0 +1,416 @@
import TagInput from "@/components/tag-input/tag-input";
import { Modifiers, PostgreSQLCharset, PostgreSQLCollation } from "@/lib/field";
import { FieldInsertType, FieldType } from "@/lib/schemas/field-schema";
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
import { Button, Checkbox, Input, Select, SelectItem, SharedSelection, Switch, Textarea } from "@heroui/react";
import { Trash2 } from "lucide-react";
import React, { Ref, useCallback, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
interface FieldSettingProps {
field: FieldType
}
const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
console.log(field.type)
const modifiers: string[] = field.type.modifiers ? JSON.parse(field.type.modifiers) : [];
const { deleteField, editField } = useDatabaseOperations();
const { t } = useTranslation();
const [charset, setCharset] = useState(new Set([field.charset]));
const [collation, setCollation] = useState(new Set([field.collate]));
const [note, setNote] = useState<string | undefined>(field.note as string | undefined);
const defaultNameRef: Ref<HTMLInputElement> = useRef<HTMLInputElement>(null);
const maxLengthRef: Ref<HTMLInputElement> = useRef<HTMLInputElement>(null);
const scaleRef: Ref<HTMLInputElement> = useRef<HTMLInputElement>(null);
const precisionRef: Ref<HTMLInputElement> = useRef<HTMLInputElement>(null);
const removeField = () => {
deleteField(field.id)
}
const updateFieldNote = useCallback(() => {
editField({
id: field.id,
note: note,
} as FieldInsertType)
}, [field])
const toggleUnqiue = useCallback((value: boolean) => {
editField({
id: field.id,
unique: value
} as FieldInsertType)
}, [field]);
const toggleUnsigned = useCallback((value: boolean) => {
editField({
id: field.id,
unsigned: value
} as FieldInsertType)
}, [field]);
const toggleAutoIncrement = useCallback((value: boolean) => {
editField({
id: field.id,
autoIncrement: value
} as FieldInsertType)
}, [field]);
const toggleZeroFill = useCallback((value: boolean) => {
editField({
id: field.id,
zeroFill: value
} as FieldInsertType)
}, [field]);
const changeCharset = useCallback((keys: SharedSelection) => {
if (keys.anchorKey != field.charset) {
editField({
id: field.id,
charset: keys.anchorKey,
} as FieldInsertType);
}
setCharset(keys as any);
}, [field]);
const changeCollation = useCallback((keys: SharedSelection) => {
if (keys.anchorKey != field.charset) {
editField({
id: field.id,
collate: keys.anchorKey,
} as FieldInsertType);
}
setCollation(keys as any);
}, [field])
const saveDefaultValue = useCallback(() => {
const value: string | undefined = defaultNameRef.current?.value;
editField({
id: field.id,
defaultValue: value ? String(value) : null,
} as FieldInsertType);
}, [defaultNameRef, field]);
const saveMaxLength = useCallback(() => {
const value: string | undefined = maxLengthRef.current?.value;
editField({
id: field.id,
maxLength: value ? value : null,
} as FieldInsertType);
}, [maxLengthRef, field]);
const savePrecision = useCallback(() => {
const value: string | undefined = precisionRef.current?.value;
editField({
id: field.id,
precision: value ? value : null,
} as FieldInsertType);
}, [precisionRef, field]);
const saveScale = useCallback(() => {
const value: string | undefined = scaleRef.current?.value;
editField({
id: field.id,
scale: value ? value : null,
} as FieldInsertType);
}, [scaleRef, field]);
const updateValues = useCallback((values: string[]) => {
const jsonValues = JSON.stringify(values);
console.log(jsonValues)
if (jsonValues != field.values)
editField({
id: field.id,
values: jsonValues,
} as FieldInsertType);
}, [field])
const showNumericModifiers: boolean = modifiers.includes(Modifiers.AUTO_INCREMENT) || modifiers.includes(Modifiers.UNSIGNED) || modifiers.includes(Modifiers.ZEROFILL)
const showDecimalModifiers: boolean = modifiers.includes(Modifiers.PRECISION) || modifiers.includes(Modifiers.SCALE);
const showTextModifiers: boolean = modifiers.includes(Modifiers.COLLATE) || modifiers.includes(Modifiers.CHARSET);
return (
<div className="w-full flex flex-col gap-2 p-2 min-w-[260px] max-w-[260px]">
<h3 className="font-semibold text-sm text-font/90">
{t("db_controller.field_settings.title")}
</h3>
<hr className="border-divider" />
<div className="flex w-full justify-between">
<span className="text-xs text-icon font-medium dark:text-font/90">
{t("db_controller.field_settings.unique")}
</span>
<Checkbox defaultSelected={field.unique as boolean} size="md" onValueChange={toggleUnqiue} />
</div>
{
showNumericModifiers && <>
<h3 className="font-semibold text-sm text-font/90">
{t("db_controller.field_settings.numeric_setting")}
</h3>
<hr className="border-divider" />
{
modifiers.includes(Modifiers.AUTO_INCREMENT) &&
<div className="flex w-full justify-between">
<span className="text-xs text-icon font-medium dark:text-font/90">
{t("db_controller.field_settings.autoIncrement")}
</span>
<Checkbox defaultSelected={field.autoIncrement as boolean} size="md" onValueChange={toggleAutoIncrement} />
</div>
}
{
modifiers.includes(Modifiers.UNSIGNED) &&
<div className="flex w-full justify-between">
<span className="text-xs text-icon font-medium dark:text-font/90">
{t("db_controller.field_settings.unsigned")}
</span>
<Checkbox defaultSelected={field.unsigned as boolean} size="md" onValueChange={toggleUnsigned} />
</div>
}
{
modifiers.includes(Modifiers.ZEROFILL) &&
<div className="flex w-full justify-between">
<span className="text-xs text-icon font-medium dark:text-font/90">
{t("db_controller.field_settings.zeroFill")}
</span>
<Checkbox defaultSelected={field.zeroFill as boolean} size="md" onValueChange={toggleZeroFill} />
</div>
}
</>
}
{
showDecimalModifiers && <>
<h3 className="font-semibold text-sm text-font/90">
{t("db_controller.field_settings.decimal_setting")}
</h3>
<hr className="border-divider" />
<div className="flex gap-2">
{
modifiers.includes(Modifiers.PRECISION) &&
<div className="flex flex-col gap-2 w-full">
<label className="text-xs font-medium text-icon dark:text-font/90">
{t("db_controller.field_settings.precision")}
</label>
<Input
type="number"
size="sm"
ref={precisionRef}
defaultValue={String(field.precision)}
onBlur={savePrecision}
radius="sm"
variant="faded"
placeholder={t("db_controller.field_settings.precision")}
className="h-8 w-full focus-visible:ring-0 shadow-none "
classNames={{
inputWrapper: "dark:bg-default border-divider group-hover:border-primary ",
}}
/>
</div>
}
{
modifiers.includes(Modifiers.SCALE) &&
<div className="flex flex-col gap-2 w-full">
<label className="text-xs font-medium text-icon dark:text-font/90">
{t("db_controller.field_settings.scale")}
</label>
<Input
type="number"
size="sm"
ref={scaleRef}
defaultValue={String(field.scale)}
onBlur={saveScale}
radius="sm"
variant="faded"
placeholder={t("db_controller.field_settings.scale")}
className="h-8 w-full focus-visible:ring-0 shadow-none "
classNames={{
inputWrapper: "dark:bg-default border-divider group-hover:border-primary ",
}}
/>
</div>
}
</div>
</>
}
{
showTextModifiers &&
<>
<h3 className="font-semibold text-sm text-font/90">
{t("db_controller.field_settings.text_setting")}
</h3>
<hr className="border-divider" />
{
modifiers.includes(Modifiers.CHARSET) && <>
<label className="text-xs font-medium text-icon dark:text-font/90">
{t("db_controller.field_settings.charset")}
</label>
<Select
className="w-full"
size="sm"
variant="bordered"
aria-label="charset"
placeholder={t("db_controller.field_settings.charset")}
selectedKeys={charset as any}
onSelectionChange={changeCharset}
classNames={{
trigger: "border-divider group-hover:border-primary",
}}
>
{
Object.values(PostgreSQLCharset).map((charset: string) => (<SelectItem key={charset}>{charset}</SelectItem>))
}
</Select>
</>
}
{
modifiers.includes(Modifiers.COLLATE) && <>
<label className="text-xs font-medium text-icon dark:text-font/90">
{t("db_controller.field_settings.collation")}
</label>
<Select
className="w-full"
size="sm"
variant="bordered"
aria-label="collation"
placeholder={t("db_controller.field_settings.collation")}
selectedKeys={collation as any}
onSelectionChange={changeCollation}
classNames={{
trigger: "border-divider group-hover:border-primary",
}}
>
{
Object.values(PostgreSQLCollation).map((collation: string) => (<SelectItem key={collation}>{collation}</SelectItem>))
}
</Select>
</>
}
</>
}
{
modifiers.includes(Modifiers.LENGTH) && <>
<label className="text-xs font-medium text-icon dark:text-font/90">
{t("db_controller.field_settings.max_length")}
</label>
<Input
type="number"
size="sm"
ref={maxLengthRef}
defaultValue={String(field.maxLength)}
onBlur={saveMaxLength}
radius="sm"
variant="faded"
placeholder={t("db_controller.field_settings.length")}
className="h-8 w-full focus-visible:ring-0 shadow-none "
classNames={{
inputWrapper: "dark:bg-default border-divider group-hover:border-primary ",
}}
/>
</>
}
{
modifiers.includes(Modifiers.VALUES) && <>
<h3 className="font-semibold text-sm text-font/90">
{field.type.name != null && (field.type.name?.[0].toUpperCase() + field.type.name?.slice(1))} {t("db_controller.field_settings.values")}
</h3>
<hr className="border-divider" />
<TagInput
onItemsChange={updateValues}
defaultItems={field.values ? JSON.parse(field.values) : []}
/>
</>
}
<label className="text-xs font-medium text-icon dark:text-font/90">
{t("db_controller.field_settings.default_value")}
</label>
<Input
type="text"
size="sm"
ref={defaultNameRef}
defaultValue={field.defaultValue as string}
onBlur={saveDefaultValue}
radius="sm"
variant="faded"
placeholder={t("db_controller.field_settings.value")}
className="h-8 w-full focus-visible:ring-0 shadow-none "
classNames={{
inputWrapper: "dark:bg-default border-divider group-hover:border-primary ",
}}
/>
<label className="text-xs font-medium text-icon dark:text-font/90">
{t("db_controller.field_settings.note")}
</label>
<Textarea
variant="bordered"
className="w-full"
label={t("db_controller.field_settings.field_note")}
value={note}
disableAutosize
disableAnimation
onValueChange={setNote}
onBlur={updateFieldNote}
classNames={{
inputWrapper: "bg-default border-divider ",
base: "max-w-xs",
input: "resize-y min-h-[60px] max-h-[180px]",
}} />
<hr className="border-divider" />
<Button
className="bg-default dark:bg-danger dark:border-none dark:text-white"
radius="sm"
variant="faded"
color="danger"
size="sm"
onPressEnd={removeField}>
<span className="font-medium text-sm ">
{t("db_controller.field_settings.delete_field")}
</span>
<Trash2 className="mr-1 size-3.5 text-danger dark:text-white" />
</Button>
</div>
)
}
export default React.memo(FieldSetting);
@@ -21,7 +21,7 @@ const TablesController: React.FC<Props> = ({ }) => {
const { database, getDefaultPrimaryKeyType } = useDatabase();
const { createTable, data_types } = useDatabaseOperations();
const { getViewport } = useReactFlow();
const { tables: allTables } = database;
const { tables: allTables } = database || { tables : []};
const [tables, setTables] = useState<TableType[]>(allTables);
const { t } = useTranslation();
@@ -3,12 +3,12 @@ import Modal, { ModalProps } from "@/components/modal/modal"
import { DatabaseType, DBTypes } from "@/lib/database";
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
import { Button, CheckboxGroup, Input } from "@heroui/react";
import { Database, SquareMenu } from "lucide-react";
import { useCallback, useEffect, useState } from "react"
import { Database, SquareMenu } from "lucide-react";
import { useCallback, useEffect, useState } from "react"
import { useTranslation } from "react-i18next";
import { v4 } from "uuid";
export const CreateDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) => {
const [isValid, setIsValid] = useState<boolean>(false);
const { t } = useTranslation();
@@ -26,16 +26,18 @@ export const CreateDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange
const databaseId: string = v4();
return new Promise(async (res, rej) => {
await createDatabase({
id: databaseId,
name: dbName,
dialect: selectedDbType[0] as any
});
switchDatabase(databaseId);
res(databaseId)
})
}, [selectedDbType, dbName])
switchDatabase(databaseId);
res(databaseId)
})
}, [selectedDbType, dbName])
@@ -43,85 +45,85 @@ export const CreateDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange
setIsValid((selectedDbType.length > 0 && dbName.trim().length > 0) as boolean)
}, [selectedDbType, dbName])
return (
<Modal
isOpen={isOpen}
onOpenChange={onOpenChange}
title={t("modals.pick_database")}
actionName={t("modals.continue")}
className="min-w-[720px]"
isDisabled={!isValid}
actionHandler={createNewDatabase}
header={t("modals.create_database_header")}
>
<div className="w-full justify-center flex ">
<div className="flex flex-col gap-1 w-[70%]">
<div className="p-8 py-2 pb-4 space-y-2">
<label className="text-sm text-font/90 font-semibold">
{t("modals.db_name")}
</label>
<Input
errorMessage={t("modals.db_name_error")}
isInvalid={dbName.trim().length == 0}
type="text"
value={dbName}
onValueChange={setDbName}
size="sm"
radius="sm"
variant="faded"
placeholder={t("modals.db_name")}
className="h-8 w-full focus-visible:ring-0 shadow-none "
startContent={
<Database className="text-icon size-4 " />
}
classNames={{
inputWrapper: "dark:bg-default border-divider group-hover:border-primary ",
}}
/>
</div>
<CheckboxGroup
classNames={{
base: "w-full p-0 ",
wrapper: "flex-row p-4 gap-8 px-0 items-center justify-center"
}}
aria-label="Select Database"
value={selectedDbType}
onChange={onDatabaseTypeChange}
>
{
DBTypes.map((db: DatabaseType) => (
<DatabaseCheckbox
database={db}
/>
))
return (
<Modal
isOpen={isOpen}
onOpenChange={onOpenChange}
title={t("modals.pick_database")}
actionName={t("modals.continue")}
className="min-w-[720px]"
isDisabled={!isValid}
actionHandler={createNewDatabase}
header={t("modals.create_database_header")}
>
<div className="w-full justify-center flex ">
<div className="flex flex-col gap-1 w-[70%]">
<div className="p-8 py-2 pb-4 space-y-2">
<label className="text-sm text-font/90 font-semibold">
{t("modals.db_name")}
</label>
<Input
errorMessage={t("modals.db_name_error")}
isInvalid={dbName.trim().length == 0}
type="text"
value={dbName}
onValueChange={setDbName}
size="sm"
radius="sm"
variant="faded"
placeholder={t("modals.db_name")}
className="h-8 w-full focus-visible:ring-0 shadow-none "
startContent={
<Database className="text-icon size-4 " />
}
</CheckboxGroup>
<div className="p-8 py-4 space-y-2">
<Button
variant="bordered"
size="sm"
className="w-full text-font border-divider"
>
<SquareMenu className="size-4" /> Check examples
</Button>
<Button
variant="bordered"
size="sm"
className="w-full text-font border-divider"
>
<span className="underline">
Empty Diagram
</span>
</Button>
</div>
classNames={{
inputWrapper: "dark:bg-default border-divider group-hover:border-primary ",
}}
/>
</div>
<CheckboxGroup
classNames={{
base: "w-full p-0 ",
wrapper: "flex-row p-4 gap-8 px-0 items-center justify-center"
}}
aria-label="Select Database"
value={selectedDbType}
onChange={onDatabaseTypeChange}
>
{
DBTypes.map((db: DatabaseType) => (
<DatabaseCheckbox
database={db}
/>
))
}
</CheckboxGroup>
<div className="p-8 py-4 space-y-2">
<Button
variant="bordered"
size="sm"
className="w-full text-font border-divider"
>
<SquareMenu className="size-4" /> Check examples
</Button>
<Button
variant="bordered"
size="sm"
className="w-full text-font border-divider"
>
<span className="underline">
Empty Diagram
</span>
</Button>
</div>
</div>
</Modal>
)
</div>
</Modal>
)
}
@@ -32,7 +32,7 @@ const CreateRelationshipModal: React.FC<CreateRelationshipModalProps> = ({ onRel
const { database } = useDatabase();
const { tables } = database;
const { tables } = database || { tables : []};
const { t } = useTranslation();
@@ -12,18 +12,20 @@ interface Props { children: React.ReactNode };
const DatabaseHistoryProvider: React.FC<Props> = ({ children }) => {
const udpateDbFlag = useRef(false);
const { database } = useDatabase();
const { database } = useDatabase() as { database: DatabaseType };
const { executeDbDiffOps } = useDatabaseOperations();
const [isProcessing, setIsProcessing] = useState<boolean>(false);
const [datatbaseState, { set, undo: undoChanges, redo: redoChanges, canUndo, canRedo }] = useUndo<DatabaseType>(database);
useEffect(() => {
udpateDbFlag.current = false;
const presentHash: string = hash(datatbaseState.present, { algorithm: 'sha1' });
const databaseHash: string = hash(database, { algorithm: 'sha1' });
if (presentHash != databaseHash)
set(database);
}, [database]);
@@ -48,13 +50,13 @@ const DatabaseHistoryProvider: React.FC<Props> = ({ children }) => {
return;
}
const normalizedDatabase = normalizeDatabase(database);
const normalizedPresent = normalizeDatabase(datatbaseState.present);
const differences = compare(normalizedDatabase, normalizedPresent);
if (differences && differences.length > 0) {
setIsProcessing(true);
@@ -14,7 +14,7 @@ import { createContext } from "react";
interface DatabaseDataContextType {
database: DatabaseType,
database: DatabaseType | undefined ,
currentDatabaseId: string | undefined,
databases: DatabaseType[],
isLoading: boolean,
@@ -18,19 +18,17 @@ import { v4 } from "uuid";
import { DataType } from "@/lib/schemas/data-type-schema";
interface Props { children: React.ReactNode }
const DatabaseProvider: React.FC<Props> = ({ children }) => {
const [currentDatabaseId, setCurrentDatabaseId] = useState<string | undefined>(localStorage.getItem("database_id") as string | undefined);
// Fetch all databases
const { data: databases, isLoading: loadingDatabases } = useQuery(toCompilableQuery(
db.query.databases.findMany()
));
// Fetch the current database with nested tables, fields, and relationships
let { data: database, isLoading: loadingCurrentDatabase, isFetching } = useQuery(
toCompilableQuery(
@@ -67,7 +65,7 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
})
)
);
const switchDatabase = useCallback((databaseId: string | undefined) => {
setCurrentDatabaseId(databaseId);
@@ -80,25 +78,27 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
// Normalize result to single object
if (database.length == 1)
database = database[0] as any;
else
database = undefined as any;
// Fetch all data types
const { data: data_types, isLoading: loadingDataTypes } = useQuery(toCompilableQuery(
// Fetch all data types
let { data: data_types, isLoading: loadingDataTypes } = useQuery(toCompilableQuery(
db.query.data_types.findMany({
where : (data_types , { eq })=> eq(data_types.dialect, (database as any).dialect)
where: (data_types, { eq }) => eq(data_types.dialect, (database as any)?.dialect)
})
));
const grouped_data_types: any = useMemo(() => {
return groupBy(data_types, "type");
}, [data_types]);
const grouped_data_types : any = useMemo(() => {
return groupBy(data_types , "type") ;
} , [data_types])
// Auto-select first database if none is selected
useEffect(() => {
if (databases.length > 0 && !currentDatabaseId) {
switchDatabase(databases[0].id);
}
}, [currentDatabaseId, databases]);
const isLoading: boolean = loadingDataTypes || loadingDatabases || loadingCurrentDatabase;
@@ -109,10 +109,12 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
// CRUD for database
const createDatabase = useCallback(async (database: DatabaseInsertType): Promise<QueryResult> => {
return await db.insert(databaseModel).values({
...database,
createdAt: getTimestamp()
});
}, [db]);
const editDatabase = useCallback(async (database: DatabaseInsertType): Promise<QueryResult> => {
@@ -130,7 +132,7 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
.from(tables)
.where(eq(tables.databaseId, databaseId))
await tx.update(databaseModel).set({
numOfTables
}).where(eq(databaseModel.id, databaseId))
@@ -291,10 +293,10 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
await db.transaction(async (tx) => {
for (const operation of operations) {
if ( operation.type == "RENAME_DATABASE") {
if (operation.type == "RENAME_DATABASE") {
currentDatabaseId && await tx.update(databaseModel).set({
name : operation.chnages.name
}).where(eq(databaseModel.id , currentDatabaseId)) ;
name: operation.chnages.name
}).where(eq(databaseModel.id, currentDatabaseId));
}
else if (operation.type == "CREATE_TABLE") {
@@ -359,8 +361,8 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
const getDefaultPrimaryKeyType = useCallback(() => {
return data_types.find((dataType : DataType) => dataType.name == "bigint" )
} , [data_types ]) ;
return data_types.find((dataType: DataType) => dataType.name == "bigint")
}, [data_types]);
const databaseOpsValue = useMemo(() => ({
@@ -386,7 +388,7 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
editIndex,
deleteIndex,
editFieldIndices,
data_types ,
data_types,
grouped_data_types
}), [
createDatabase,
@@ -410,10 +412,11 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
editIndex,
deleteIndex,
editFieldIndices,
data_types ,
data_types,
grouped_data_types
]);
return (
<DatabaseDataContext.Provider value={{
@@ -422,10 +425,16 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
databases: databases as DatabaseType[],
isLoading,
isSwitchingDatabase,
getDefaultPrimaryKeyType ,
getDefaultPrimaryKeyType,
getField,
}}>
<DatabaseOperationsContext.Provider value={databaseOpsValue}>
{
!database && !isLoading &&
<>
{children}
</>
}
{
database && !isLoading && !isSwitchingDatabase &&
<DatabaseHistoryProvider>
@@ -33,13 +33,13 @@ export const SyncProvider: React.FC<SyncProviderProps> = ({ children }) => {
const [connector] = useState(new StackRenderConnector());
useEffect(() => {
powerSync.init();
powerSync.connect(connector);
(async () => {
await powerSync.execute("PRAGMA foreign_keys = ON;");
})()
await powerSync.execute("PRAGMA foreign_keys = ON;");
})();
}, [powerSync, connector])
+30 -2
View File
@@ -175,7 +175,35 @@ tbody[role="rowgroup"] tr td:first-child {
}
.cm-editor {
.cm-editor {
min-width: 100% !important;
}
.cm-scroller::-webkit-scrollbar {
height: 6px;
}
button[data-testid="remove"] {
top : 1px ;
position: relative;
}
button[data-testid="remove"]:after {
content: '×' ;
color : white;
font-size : 11px ;
position: relative;
top: -2px ;
min-width : 100% !important ;
}
button[data-testid="remove"] svg {
display: none ;
}
+26 -2
View File
@@ -1,4 +1,6 @@
import { FieldType } from "@/lib/schemas/field-schema"
import { FieldType } from "@/lib/schemas/field-schema"
import { Cardinality, RelationshipType } from "@/lib/schemas/relationship-schema"
import { TableType } from "@/lib/schemas/table-schema"
export const getRelationshipSourceAndTarget = (sourceTableId: string, sourceField: FieldType, targetTableId: string, targetField: FieldType) => {
if (sourceField.isPrimary || targetField.isPrimary) {
@@ -47,4 +49,26 @@ export const getRelationshipSourceAndTarget = (sourceTableId: string, sourceFiel
}
}
export const getForeignRelationships = (table: TableType): RelationshipType[] => {
let foreignRelationships = (table.sourceRelations ? table.sourceRelations?.filter((relationship: RelationshipType) =>
relationship.cardinality == Cardinality.many_to_one
) : []).map((relationship: RelationshipType) => {
return {
...relationship ,
sourceTable : relationship.targetTable ,
targetTable : relationship.sourceTable ,
sourceField : relationship.targetField ,
targetField : relationship.sourceField ,
}
});
foreignRelationships = foreignRelationships.concat(
table.targetRelations ? table.targetRelations?.filter((relationship: RelationshipType) =>
relationship.cardinality == Cardinality.one_to_many || relationship.cardinality == Cardinality.one_to_one
) : []);
return foreignRelationships;
}
+66 -55
View File
@@ -1,5 +1,6 @@
import { DatabaseDialect } from "@/lib/database";
import { DataType } from "@/lib/schemas/data-type-schema";
import { DatabaseType } from "@/lib/schemas/database-schema";
import { FieldType } from "@/lib/schemas/field-schema";
@@ -7,29 +8,40 @@ import { FieldIndexType } from "@/lib/schemas/field_index-schema";
import { IndexType } from "@/lib/schemas/index-schema";
import { Cardinality, RelationshipType } from "@/lib/schemas/relationship-schema";
import { TableType } from "@/lib/schemas/table-schema";
import { RenderableTable, SortableTable, toRenderableTable, toSortableTable } from "@/lib/table";
import { getForeignRelationships } from "@/utils/relationship";
import { orderTables } from "@/utils/tables";
export const DatabaseToAst = (database: DatabaseType, data_types: DataType[]) => {
let dbAst: any = [];
if (!data_types || data_types.length == 0)
return dbAst;
for (const table of database.tables) {
dbAst.push(TableToAst({
...table,
}, data_types));
if (table.indices && table.indices.length > 0)
for (const index of table.indices)
dbAst.push(IndexToAst({
...index,
fields: index.fieldIndices.map((fieldIndex: FieldIndexType) => table.fields.find((field: FieldType) => field.id == fieldIndex.fieldId)) as FieldType[]
}, table));
};
let renderableTables: RenderableTable[] = database.tables.map((table: TableType) => toRenderableTable(table, database));
let sortableTables: SortableTable[] = renderableTables.map((table: RenderableTable) => toSortableTable(table));
try {
const sortedTablesIds: string[] = orderTables(sortableTables)
const sortedTables: TableType[] = sortedTablesIds.map((id: string) =>
renderableTables.find((table: TableType) => table.id == id) as TableType
);
for (const relationship of database.relationships) {
dbAst.push(relationshipToAst(relationship))
}
//console.log (dbAst)
for (const table of sortedTables) {
dbAst.push(TableToAst(table, data_types));
if (table.indices && table.indices.length > 0)
for (const index of table.indices)
dbAst.push(IndexToAst({
...index,
fields: index.fieldIndices.map((fieldIndex: FieldIndexType) =>
table.fields.find((field: FieldType) => field.id == fieldIndex.fieldId)
) as FieldType[],
}, table));
};
} catch (error) {
console.log(error);
}
return dbAst;
}
@@ -47,12 +59,10 @@ export const TableToAst = (table: TableType, data_types: DataType[]) => {
}))
}] : [];
let foreignRelationships = getForeignRelationships(table);
const constraints: any[] = [...primaryKeysConstraints];
const foreignKeysConstraints = foreignRelationships.map((relationship: RelationshipType) => relationshipToAst(relationship));
const constraints: any[] = [...primaryKeysConstraints, ...foreignKeysConstraints];
const filed_definitions = table.fields.map((field: FieldType) => FieldToAst({
...field,
type: data_types.find((dataType: DataType) => dataType.id == field.typeId) as DataType,
@@ -84,8 +94,8 @@ export const FieldToAst = (field: FieldType, ignorePrimaryKey: boolean = false)
},
definition: {
dataType: field.type?.name?.toLocaleUpperCase(),
length : field.type.name == "varchar" && field.type.dialect == "mysql" ? 255 : null ,
length: field.type?.name == "varchar" && (field.type?.dialect == DatabaseDialect.MYSQL || field.type?.dialect == DatabaseDialect.MARIADB) ? 255 : null,
},
primary_key: field.isPrimary && !ignorePrimaryKey ? "primary key" : null,
@@ -112,52 +122,53 @@ export const IndexToAst = (index: IndexType, table: TableType) => {
}
export const relationshipToAst = (relationship: RelationshipType) => {
console.log(relationship);
let primaryKey: FieldType = relationship.sourceField;
let foreignKey: FieldType = relationship.targetField;
let sourceTable : TableType = relationship.sourceTable ;
let targetTable : TableType = relationship.targetTable ;
let sourceTable: TableType = relationship.sourceTable;
let targetTable: TableType = relationship.targetTable;
if (relationship.cardinality == Cardinality.many_to_one) {
primaryKey = relationship.targetField;
foreignKey = relationship.sourceField;
sourceTable = relationship.targetTable ;
targetTable = relationship.sourceTable ;
sourceTable = relationship.targetTable;
targetTable = relationship.sourceTable;
}
return {
table: [{
table: targetTable.name,
}],
type: "alter",
expr: [{
action: "add",
resource: "constraint",
type: "alter",
create_definitions: {
constraint: relationship.name ? relationship.name : null,
constraint_type: "FOREIGN KEY",
index: null,
keyword: null,
resource: "constraint",
definition: [{
column: foreignKey.name,
type: "column_ref"
}],
reference_definition: {
keyword: "references",
on_action: [],
table: [{ table: sourceTable.name }],
definition: [{
column: primaryKey.name,
type: "column_ref"
}],
}
constraint: null,
definition: [
{
type: "column_ref",
column: foreignKey.name,
}
}]
],
constraint_type: "FOREIGN KEY",
resource: "constraint",
reference_definition: {
definition: [
{
type: "column_ref",
column: primaryKey.name,
}
],
table: [
{
table: sourceTable.name,
}
],
keyword: "references",
on_action: []
}
}
}
+109 -18
View File
@@ -7,6 +7,7 @@ import ELK from "elkjs/lib/elk.bundled.js";
import { cloneField } from "./field";
import { v4 } from "uuid";
import { getTimestamp } from "./utils";
import { SortableTable } from "@/lib/table";
const elk = new ELK();
@@ -16,7 +17,7 @@ const adjustTablesPositions = async (
): Promise<TableType[]> => {
// Extract tables (same as before)
const tables: TableType[] = nodes.map((node: Node) => (
const tables: TableType[] = nodes.map((node: Node) => (
{ ...node.data.table as TableType }
)) as TableType[];
@@ -40,7 +41,7 @@ const adjustTablesPositions = async (
targets: [rel.targetTableId],
})),
};
console.log (graph.children ) ;
console.log(graph.children);
// Run ELK layout (async)
const layoutedGraph = await elk.layout(graph);
@@ -49,8 +50,8 @@ const adjustTablesPositions = async (
const node = layoutedGraph?.children?.find((n) => n.id === table.id);
if (node) {
// ELK positions are top-left, adjust to center like before
table.posX = node.x || 0 + (node.width / 2) + 112;
table.posY = node.y || 0 + (node.height / 2) + 75;
table.posX = node.x || 0 + (node.width / 2) + 112;
table.posY = node.y || 0 + (node.height / 2) + 75;
}
});
@@ -59,7 +60,7 @@ const adjustTablesPositions = async (
const isTablesOverlapping = (tableA: TableType, tableB: TableType) => {
@@ -69,7 +70,7 @@ const isTablesOverlapping = (tableA: TableType, tableB: TableType) => {
const tableBWidth: number = 224;
const tableBHeight: number = tableB.fields.length * 32 + 36;
const a = {
left: tableA.posX,
right: tableA.posX + tableAWidth,
@@ -90,7 +91,7 @@ const isTablesOverlapping = (tableA: TableType, tableB: TableType) => {
const getDefaultTableOverlapping = (table: TableType, tables: TableType[]): boolean => {
for (let index: number = 0; index < tables.length; index++) {
if ( table.id == tables[index].id) continue ;
if (table.id == tables[index].id) continue;
if (isTablesOverlapping(table, tables[index]))
return true;
}
@@ -99,25 +100,115 @@ const getDefaultTableOverlapping = (table: TableType, tables: TableType[]): bool
const cloneTable = ( table: TableType) : TableType => {
const cloneTable = (table: TableType): TableType => {
return {
...table ,
id : v4() ,
name : `${table.name}_copy` ,
fields : table.fields.map((field : FieldType) => cloneField(field)) ,
posX : table.posX - 360 ,
createdAt : getTimestamp()
...table,
id: v4(),
name: `${table.name}_copy`,
fields: table.fields.map((field: FieldType) => cloneField(field)),
posX: table.posX - 360,
createdAt: getTimestamp()
}
}
function orderTables(tables: SortableTable[]): string[] {
// Build adjacency list and in-degree count
const graph = new Map();
const inDegree = new Map();
// Initialize graph and in-degree map
tables.forEach(({ tableId, relationships }) => {
graph.set(tableId, new Set());
inDegree.set(tableId, 0);
});
// Populate the graph and in-degree map
tables.forEach(({ tableId, relationships }) => {
relationships.forEach(fk => {
if (graph.has(fk)) {
graph.get(fk).add(tableId);
inDegree.set(tableId, (inDegree.get(tableId) || 0) + 1);
}
});
});
// Find tables with no dependencies (in-degree = 0)
const queue: string[] = [];
inDegree.forEach((count, table) => {
if (count === 0) queue.push(table);
});
// Process tables in topological order
const sortedOrder: string[] = [];
while (queue.length > 0) {
const table = queue.shift();
sortedOrder.push(table as string);
// Reduce in-degree of dependent tables
graph.get(table).forEach((dependent: string) => {
inDegree.set(dependent, inDegree.get(dependent) - 1);
if (inDegree.get(dependent) === 0) {
queue.push(dependent);
}
});
}
// If not all tables are processed, a cycle exists
if (sortedOrder.length !== tables.length) {
const visited = new Set<string>();
const onStack = new Set<string>();
const path: string[] = [];
let cycle: string[] = [];
function dfs(node: string): boolean {
visited.add(node);
onStack.add(node);
path.push(node);
for (const neighbor of graph.get(node)!) {
if (!visited.has(neighbor)) {
if (dfs(neighbor)) return true;
} else if (onStack.has(neighbor)) {
// Found the cycle
const cycleStartIndex = path.indexOf(neighbor);
cycle = path.slice(cycleStartIndex);
cycle.push(neighbor); // close the loop
return true;
}
}
onStack.delete(node);
path.pop();
return false;
}
for (const node of graph.keys()) {
if (!visited.has(node)) {
if (dfs(node)) break;
}
}
throw {
success: false,
message: "Cycle detected",
cycle: cycle,
};
}
return sortedOrder;
}
export {
adjustTablesPositions,
getDefaultTableOverlapping ,
isTablesOverlapping ,
cloneTable
getDefaultTableOverlapping,
isTablesOverlapping,
cloneTable,
orderTables
}