diff --git a/src/components/auto-complete/auto-complete.tsx b/src/components/auto-complete/auto-complete.tsx index a67c2fb..8c5ead2 100644 --- a/src/components/auto-complete/auto-complete.tsx +++ b/src/components/auto-complete/auto-complete.tsx @@ -1,5 +1,5 @@ -import { Autocomplete as HeroUiAutocomplete, AutocompleteItem } from "@heroui/react"; +import { Autocomplete as HeroUiAutocomplete, AutocompleteItem, AutocompleteSection } from "@heroui/react"; import { Key } from "react"; interface AutocompleteProps { @@ -10,29 +10,60 @@ interface AutocompleteProps { onSelectionChange?: (item: any) => void, placeholder?: string isDisabled?: boolean - selectedItem?: Key + selectedItem?: Key, + grouped?: boolean } +const headingClasses = + "flex w-full py-1.5 px-2 bg-default shadow-small rounded-small"; -const Autocomplete: React.FC = ({ items, label = "name", onSelectionChange, defaultSelection, placeholder, isDisabled, selectedItem }) => { +const Autocomplete: React.FC = ({ items, label = "name", onSelectionChange, defaultSelection, placeholder, isDisabled, selectedItem, grouped = false }) => { + + const onItemChange = (item: Key | null) => { onSelectionChange && onSelectionChange(item); } + if (!grouped) + return ( + + {(item: any) => {item[label]}} + + ) + if (grouped) { + return = ({ items, label = "name", onSe }} > - {(item: any) => {item[label]}} + { + items ? Object.keys(items).map((key: any) => ( + + { + items[key].map((item: any) => ( + {item[label]} + )) + } + + + )) : [] + } + + - ) + } } export default Autocomplete; \ No newline at end of file diff --git a/src/hooks/user-render-sql.tsx b/src/hooks/user-render-sql.tsx index 3160e38..9ccbef6 100644 --- a/src/hooks/user-render-sql.tsx +++ b/src/hooks/user-render-sql.tsx @@ -7,6 +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"; const parser = new Parser(); @@ -16,8 +17,11 @@ export const useRenderSql = (database: DatabaseType) => { const { data_types } = useDatabaseOperations(); useEffect(() => { - const dbAst: any = DatabaseToAst(database, data_types); - const formattedSqlCode = format(parser.sqlify(dbAst), { language: 'sql' }); // or 'mysql', 'postgresql', etc. + const dbAst: any = DatabaseToAst(database, data_types); + + const formattedSqlCode = format(parser.sqlify(dbAst , { + database : "MySQL" // getDatabaseByDialect(database.dialect).name + }), { language: 'sql' }); setSql( formattedSqlCode ); diff --git a/src/lib/database.ts b/src/lib/database.ts index fcaed01..8c74528 100644 --- a/src/lib/database.ts +++ b/src/lib/database.ts @@ -10,11 +10,11 @@ export interface DatabaseType { export const DBTypes: DatabaseType[] = [ { - name: "PostgreSql", + name: "Postgresql", dialect: "postgres", logo: "/postgresql_logo.png" }, { - name: "Mysql", + name: "MySQL", dialect: "mysql", logo: "/mysql_logo.png" }, diff --git a/src/lib/schemas/data-type-schema.ts b/src/lib/schemas/data-type-schema.ts index ec2ccad..49ea2f7 100644 --- a/src/lib/schemas/data-type-schema.ts +++ b/src/lib/schemas/data-type-schema.ts @@ -9,6 +9,7 @@ export const data_types = sqliteTable('data_types', { dialect: text("dialect", { enum: ["postgres", "mysql", "sqlite", "mariadb"], }).notNull().default("postgres"), + type: text('type').notNull() }); diff --git a/src/pages/database/database-page.tsx b/src/pages/database/database-page.tsx index b329087..8a32508 100644 --- a/src/pages/database/database-page.tsx +++ b/src/pages/database/database-page.tsx @@ -20,7 +20,7 @@ import { TableInsertType } from "@/lib/schemas/table-schema"; import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider"; import { useTableToNode } from "@/hooks/use-table-to-node"; import { useRelationshipToEdge } from "@/hooks/use-relationship-to-edge"; -import { RelationshipInsertType } from "@/lib/schemas/relationship-schema"; +import { Cardinality, RelationshipInsertType } from "@/lib/schemas/relationship-schema"; // Utils and constants import { v4 } from "uuid"; @@ -38,6 +38,8 @@ 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(); const DatabasePage: React.FC = () => { @@ -65,25 +67,29 @@ const DatabasePage: React.FC = () => { const edgeTypes = useMemo(() => ({ 'relationship-edge': Relationship }), []); // Called when a connection is made between fields - const onConnect = useCallback((connection: Connection) => { - const sourceFieldId: string | undefined = (connection.sourceHandle as string).split("_").pop(); - const targetFieldId: string = (connection.targetHandle as string).replace(TARGET_PREFIX, ""); + const onConnect = useCallback(async (connection: Connection) => { + const sourceId: string | undefined = (connection.sourceHandle as string).split("_").pop(); + const targetId: string = (connection.targetHandle as string).replace(TARGET_PREFIX, ""); - const sourceField: FieldType | undefined = getField(connection.source, sourceFieldId as string); - const targetField: FieldType | undefined = getField(connection.target, targetFieldId); + const sourceField: FieldType = getField(connection.source, sourceId as string) as FieldType; + const targetField: FieldType = getField(connection.target, targetId) as FieldType; + + + 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) { - createRelationship({ + await createRelationship({ id: v4(), - sourceTableId: connection.source, - targetTableId: connection.target, + sourceTableId, + targetTableId, sourceFieldId, - targetFieldId + targetFieldId, + } as RelationshipInsertType); // Add edge to the diagram - setEdges((eds) => addEdge(connection, eds)); + //setEdges((eds) => addEdge(connection, eds)); } else { // Show error toast if invalid relationship addToast({ diff --git a/src/pages/database/db-controller/relationship-controller/relationship-accordion-item/relationship-accordion-body.tsx b/src/pages/database/db-controller/relationship-controller/relationship-accordion-item/relationship-accordion-body.tsx index a1f7b57..59f7dcf 100644 --- a/src/pages/database/db-controller/relationship-controller/relationship-accordion-item/relationship-accordion-body.tsx +++ b/src/pages/database/db-controller/relationship-controller/relationship-accordion-item/relationship-accordion-body.tsx @@ -2,34 +2,33 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/tooltip/tooltip"; import { Cardinality, RelationshipInsertType, RelationshipType } from "@/lib/schemas/relationship-schema"; import { useDatabaseOperations } from "@/providers/database-provider/database-provider"; + import { Button, Select, SelectItem, SharedSelection } from "@heroui/react"; import { ChevronsLeftRightEllipsis, FileMinus2, FileOutput, Trash2 } from "lucide-react"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; - interface RelationshipAccordionBodyProps { relationship: RelationshipType } - - const RelationshipAccordionBody: React.FC = ({ relationship }) => { const [cardinality, setCardinality] = useState(new Set([relationship.cardinality])); const { editRelationship, deleteRelationship } = useDatabaseOperations(); const { t } = useTranslation(); - - const changeCardinality = (keys: SharedSelection) => { - if (keys.anchorKey != relationship.cardinality) + if (keys.anchorKey != relationship.cardinality) { + editRelationship({ id: relationship.id, - cardinality: keys.anchorKey as Cardinality + cardinality: keys.anchorKey as Cardinality, + } as RelationshipInsertType); + } setCardinality(keys as any); } diff --git a/src/pages/database/db-controller/sql-preview.tsx b/src/pages/database/db-controller/sql-preview.tsx index f85232b..7450686 100644 --- a/src/pages/database/db-controller/sql-preview.tsx +++ b/src/pages/database/db-controller/sql-preview.tsx @@ -11,23 +11,27 @@ import { Parser } from "node-sql-parser"; const parser = new Parser(); const code = ` --- Addresses table with a foreign key to users -CREATE TABLE addresses ( - id INTEGER PRIMARY KEY, - user_id INT NOT NULL, - street VARCHAR(255), - city VARCHAR(100), - state VARCHAR(100), - postal_code VARCHAR(20), - country VARCHAR(100), - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE +CREATE TABLE \`users\` ( + id BIGINT NOT NULL PRIMARY KEY UNIQUE, + name VARCHAR(255) NOT NULL, + lastname VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL UNIQUE, + password VARCHAR(255) NOT NULL, + profile_url VARCHAR(255), + age SMALLINT, + birthday DATE, + created_at TIMESTAMP NOT NULL ); --- Indexes to improve lookup performance -CREATE UNIQUE INDEX idx_addresses_user_id ON addresses(user_id); -CREATE INDEX idx_addresses_city ON addresses(city , country); - +CREATE TABLE \`addresses\` ( + id BIGINT NOT NULL PRIMARY KEY UNIQUE, + street_line VARCHAR(255) NOT NULL, + latitude FLOAT, + longitude FLOAT, -- fixed spelling + state_id BIGINT NOT NULL, + user_id BIGINT NOT NULL, + FOREIGN KEY (\`user_id\`) REFERENCES \`users\` (\`id\`) +); ` @@ -40,7 +44,9 @@ const SqlPreview: React.FC = ({ }) => { const {resolvedTheme} = useTheme(); useEffect(() => { - const ast = parser.astify(code ) + const ast = parser.astify(code , { + database : "Mysql" + } ) console.log (ast) }, []) diff --git a/src/pages/database/db-controller/tables-controller/table-accordion-item/field/field-item.tsx b/src/pages/database/db-controller/tables-controller/table-accordion-item/field/field-item.tsx index 443c693..0d3e53d 100644 --- a/src/pages/database/db-controller/tables-controller/table-accordion-item/field/field-item.tsx +++ b/src/pages/database/db-controller/tables-controller/table-accordion-item/field/field-item.tsx @@ -21,7 +21,7 @@ const FieldItem: React.FC = ({ field }) => { const [fieldName, setFieldName] = useState(field.name); const [popOverOpen, setPopOverOpen] = useState(false); - const { data_types } = useDatabaseOperations(); + const { data_types , grouped_data_types } = useDatabaseOperations(); const { deleteField, editField } = useDatabaseOperations(); const [note, setNote] = useState(field.note as string | undefined); @@ -109,9 +109,9 @@ const FieldItem: React.FC = ({ field }) => { }} /> diff --git a/src/pages/database/db-controller/tables-controller/tables-controller.tsx b/src/pages/database/db-controller/tables-controller/tables-controller.tsx index 66b1345..5471343 100644 --- a/src/pages/database/db-controller/tables-controller/tables-controller.tsx +++ b/src/pages/database/db-controller/tables-controller/tables-controller.tsx @@ -12,9 +12,6 @@ import { useDiagram } from "@/providers/diagram-provider/diagram-provider"; import { useReactFlow } from "@xyflow/react"; import SqlPreview from "../sql-preview"; - - - interface Props { } const PADDING_X = 40; const PADDING_Y = 80; diff --git a/src/providers/database-provider/database-context.ts b/src/providers/database-provider/database-context.ts index 2914541..5b0d9a5 100644 --- a/src/providers/database-provider/database-context.ts +++ b/src/providers/database-provider/database-context.ts @@ -27,6 +27,7 @@ interface DatabaseDataContextType { interface DatabaseOperationsContextType { data_types: DataType[], + grouped_data_types : any ; // database operations createDatabase: (database: DatabaseInsertType) => Promise, editDatabase: (database: DatabaseInsertType) => Promise, diff --git a/src/providers/database-provider/database-provider.tsx b/src/providers/database-provider/database-provider.tsx index 922a4a3..9851189 100644 --- a/src/providers/database-provider/database-provider.tsx +++ b/src/providers/database-provider/database-provider.tsx @@ -11,7 +11,7 @@ import { RelationshipInsertType, relationships } from "@/lib/schemas/relationshi import DatabaseHistoryProvider from "../database-history/database-history-provider"; import { DBDiffOperation } from "@/utils/database"; import { DatabaseInsertType, DatabaseType, databases as databaseModel } from "@/lib/schemas/database-schema"; -import { getTimestamp } from "@/utils/utils"; +import { getTimestamp, groupBy } from "@/utils/utils"; import { IndexInsertType, indices } from "@/lib/schemas/index-schema"; import { field_indices } from "@/lib/schemas/field_index-schema"; import { v4 } from "uuid"; @@ -87,6 +87,12 @@ const DatabaseProvider: React.FC = ({ children }) => { 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]) + + // Auto-select first database if none is selected useEffect(() => { @@ -380,7 +386,8 @@ const DatabaseProvider: React.FC = ({ children }) => { editIndex, deleteIndex, editFieldIndices, - data_types + data_types , + grouped_data_types }), [ createDatabase, editDatabase, @@ -403,7 +410,8 @@ const DatabaseProvider: React.FC = ({ children }) => { editIndex, deleteIndex, editFieldIndices, - data_types + data_types , + grouped_data_types ]); return ( diff --git a/src/utils/field.ts b/src/utils/field.ts index 6698e63..725dc15 100644 --- a/src/utils/field.ts +++ b/src/utils/field.ts @@ -1,6 +1,6 @@ import { FieldType } from "@/lib/schemas/field-schema"; import { v4 } from "uuid"; - + @@ -17,13 +17,13 @@ export const getNextSequence = (fields: FieldType[]): number => { -export const cloneField = ( field : FieldType) : FieldType => { +export const cloneField = (field: FieldType): FieldType => { return { - ...field , - id : v4() , - } as FieldType ; + ...field, + id: v4(), + } as FieldType; } - \ No newline at end of file + diff --git a/src/utils/relationship.ts b/src/utils/relationship.ts new file mode 100644 index 0000000..d7225fd --- /dev/null +++ b/src/utils/relationship.ts @@ -0,0 +1,50 @@ +import { FieldType } from "@/lib/schemas/field-schema" + +export const getRelationshipSourceAndTarget = (sourceTableId: string, sourceField: FieldType, targetTableId: string, targetField: FieldType) => { + if (sourceField.isPrimary || targetField.isPrimary) { + if (sourceField.isPrimary) { + return { + sourceTableId: sourceTableId, + targetTableId: targetTableId, + sourceFieldId: sourceField.id, + targetFieldId: targetField.id, + } + } else { + return { + + sourceTableId: targetTableId, + targetTableId: sourceTableId, + sourceFieldId: targetField.id, + targetFieldId: sourceField.id, + } + } + } + else if (sourceField.unique || targetField.unique) { + if (sourceField.unique) { + return { + + sourceTableId: sourceTableId, + targetTableId: targetTableId, + sourceFieldId: sourceField.id, + targetFieldId: targetField.id, + } + } else { + return { + + sourceTableId: targetTableId, + targetTableId: sourceTableId, + sourceFieldId: targetField.id, + targetFieldId: sourceField.id, + } + } + } else { + return { + sourceTableId: sourceTableId, + targetTableId: targetTableId, + sourceFieldId: sourceField.id, + targetFieldId: targetField.id, + } + } +} + + \ No newline at end of file diff --git a/src/utils/render/parsers/database_to_ast.ts b/src/utils/render/parsers/database_to_ast.ts index 20be177..3dd3990 100644 --- a/src/utils/render/parsers/database_to_ast.ts +++ b/src/utils/render/parsers/database_to_ast.ts @@ -5,7 +5,7 @@ import { DatabaseType } from "@/lib/schemas/database-schema"; import { FieldType } from "@/lib/schemas/field-schema"; import { FieldIndexType } from "@/lib/schemas/field_index-schema"; import { IndexType } from "@/lib/schemas/index-schema"; -import { RelationshipType } from "@/lib/schemas/relationship-schema"; +import { Cardinality, RelationshipType } from "@/lib/schemas/relationship-schema"; import { TableType } from "@/lib/schemas/table-schema"; @@ -16,8 +16,6 @@ export const DatabaseToAst = (database: DatabaseType, data_types: DataType[]) => for (const table of database.tables) { dbAst.push(TableToAst({ ...table, - sourceRelations: database.relationships.filter((relationship: RelationshipType) => relationship.sourceTableId == table.id), - targetRelations: database.relationships.filter((relationship: RelationshipType) => relationship.targetTableId == table.id), }, data_types)); if (table.indices && table.indices.length > 0) @@ -28,55 +26,37 @@ export const DatabaseToAst = (database: DatabaseType, data_types: DataType[]) => }, table)); }; + for (const relationship of database.relationships) { + dbAst.push(relationshipToAst(relationship)) + } + //console.log (dbAst) return dbAst; } export const TableToAst = (table: TableType, data_types: DataType[]) => { - const constraints: any[] = table.sourceRelations ? table.sourceRelations?.map((relationship: RelationshipType) => { - return { - constraint_type: "FOREIGN KEY", - resource: "constraint", - definition: [ - { - type: "column_ref", - column: relationship.sourceField.name, + const primaryKeys: FieldType[] = table.fields.filter((field: FieldType) => field.isPrimary); + const multiPrimaryKeys: boolean = primaryKeys.length > 1; + const primaryKeysConstraints: any[] | undefined = multiPrimaryKeys ? [{ + constraint_type: "primary key", + resource: "constraint", + definition: primaryKeys.map((field: FieldType) => ({ + column: field.name, + type: "column_ref" + })) + }] : []; - } - ], - reference_definition: { - on_action: [ - { - "type": "on delete", - "value": { - "type": "origin", - "value": "cascade" - } - } - ], - keyword: "references", - table: [{ - table: relationship.targetTable.name, - }], - definition: [ - { - type: "column_ref", - column: relationship.targetField.name, - } - ], - } - } - }) : []; + + + + const constraints: any[] = [...primaryKeysConstraints]; const filed_definitions = table.fields.map((field: FieldType) => FieldToAst({ ...field, type: data_types.find((dataType: DataType) => dataType.id == field.typeId) as DataType, - sourceRelations: table.sourceRelations?.filter((relationship: RelationshipType) => relationship.sourceFieldId == field.id), - targetRelations: table.targetRelations?.filter((relationship: RelationshipType) => relationship.targetFieldId == field.id), - - })); + }, multiPrimaryKeys)); return { keyword: "table", type: "create", @@ -85,12 +65,8 @@ export const TableToAst = (table: TableType, data_types: DataType[]) => { }], create_definitions: [...filed_definitions, ...constraints] } - } - - -export const FieldToAst = (field: FieldType) => { - +export const FieldToAst = (field: FieldType, ignorePrimaryKey: boolean = false) => { return { column: { type: "column_ref", @@ -108,9 +84,11 @@ export const FieldToAst = (field: FieldType) => { }, definition: { dataType: field.type?.name?.toLocaleUpperCase(), + length : field.type.name == "varchar" && field.type.dialect == "mysql" ? 255 : null , + }, - primary_key: field.isPrimary ? "primary key" : null, + primary_key: field.isPrimary && !ignorePrimaryKey ? "primary key" : null, resource: "column" } } @@ -124,10 +102,62 @@ export const IndexToAst = (index: IndexType, table: TableType) => { type: "create", table: { table: table.name }, keyword: "index", + on_kw: "on", index_type: index.unique ? "unique" : null, index_columns: index.fields.map((field: FieldType) => ({ column: field.name, type: "column_ref" })) } +} + + +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 ; + + if (relationship.cardinality == Cardinality.many_to_one) { + primaryKey = relationship.targetField; + foreignKey = relationship.sourceField; + 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" + }], + } + } + }] + } } \ No newline at end of file diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 1840d3c..238a4b0 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -52,12 +52,24 @@ function excludeFields( } - + + +function groupBy(array: any[], key: string) { + return array.reduce((acc, item) => { + const groupKey = item[key]; + if (!acc[groupKey]) { + acc[groupKey] = []; + } + acc[groupKey].push(item); + return acc; + }, {}); +} + export { areArraysEqual, getTimestamp, - excludeFields , - + excludeFields, + groupBy } \ No newline at end of file