mirror of
https://github.com/stackrender/stackrender.git
synced 2026-09-10 11:15:42 +00:00
Field Modifiers integrated , extra options added to the field settings based on the data type of that field
This commit is contained in:
@@ -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
@@ -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>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
|
||||
@@ -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 "
|
||||
/>
|
||||
|
||||
@@ -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)
|
||||
@@ -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) ||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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] ;
|
||||
}
|
||||
@@ -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',
|
||||
}
|
||||
@@ -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 ;
|
||||
|
||||
};
|
||||
@@ -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> { };
|
||||
@@ -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 }) => ({
|
||||
|
||||
@@ -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 ;
|
||||
|
||||
|
||||
}
|
||||
@@ -13,9 +13,7 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<BrowserRouter>
|
||||
<Provider>
|
||||
|
||||
<App />
|
||||
|
||||
</Provider>
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>,
|
||||
|
||||
@@ -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
|
||||
|
||||
+2
-2
@@ -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>
|
||||
)
|
||||
|
||||
+9
-70
@@ -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>
|
||||
|
||||
+416
@@ -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
@@ -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 ;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user