Implement data validation, add identifier quoting to the generated SQL & fix prefered language lost after refreching

This commit is contained in:
KarimTamani
2026-09-13 18:29:50 +01:00
parent e169b84ade
commit 456b6c792c
46 changed files with 883 additions and 312 deletions
+7 -7
View File
@@ -19,7 +19,6 @@ import {
PopoverTrigger,
} from "@/components/ui/popover"
interface ComboboxProps {
items: any[]
@@ -28,13 +27,14 @@ interface ComboboxProps {
selectedItem?: string;
onSelectionChange?: (id: string) => void;
className?: string;
isDisabled?: boolean
isDisabled?: boolean;
valueLength?: number
}
export const Combobox = (props: ComboboxProps) => {
const { items, label = "label", placeholder, onSelectionChange, selectedItem, className, isDisabled } = props;
const { items, label = "label", placeholder, onSelectionChange, selectedItem, className, isDisabled , valueLength } = props;
const [query, setQuery] = React.useState<string>("");
const [open, setOpen] = React.useState(false)
@@ -51,10 +51,10 @@ export const Combobox = (props: ComboboxProps) => {
React.useEffect(() => {
if (open)
setQuery("");
setQuery("");
}, [open])
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild disabled={isDisabled}>
@@ -64,10 +64,10 @@ export const Combobox = (props: ComboboxProps) => {
aria-expanded={open}
className={cn("justify-between", className)}
>
<label className="truncate">
<label className="truncate ">
{
value
? items.find((item) => item.id === value)?.[label]
? (items.find((item) => item.id === value)?.[label] as string)?.substring(0 , valueLength)
: (placeholder ? placeholder : "Select items...")}
</label>
<ChevronsUpDownIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
+1 -1
View File
@@ -85,7 +85,7 @@ export function CommandMenu() {
</CommandGroup>
))}
<CommandSeparator />
<CommandGroup heading='Theme'>
<CommandGroup heading={t("menu.theme")}>
<CommandItem onSelect={() => runCommand(() => setTheme('light'))}>
<IconSun /> <span>{t("menu.light")}</span>
</CommandItem>
+19 -8
View File
@@ -11,33 +11,44 @@ import {
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
import { cn } from "@/lib/utils"
interface DatePickerProps {
value?: Date | undefined;
onValueChange?: (value?: Date) => void
onValueChange?: (value?: Date) => void;
noLabel?: boolean;
className?: string;
placeholder?: string ;
}
const DatePicker: React.FC<DatePickerProps> = ({value, onValueChange}) => {
const DatePicker: React.FC<DatePickerProps> = ({ value, onValueChange, noLabel = false, className , placeholder = "Pick a date"}) => {
const [open, setOpen] = React.useState(false)
const [date, setDate] = React.useState<Date | undefined>(value)
React.useEffect(() => {
setDate (value) ;
} , [ value])
React.useEffect(() => {
onValueChange && onValueChange(date)
}, [date])
}, [date])
return (
<div className="flex flex-col gap-3">
<Label htmlFor="date" className="px-1">
Date of birth
</Label>
{
!noLabel &&
<Label htmlFor="date" className="px-1">
{placeholder}
</Label>
}
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
id="date"
className="w-full justify-between font-normal"
className={cn("w-full justify-between font-normal" , className)}
>
{date ? date.toLocaleDateString() : "Select date"}
{date ? date.toLocaleDateString() : placeholder }
<ChevronDownIcon />
</Button>
</PopoverTrigger>
@@ -18,8 +18,8 @@ import { TableInsertType } from "@/lib/schemas/table-schema";
// Custom context providers and hooks
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 { useTableToNode } from "@/features/database/hooks/use-table-to-node";
import { useRelationshipToEdge } from "@/features/database/hooks/use-relationship-to-edge";
import { RelationshipInsertType } from "@/lib/schemas/relationship-schema";
// Utils and constants
@@ -33,9 +33,9 @@ import { AlertTriangle, Menu } from "lucide-react";
import { adjustTablesPositions } from "@/utils/tables";
//import DatabaseControlButtons from "./database-control-buttons";
import { FieldType } from "@/lib/schemas/field-schema";
import useHighlightedEdges from "@/hooks/use-highlighted-edges";
import useHighlightedEdges from "@/features/database/hooks/use-highlighted-edges";
import { useTranslation } from "react-i18next";
import useOverlappingTables from "@/hooks/use-overlapping-tables";
import useOverlappingTables from "@/features/database/hooks/use-overlapping-tables";
import { getRelationshipSourceAndTarget } from "@/utils/relationship";
@@ -73,7 +73,7 @@ const DatabaseDiagram: React.FC = () => {
// Diagram-related state (e.g. connection in progress)
const { setIsConnectionInProgress, cardinalityStyle, showController, openController } = useDiagramOps();
const raise = useToast() ;
const raise = useToast();
// Destructure tables and relationships from database
const { tables, relationships } = database || { tables: [], relationships: [] };
@@ -146,10 +146,10 @@ const DatabaseDiagram: React.FC = () => {
} else {
raise(
raise(
t("db_controller.invalid_relationship.title"),
t("db_controller.invalid_relationship.description"),
"ERROR"
"ERROR"
)
}
@@ -215,7 +215,7 @@ const DatabaseDiagram: React.FC = () => {
// Convert tables and relationships into flow elements
useTableToNode(tables);
useTableToNode(tables, database?.dialect);
useRelationshipToEdge(relationships);
useHighlightedEdges(nodes, relationships, edges);
const { isOverlapping, puls } = useOverlappingTables(tables);
@@ -122,7 +122,7 @@ const RelationshipController: React.FC = ({ }) => {
>
{relationships.map((relationship: RelationshipType) => (
<>
<RelationshipAccordionItem relationship={relationship} key={relationship.id} />
<RelationshipAccordionItem relationship={relationship} key={relationship.id} dialect={database?.dialect}/>
<Separator className="my-1" />
</>
))}
@@ -79,8 +79,8 @@ const RelationshipAccordionContent: React.FC<RelationshipAccordionContentProps>
return (
<div className="w-full p-2 space-y-4">
<div className="flex">
<div className="w-full space-y-1">
<div className="flex gap-2">
<div className="space-y-1 w-full truncate">
<Label >
{t("db_controller.source_table")}
</Label>
@@ -96,7 +96,7 @@ const RelationshipAccordionContent: React.FC<RelationshipAccordionContentProps>
</Tooltip>
</div>
<div className="w-full space-y-1">
<div className="space-y-1 w-full truncate">
<Label >
{t("db_controller.referenced_table")}
</Label>
@@ -6,17 +6,20 @@ import {
import { RelationshipType } from "@/lib/schemas/relationship-schema";
import RelationshipAccordionTrigger from "./relationship-accordion-trigger";
import RelationshipAccordionContent from "./relationship-accordion-content";
import { DatabaseDialect } from "@/lib/database";
interface RelationshipAccordionItemProps {
relationship: RelationshipType
relationship: RelationshipType ,
dialect? : DatabaseDialect
}
const RelationshipAccordionItem: React.FC<RelationshipAccordionItemProps> = ({ relationship }) => {
const RelationshipAccordionItem: React.FC<RelationshipAccordionItemProps> = ({ relationship , dialect }) => {
return (
<AccordionItem value={relationship.id} className="border-none" id={relationship.id}>
<RelationshipAccordionTrigger relationship={relationship} />
<RelationshipAccordionTrigger relationship={relationship} dialect = { dialect} />
<AccordionContent className="flex flex-col gap-4 text-balance">
<RelationshipAccordionContent
relationship={relationship}
@@ -9,17 +9,22 @@ import hash from "object-hash";
import { getDefaultRelationshipName } from "@/utils/relationship";
import { AccordionTrigger } from "@/components/ui/accordion";
import { Button } from "@/components/ui/button";
import { IconCheck, IconDotsVertical, IconFocus2, IconPencil, IconTrash } from "@tabler/icons-react";
import { IconAlertTriangle, IconCheck, IconDotsVertical, IconFocus2, IconPencil, IconTrash } from "@tabler/icons-react";
import { Input } from "@/components/ui/input";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
import { DatabaseDialect } from "@/lib/database";
import { useDatabaseInvalidIdentifier } from "@/features/database/hooks/use-invalid-identifier";
import { cn } from "@/lib/utils";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
interface RelationshipAccordionTriggerProps {
isOpen?: boolean;
relationship: RelationshipType
relationship: RelationshipType ,
dialect?: DatabaseDialect
}
const RelationshipAccordionTrigger: React.FC<RelationshipAccordionTriggerProps> = ({ isOpen, relationship }) => {
const RelationshipAccordionTrigger: React.FC<RelationshipAccordionTriggerProps> = ({ isOpen, relationship , dialect }) => {
const defaultName: string = useMemo(() => {
return getDefaultRelationshipName(relationship);
}, [relationship])
@@ -30,20 +35,27 @@ const RelationshipAccordionTrigger: React.FC<RelationshipAccordionTriggerProps>
const { editRelationship, deleteRelationship } = useDatabaseOperations();
const { t } = useTranslation();
const [name, setName] = useState<string>(relationship.name ? relationship.name : defaultName);
const { focusOnRelationship } = useDiagramOps();
const { identifierError , showIdentifierError, identifierMaxLength } = useDatabaseInvalidIdentifier(name, dialect, "constraint");
const editRelationshipName = () => {
if (identifierError == "empty") {
setName(relationship.name ? relationship.name : defaultName);
setEditMode(false);
return;
}
if (relationship.name || name.trim().toLocaleLowerCase() != defaultName)
editRelationship({
id: relationship.id,
name
name : name.trim()
} as RelationshipInsertType);
setEditMode(false);
}
const onDeleteRelationship = () => {
deleteRelationship(relationship.id);
@@ -60,7 +72,9 @@ const RelationshipAccordionTrigger: React.FC<RelationshipAccordionTriggerProps>
<>
<div className="w-full flex">
<label
className="py-2 !truncate text-sm cursor-pointer max-w-80"
className={cn("py-2 truncate text-sm cursor-pointer max-w-50 ", {
"text-destructive": identifierError != null
})}
>
{relationship.name ? relationship.name : defaultName}
</label>
@@ -79,11 +93,28 @@ const RelationshipAccordionTrigger: React.FC<RelationshipAccordionTriggerProps>
<IconFocus2 className="size-4 text-muted-foreground " />
</Button>
</div>
{
identifierError &&
<Tooltip>
<TooltipTrigger asChild>
<Button variant={"ghost"} size="icon" className="size-8 shrink-0 hover:bg-destructive/10 hover:border-destructive/30 dark:hover:bg-destructive/10 dark:hover:border-destructive/30" onClick={(event: any) => {
event.stopPropagation();
showIdentifierError();
setEditMode(true);
}} >
<IconAlertTriangle className="size-4 text-destructive " />
</Button>
</TooltipTrigger>
<TooltipContent className="bg-destructive fill-destructive [&_svg]:bg-destructive [&_svg]:fill-destructive">
{t("db_controller.validation.error_message")}
</TooltipContent>
</Tooltip>
}
</>
}
{
editMode && <>
<Input
<Input
placeholder={"Relationship name"}
value={name}
onChange={(event: any) => setName(event.target.value)}
@@ -91,8 +122,12 @@ const RelationshipAccordionTrigger: React.FC<RelationshipAccordionTriggerProps>
autoFocus
type="text"
onClick={(event) => event.stopPropagation()}
className="h-8"
onKeyDown={(e: any) => {
aria-invalid={identifierError != null}
className={cn("h-8", {
"text-destructive": identifierError != null
})}
maxLength={identifierMaxLength}
onKeyDown={(e: any) => {
if (e.key === "Enter") {
e.preventDefault();
editRelationshipName();
@@ -9,7 +9,7 @@ import { v4 } from "uuid";
import { useDiagram } from "@/providers/diagram-provider/diagram-provider";
import { useReactFlow } from "@xyflow/react";
import SqlPreview from "../sql-preview";
import EmptyList from "@/components/empty-list";
import EmptyList from "@/components/empty-list";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { IconListDetails, IconPlus } from "@tabler/icons-react";
@@ -38,9 +38,9 @@ const TablesController: React.FC = ({ }) => {
const { tables: allTables } = database || { tables: [] };
const [tables, setTables] = useState<TableType[]>(allTables);
const { t } = useTranslation();
const [showSqlPreview, setShowSqlPreview] = useState<boolean>(false);
const { focusedTableId , setFocusedTableId} = useDiagram();
const { focusedTableId, setFocusedTableId } = useDiagram();
const nameRef: Ref<HTMLInputElement> = useRef<HTMLInputElement>(null);
const sensors = useSensors(
@@ -100,7 +100,7 @@ const TablesController: React.FC = ({ }) => {
}, [database, tables, getViewport, getInteger]);
useEffect(() => {
if (focusedTableId) {
if (focusedTableId) {
const accordionItem = document.getElementById(focusedTableId)
if (accordionItem)
accordionItem?.scrollIntoView({
@@ -177,7 +177,7 @@ const TablesController: React.FC = ({ }) => {
type="single"
collapsible
className="w-full"
value={focusedTableId}
value={focusedTableId}
onValueChange={setFocusedTableId}
>
<DndContext
@@ -193,7 +193,7 @@ const TablesController: React.FC = ({ }) => {
{tables.map((table: TableType) => (
<>
<TableAccordionItem table={table} key={table.id} />
<TableAccordionItem table={table} key={table.id} dialect={database?.dialect} />
<Separator className="my-1" />
</>
))}
@@ -277,6 +277,7 @@ const fieldDefautlValue: React.FC<FieldDefaultValueProps> = ({ field }) => {
ref={defaultValueRef}
aria-invalid={!defaultValueValidation.isValid}
onChange={defaultValueChange}
maxLength={255}
defaultValue={field.defaultValue as string}
onBlur={saveDefaultValue as any}
placeholder={t("db_controller.field_settings.value")}
@@ -307,14 +308,12 @@ const fieldDefautlValue: React.FC<FieldDefaultValueProps> = ({ field }) => {
{
(defaultValueType.select) &&
<Select
aria-label="value"
value={selectedValues as any}
onValueChange={enumValueChange as any}
>
<SelectTrigger id="charset" className="w-full flex ">
<SelectTrigger className="w-full flex ">
<SelectValue placeholder={t('db_controller.field_settings.pick_value')} />
</SelectTrigger>
@@ -322,13 +321,11 @@ const fieldDefautlValue: React.FC<FieldDefaultValueProps> = ({ field }) => {
<SelectItem value={"none"} >
{t("db_controller.field_settings.no_default")}
</SelectItem>
{
values.map((value: string) => (
<SelectItem
key={value}
value={value}
>
value={value}>
{value}
</SelectItem>
))
@@ -347,7 +344,7 @@ const fieldDefautlValue: React.FC<FieldDefaultValueProps> = ({ field }) => {
onValueChange={enumValueChange as any}
>
<SelectTrigger id="charset" className="w-full flex ">
<SelectTrigger className="w-full flex ">
<SelectValue placeholder={t('db_controller.field_settings.pick_value')} />
</SelectTrigger>
@@ -372,7 +369,7 @@ const fieldDefautlValue: React.FC<FieldDefaultValueProps> = ({ field }) => {
}
onValueChange={enumValueChange}
defaultValue={selectedValues as any}
placeholder="Select fields..."
placeholder={t("db_controller.field_settings.multi_select_placeholder")}
variant={"secondary"}
hideSelectAll
/>
@@ -408,6 +405,7 @@ const fieldDefautlValue: React.FC<FieldDefaultValueProps> = ({ field }) => {
<DatePicker
value={defaultDateTime}
onValueChange={saveDefaultDateTime}
placeholder={t("db_controller.field_settings.date_placeholder")}
/>
}
@@ -415,7 +413,7 @@ const fieldDefautlValue: React.FC<FieldDefaultValueProps> = ({ field }) => {
timeSelection == TimeDefaultValues.CUSTOM && field.type.name == "time" &&
<>
<Label htmlFor="time">
Time
{t("db_controller.field_settings.time")}
</Label>
<Input
id="time"
@@ -424,6 +422,7 @@ const fieldDefautlValue: React.FC<FieldDefaultValueProps> = ({ field }) => {
defaultValue={defaultDateTime}
onChange={(event) => setDefaultDateTime(event.target.value)}
onBlur={saveDefaultDateTime as any}
maxLength={255}
className="bg-background appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none"
/>
</>
@@ -5,11 +5,11 @@ import { Settings2 } from "lucide-react";
import { useTranslation } from "react-i18next";
import { CSS } from "@dnd-kit/utilities";
import { FieldType } from "@/lib/schemas/field-schema";
import { Key, useEffect, useState } from "react";
import { Key, useEffect, useRef, useState } from "react";
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
import FieldSetting from "./field-setting";
import { DataType } from "@/lib/schemas/data-type-schema";
import { IconGripVertical, IconKey, IconKeyframe } from "@tabler/icons-react";
import { IconAlertTriangle, IconGripVertical, IconKey, IconKeyframe } from "@tabler/icons-react";
import { Input } from "@/components/ui/input";
import { Combobox } from "@/components/combobox";
import { Toggle } from "@/components/ui/toggle";
@@ -17,14 +17,19 @@ import { Toggle } from "@/components/ui/toggle";
import { TooltipTrigger, Tooltip, TooltipContent } from "@/components/ui/tooltip";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Button } from "@/components/ui/button";
import { DatabaseDialect } from "@/lib/database";
import { useDatabaseInvalidIdentifier } from "@/features/database/hooks/use-invalid-identifier";
import { cn } from "@/lib/utils";
interface Props {
field: FieldType
field: FieldType;
dialect?: DatabaseDialect
}
const FieldItem: React.FC<Props> = ({ field }) => {
const FieldItem: React.FC<Props> = ({ field, dialect }) => {
const [fieldName, setFieldName] = useState<string>(field.name);
@@ -34,6 +39,9 @@ const FieldItem: React.FC<Props> = ({ field }) => {
const [selectedType, setSelectedType] = useState<string | undefined>(field.typeId as string | undefined);
const { t } = useTranslation();
const { identifierError, showIdentifierError, identifierMaxLength } = useDatabaseInvalidIdentifier(fieldName, dialect, "column");
const inputRef = useRef<HTMLInputElement>(null);
const { attributes, listeners, setNodeRef, transform } = useSortable({ id: field.id });
const style = {
@@ -49,10 +57,17 @@ const FieldItem: React.FC<Props> = ({ field }) => {
}, [field.typeId])
const saveFieldName = () => {
if (identifierError == "empty") {
setFieldName(field.name);
return;
}
editField({
id: field.id,
name: fieldName
name: fieldName.trim()
} as FieldType);
}
const updateFieldType = (key: Key | null) => {
@@ -93,28 +108,59 @@ const FieldItem: React.FC<Props> = ({ field }) => {
<IconGripVertical className="size-4 text-muted-foreground hover:text-foreground cursor-move shrink-0" />
</div>
<div className="flex gap-2 w-full">
<Input
aria-label={t("db_controller.name")}
placeholder={t("db_controller.name")}
value={fieldName}
onChange={(event: any) => setFieldName(event.target.value)}
onBlur={saveFieldName}
className=" flex flex-1 !bg-transparent"
onKeyDown={(e : any) => {
if (e.key === "Enter") {
e.preventDefault();
saveFieldName() ;
e.target.blur() ;
}
}}
/>
<div className="flex flex-1 relative items-center min-w-0 ">
<Input
aria-label={t("db_controller.name")}
placeholder={t("db_controller.name")}
value={fieldName}
onChange={(event: any) => setFieldName(event.target.value)}
onBlur={saveFieldName}
maxLength={identifierMaxLength}
aria-invalid={identifierError != null}
ref={inputRef}
onKeyDown={(e: any) => {
if (e.key === "Enter") {
e.preventDefault();
saveFieldName();
e.target.blur();
}
}}
className={cn("flex flex-1 !bg-transparent !opacity-100", {
"text-destructive pr-10": identifierError != null
})}
/>
{
identifierError &&
<Tooltip >
<TooltipTrigger asChild className="absolute">
<Button variant={"ghost"} size="icon"
className="size-7 shrink-0 hover:bg-destructive/10 dark:hover:bg-destructive/10 right-1 bg-background rounded-sm"
onClick={(event: any) => {
event.stopPropagation();
showIdentifierError();
inputRef.current?.focus();
}}>
<IconAlertTriangle className="size-4 text-destructive " />
</Button>
</TooltipTrigger>
<TooltipContent className="bg-destructive fill-destructive [&_svg]:bg-destructive [&_svg]:fill-destructive">
{t("db_controller.validation.error_message")}
</TooltipContent>
</Tooltip>
}
</div>
<Combobox
items={data_types}
label="name"
placeholder={t("db_controller.type")}
selectedItem={selectedType}
onSelectionChange={updateFieldType}
className="flex flex-1 !bg-transparent !font-normal min-w-0"
className={cn("flex flex-1 !bg-transparent !font-normal !opacity-100 min-w-0 w-full ", {
"text-destructive border-destructive hover:text-destructive": !selectedType
})}
valueLength={20}
/>
</div>
<div className="flex gap-2 ml-2 ">
@@ -11,20 +11,21 @@ import React, { useEffect, useState } from "react";
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
import hash from "object-hash" ;
import { DatabaseDialect } from "@/lib/database";
interface Props {
tableFields: FieldType[] ;
tableId : string ;
tableId : string ;
dialect?: DatabaseDialect
}
const FieldList: React.FC<Props> = ({ tableFields , tableId}) => {
const FieldList: React.FC<Props> = ({ tableFields , tableId , dialect}) => {
const [fields, setFields] = useState<FieldType[]>(tableFields);
const { orderTableFields } = useDatabaseOperations();
useEffect(() => {
setFields(tableFields)
}, [tableFields])
@@ -65,7 +66,7 @@ const FieldList: React.FC<Props> = ({ tableFields , tableId}) => {
>
{
fields.map((field: FieldType) => (
<FieldItem field={field} key={field.id} />
<FieldItem field={field} key={field.id} dialect={dialect}/>
))
}
@@ -273,7 +273,7 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
return (
<div className="w-full flex flex-col gap-2 max-w-[260px]">
<div className="w-full flex flex-col gap-2 max-w-[260px]">
<h3 className="font-medium text-sm">
{t("db_controller.field_settings.title")}
</h3>
@@ -366,8 +366,6 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
{
modifiers.includes(Modifiers.SCALE) &&
<div className="flex flex-col gap-2 w-full">
<Tooltip>
<TooltipTrigger asChild>
<Label className="flex items-center justify-between font-medium" htmlFor="scale">
@@ -438,7 +436,8 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
</SelectTrigger>
<SelectContent>
<SelectItem value={"none"} >
No charset
{t("db_controller.field_settings.no_charset")}
</SelectItem>
{
@@ -464,7 +463,6 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
aria-label="collation"
value={collation as any}
onValueChange={changeCollation as any}
>
<SelectTrigger id="collation" className="w-full">
<SelectValue placeholder={t("db_controller.field_settings.collation")} />
@@ -472,7 +470,7 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
<SelectContent>
<SelectItem value={"none"} >
No collation
{t("db_controller.field_settings.no_collation")}
</SelectItem>
{
Object.values(collations).map((collation: string) => (<SelectItem
@@ -530,7 +528,7 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
<InputTags
value={jsonValues}
onChange={updateValues as any}
placeholder="Enter values, comma separated..."
placeholder={t("db_controller.field_settings.input_tag_placeholder")}
/>
</>
}
@@ -5,14 +5,18 @@ import { Label } from "@/components/ui/label";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Separator } from "@/components/ui/separator";
import { Switch } from "@/components/ui/switch";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { useDatabaseInvalidIdentifier } from "@/features/database/hooks/use-invalid-identifier";
import { DatabaseDialect } from "@/lib/database";
import { FieldType } from "@/lib/schemas/field-schema";
import { FieldIndexType } from "@/lib/schemas/field_index-schema";
import { IndexInsertType, IndexType } from "@/lib/schemas/index-schema";
import { cn } from "@/lib/utils";
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
import { areArraysEqual } from "@/utils/utils";
import { IconTrash } from "@tabler/icons-react";
import { IconAlertTriangle, IconTrash } from "@tabler/icons-react";
import { Settings2 } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
@@ -21,19 +25,30 @@ import { useTranslation } from "react-i18next";
interface Props {
index: IndexType;
fields: FieldType[]
fields: FieldType[];
dialect?: DatabaseDialect
}
const IndexItem: React.FC<Props> = ({ index, fields }) => {
const IndexItem: React.FC<Props> = ({ index, fields, dialect }) => {
const { t } = useTranslation();
const { editIndex, deleteIndex, editFieldIndices } = useDatabaseOperations();
const [isUnique, setIsUnique] = useState<boolean>(index.unique as boolean);
const [fieldIndices, setFieldIndices] = useState<string[]>([]);
const inputRef = useRef<HTMLInputElement>(null);
const [indexName, setIndexName] = useState<string>(index.name);
const { identifierError, showIdentifierError, identifierMaxLength } = useDatabaseInvalidIdentifier(indexName, dialect, "index");
useEffect(() => {
setIndexName(index.name);
}, [index.name]);
useEffect(() => {
setFieldIndices(
@@ -52,10 +67,15 @@ const IndexItem: React.FC<Props> = ({ index, fields }) => {
unique
} as IndexInsertType);
}
const editIndexName = (event: any) => {
const editIndexName = () => {
if (identifierError == "empty") {
setIndexName(index.name);
return;
}
editIndex({
id: index.id,
name: event.target.value
name: indexName
} as IndexInsertType);
}
const removeIndex = () => {
@@ -63,9 +83,15 @@ const IndexItem: React.FC<Props> = ({ index, fields }) => {
}
const updateFieldIndices = useCallback((fieldIds: string[]) => {
const previousFieldids: string[] = index.fieldIndices.map((fieldIndex: FieldIndexType) => fieldIndex.fieldId);
const deletedFieldIndicesIds: string[] = index.fieldIndices.filter((fieldIndex: FieldIndexType) => !fieldIds.includes(fieldIndex.fieldId)).map((fieldIndex: FieldIndexType) => fieldIndex.id);
const createFieldIndices: string[] = fieldIds.filter((fieldId: string) => !previousFieldids.includes(fieldId))
setFieldIndices(fieldIds);
if (!areArraysEqual(fieldIds, index.fieldIndices.map((fieldIndex: FieldIndexType) => fieldIndex.fieldId)))
editFieldIndices(index.id, fieldIds);
editFieldIndices(index.id, createFieldIndices, deletedFieldIndicesIds);
}, [index.fieldIndices])
return (
@@ -90,12 +116,15 @@ const IndexItem: React.FC<Props> = ({ index, fields }) => {
<Button
size="icon"
variant={"ghost"}
className="dark:bg-card dark:border-none size-9 text-muted-foreground hover:text-foreground"
className="dark:bg-card dark:border-none size-9 text-muted-foreground hover:text-foreground relative"
>
<Settings2 className="size-4 " />
{
identifierError && <span className="size-1.5 bg-destructive rounded-md absolute bottom-1.5 right-1.5"></span>
}
</Button>
</PopoverTrigger>
<PopoverContent side="right" className="max-w-[210px]">
<PopoverContent side="right" className="max-w-[224px]">
<div className="w-full flex flex-col gap-2 ">
<h3 className="font-medium text-sm ">
{t("db_controller.index_setting")}
@@ -115,15 +144,44 @@ const IndexItem: React.FC<Props> = ({ index, fields }) => {
{t("db_controller.name")}
</Label>
<Input
id="name"
aria-label={t("db_controller.index_name")}
placeholder={t("db_controller.index_name")}
onBlur={editIndexName}
autoFocus
defaultValue={index.name}
/>
<div className="flex items-center">
<Input
id="name"
aria-label={t("db_controller.index_name")}
placeholder={t("db_controller.index_name")}
onBlur={editIndexName}
onChange={(event: any) => setIndexName(event.target.value)}
ref={inputRef}
aria-invalid={identifierError != null}
autoFocus
defaultValue={indexName}
value={indexName}
maxLength={identifierMaxLength}
className={cn({
"text-destructive pr-10": identifierError != null
})}
/>
{
identifierError &&
<Tooltip>
<TooltipTrigger asChild className="absolute">
<Button variant={"ghost"} size="icon"
className="size-7 shrink-0 hover:bg-destructive/10 dark:hover:bg-destructive/10 right-5.5 bg-background rounded-sm"
onClick={(event: any) => {
event.stopPropagation();
showIdentifierError();
inputRef.current?.focus();
}}>
<IconAlertTriangle className="size-4 text-destructive " />
</Button>
</TooltipTrigger>
<TooltipContent className="bg-destructive fill-destructive [&_svg]:bg-destructive [&_svg]:fill-destructive">
{t("db_controller.validation.error_message")}
</TooltipContent>
</Tooltip>
}
</div>
<Separator />
<Button
variant="destructive"
@@ -138,7 +196,7 @@ const IndexItem: React.FC<Props> = ({ index, fields }) => {
</PopoverContent>
</Popover>
</div>
</div>
)
}
@@ -8,38 +8,47 @@ import { FieldType } from "@/lib/schemas/field-schema";
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
import { v4 } from "uuid";
import { IconPlus } from "@tabler/icons-react";
import { TableType } from "@/lib/schemas/table-schema";
import { DatabaseDialect } from "@/lib/database";
import { useTranslation } from "react-i18next";
interface IndexesListProps {
indices: IndexType[],
fields: FieldType[],
tableId: string
tableId: string,
table: TableType,
dialect?: DatabaseDialect
}
const IndexesList: React.FC<IndexesListProps> = ({ indices, fields, tableId }) => {
const IndexesList: React.FC<IndexesListProps> = ({ indices, fields, tableId, table, dialect }) => {
const { createIndex } = useDatabaseOperations();
const { t } = useTranslation();
const addIndex = (event: any) => {
event.stopPropagation && event.stopPropagation();
createIndex({
id: v4(),
name: `index_${indices.length + 1}`,
name: `${table.name}_index_${indices.length + 1}`,
unique: false,
tableId: tableId
} as IndexInsertType);
}
return (
<div className="w-full flex-col space-y-2 ">
{
indices.map((index: IndexType) => <IndexItem index={index} fields={fields} key={index.id} />)
indices.map((index: IndexType) => <IndexItem index={index} fields={fields} key={index.id} dialect={dialect} />)
}
{
indices.length == 0 && <p className="text-muted-foreground py-1">
No Indices found <span className="text-foreground hover:underline cursor-pointer" onClick={addIndex}>
create new one <IconPlus className="size-3.5 shrink-0 inline"/>
</span>
{t("db_controller.no_index")} { <span className="text-foreground hover:underline cursor-pointer" onClick={addIndex}>
{t("db_controller.create_new_index")} <IconPlus className="size-3.5 shrink-0 inline" />
</span>}
</p>
}
@@ -4,25 +4,27 @@ import { useTranslation } from "react-i18next";
import FieldList from "./field/field-list";
import IndexesList from "./index/indexes-list";
import { TableType } from "@/lib/schemas/table-schema";
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
import { getNextSequence } from "@/utils/field";
import { v4 } from "uuid";
import { IndexInsertType } from "@/lib/schemas/index-schema";
import { FieldInsertType } from "@/lib/schemas/field-schema";
import { AccordionContent, Accordion, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
import { IconBolt , IconFolder, IconMessageCircle, } from "@tabler/icons-react";
import { IconBolt, IconFolder, IconMessageCircle, } from "@tabler/icons-react";
import { Textarea } from "@/components/ui/textarea";
import { Button } from "@/components/ui/button";
import ColorPicker from "@/components/color-picker";
import { DatabaseDialect } from "@/lib/database";
export interface TableAccordionBodyProps {
table: TableType,
keys?: string[];
dialect?: DatabaseDialect
}
const TableAccordionContent: React.FC<TableAccordionBodyProps> = ({ table, keys }) => {
const TableAccordionContent: React.FC<TableAccordionBodyProps> = ({ table, keys , dialect}) => {
const [selectedItems, setSelectedItems] = useState<string[]>(["fields"]);
const [note, setNote] = useState<string>(table.note ? table.note : "");
@@ -79,17 +81,16 @@ const TableAccordionContent: React.FC<TableAccordionBodyProps> = ({ table, keys
setSelectedItems([...selectedItems, "indexes"])
}, [table.indices.length])
return (
<AccordionContent className=" p-0 ">
<Accordion
type="multiple"
className="w-full"
value={selectedItems}
onValueChange={setSelectedItems as any}
>
<AccordionItem value="fields" className="border-none">
<AccordionTrigger className="py-1 rounded-none text-muted-foreground pr-1.5" position="right" >
<div className="w-full flex gap-1 items-center">
@@ -98,26 +99,26 @@ const TableAccordionContent: React.FC<TableAccordionBodyProps> = ({ table, keys
</div>
</AccordionTrigger>
<AccordionContent className="pb-0 ">
<FieldList tableFields={table.fields} tableId={table.id} />
<FieldList tableFields={table.fields} tableId={table.id} dialect={dialect}/>
</AccordionContent>
</AccordionItem>
<AccordionItem value="indexes" className="border-none">
<AccordionTrigger className=" py-1 rounded-none text-muted-foreground pr-1.5" position="right">
<div className="w-full flex gap-1 items-center">
<div className="w-full flex gap-1 items-center">
<IconBolt className="size-4" />
{t("db_controller.indexes")}
</div>
</AccordionTrigger>
<AccordionContent className="pb-2">
<IndexesList indices={table.indices} fields={table.fields} tableId={table.id} />
<IndexesList indices={table.indices} fields={table.fields} table={table} tableId={table.id} dialect={dialect}/>
</AccordionContent>
</AccordionItem>
<AccordionItem value="note" className="border-none">
<AccordionItem value="note" className="border-none">
<AccordionTrigger className=" py-1 rounded-none text-muted-foreground pr-1.5" position="right">
<div className="w-full flex gap-1 items-center">
<IconMessageCircle className="size-4 " />
<IconMessageCircle className="size-4 " />
{t("db_controller.note")}
</div>
</AccordionTrigger>
@@ -125,20 +126,20 @@ const TableAccordionContent: React.FC<TableAccordionBodyProps> = ({ table, keys
<Textarea
placeholder={t("db_controller.table_note")}
value={note}
onChange={(event : any) => setNote(event.target.value) }
onChange={(event: any) => setNote(event.target.value)}
onBlur={saveNote}
className="resize-none min-h-[86px] focus-visible:ring-0 bg-secondary dark:bg-background"
/>
</AccordionContent>
</AccordionItem>
</AccordionItem>
</Accordion>
<div className="flex pt-2 justify-between pr-1.5 items-center ">
<div className="h-full flex items-center">
<ColorPicker
defaultColor={table.color as string}
onChange={onColorChange}
/>
</div>
<div className="flex gap-2 ">
@@ -168,4 +169,3 @@ const TableAccordionContent: React.FC<TableAccordionBodyProps> = ({ table, keys
export default React.memo(TableAccordionContent);
@@ -8,13 +8,15 @@ import TableAccordionContent from "./table-accordion-content";
import React, { forwardRef } from "react";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { DatabaseDialect } from "@/lib/database";
interface TableAccordionItemProps {
table: TableType;
dialect?: DatabaseDialect
}
const TableAccordionItem = forwardRef<HTMLDivElement, TableAccordionItemProps>(
({ table }, ref) => {
({ table , dialect }, ref) => {
const { attributes, setNodeRef, transform, transition } = useSortable({ id: table.id });
if (transform?.scaleY)
@@ -35,9 +37,9 @@ const TableAccordionItem = forwardRef<HTMLDivElement, TableAccordionItemProps>(
{...attributes}
className="w-full "
>
<TableAccordionTrigger table={table} />
<TableAccordionTrigger table={table} dialect={dialect} />
<AccordionContent className="flex flex-col gap-4 text-balance">
<TableAccordionContent table={table} />
<TableAccordionContent table={table} dialect={dialect} />
</AccordionContent>
</div>
</AccordionItem>
@@ -12,22 +12,27 @@ import { cloneTable } from "@/utils/tables";
import { IndexInsertType } from "@/lib/schemas/index-schema";
import { FieldInsertType } from "@/lib/schemas/field-schema";
import { AccordionTrigger } from "@/components/ui/accordion";
import { IconBolt, IconCheck, IconCopy, IconDotsVertical, IconFocus2, IconFolder, IconGripVertical, IconPencil, IconTrash } from "@tabler/icons-react";
import { IconAlertTriangle, IconBolt, IconCheck, IconCopy, IconDotsVertical, IconFocus2, IconFolder, IconGripVertical, IconPencil, IconTrash } from "@tabler/icons-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { DropdownMenuTrigger, DropdownMenu, DropdownMenuContent, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuItem, DropdownMenuShortcut } from "@/components/ui/dropdown-menu";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { useSortable } from "@dnd-kit/sortable";
import { DatabaseDialect } from "@/lib/database";
import { useDatabaseInvalidIdentifier } from "@/features/database/hooks/use-invalid-identifier";
import { isValidIdentifier } from "@/utils/database";
import { cn } from "@/lib/utils";
export interface TableAccordionHeaderProps {
table: TableType,
isOpen?: boolean,
dialect?: DatabaseDialect
}
const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOpen }) => {
const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOpen, dialect }) => {
const { editTable, deleteTable, createField, createTable, createIndex, getInteger } = useDatabaseOperations();
@@ -36,14 +41,31 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
const [editMode, setEditMode] = useState<boolean>(false);
const { focusOnTable } = useDiagramOps();
const { identifierError, setIdentifierError, showIdentifierError, identifierMaxLength } = useDatabaseInvalidIdentifier(tableName, dialect);
useEffect(() => {
setTableName(table.name);
}, [table.name])
}, [table.name]);
useEffect(() => {
setIdentifierError(isValidIdentifier(
tableName.trim(),
dialect
))
}, [tableName]);
const saveTableName = useCallback(async () => {
await editTable({ id: table.id, name: tableName } as TableInsertType);
if (identifierError == "empty") {
setTableName(table.name);
setEditMode(false);
return;
}
await editTable({ id: table.id, name: tableName.trim() } as TableInsertType);
setEditMode(false);
}, [tableName])
}, [tableName, identifierError, table])
const onDeleteTable = useCallback(async (event: any) => {
event.stopPropagation();
@@ -74,10 +96,10 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
unique: false,
tableId: table.id
} as IndexInsertType);
}, [table])
const duplicate = useCallback(async (event : any) => {
const duplicate = useCallback(async (event: any) => {
event.stopPropagation()
const clonedTable: TableType = cloneTable(table);
await createTable(clonedTable)
@@ -110,9 +132,10 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
<div className="w-full flex items-center ">
<Tooltip>
<TooltipTrigger asChild>
<label
className=" py-2 truncate text-sm cursor-pointer max-w-60"
className={cn("py-2 truncate text-sm cursor-pointer max-w-50 ", {
"text-destructive": identifierError != null
})}
onDoubleClick={() => setEditMode(true)}
onClick={((event: any) => event.stopPropagation())}
>
@@ -129,7 +152,6 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
<Button variant="outline" size="icon" className="size-8 shrink-0" onClick={(event: any) => {
event.stopPropagation();
setEditMode(true)
}}>
<IconPencil className="size-4 text-muted-foreground " />
</Button>
@@ -140,8 +162,24 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
<IconFocus2 className="size-4 text-muted-foreground " />
</Button>
</div>
{
identifierError &&
<Tooltip>
<TooltipTrigger asChild>
<Button variant={"ghost"} size="icon" className="size-8 shrink-0 hover:bg-destructive/10 hover:border-destructive/30 dark:hover:bg-destructive/10 dark:hover:border-destructive/30" onClick={(event: any) => {
event.stopPropagation();
showIdentifierError();
setEditMode(true);
}} >
<IconAlertTriangle className="size-4 text-destructive " />
</Button>
</TooltipTrigger>
<TooltipContent className="bg-destructive fill-destructive [&_svg]:bg-destructive [&_svg]:fill-destructive">
{t("db_controller.validation.error_message")}
</TooltipContent>
</Tooltip>
}
</>
}
{
editMode && <>
@@ -152,9 +190,13 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
onBlur={saveTableName}
autoFocus
type="text"
aria-invalid={identifierError != null}
onClick={(event) => event.stopPropagation()}
className="h-8"
onKeyDown={(e: any) => {
className={cn("h-8", {
"text-destructive": identifierError != null
})}
maxLength={identifierMaxLength}
onKeyDown={(e: any) => {
if (e.key === "Enter") {
e.preventDefault();
saveTableName();
+11 -6
View File
@@ -2,10 +2,8 @@
import { FieldType } from "@/lib/schemas/field-schema";
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
import { useDiagramOps } from "@/providers/diagram-provider/diagram-provider";
import { Handle, Position } from "@xyflow/react";
import React, { useCallback, useEffect, useState } from "react";
import hash from 'object-hash';
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { IconCheck, IconKey, IconKeyframe, IconMessageCircle, IconPencil, IconTrash } from "@tabler/icons-react";
@@ -18,7 +16,8 @@ interface Props {
showHandles?: boolean,
highlight?: boolean,
color?: string,
className?: string
className?: string,
maxLength?: number
}
@@ -29,7 +28,7 @@ export const TARGET_PREFIX = "target_";
const Field: React.FC<Props> = (props) => {
const { field, showHandles, highlight, color, className } = props;
const { field, showHandles, highlight, color, className, maxLength } = props;
const [editMode, setEditMode] = useState<boolean>(false);
const { deleteField, editField } = useDatabaseOperations();
const [fieldName, setFieldName] = useState<string>(field.name);
@@ -46,13 +45,18 @@ const Field: React.FC<Props> = (props) => {
const saveFieldName = useCallback(() => {
if (fieldName.trim().length == 0) {
setFieldName(field.name);
setEditMode(false);
return;
}
editField({
id: field.id,
name: fieldName
name: fieldName.trim()
} as FieldType);
setEditMode(false);
}, [fieldName]);
}, [fieldName, field]);
return (
@@ -103,6 +107,7 @@ const Field: React.FC<Props> = (props) => {
onClick={(e) => e.stopPropagation()}
onChange={(e) => setFieldName(e.target.value)}
className="h-6 font-bold pb-1.5 rounded-sm px-1"
maxLength={maxLength}
onKeyDown={(e: any) => {
if (e.key === "Enter") {
e.preventDefault();
+10 -4
View File
@@ -95,17 +95,23 @@ const RenameDB: React.FC = ({ }) => {
value={dbName}
onBlur={saveDatabaseName}
autoFocus
onKeyDown={(e: any) => {
if (e.key === "Enter") {
e.preventDefault();
saveDatabaseName();
e.target.blur();
}
}}
className="h-8 min-w-[164px]"
maxLength={255}
aria-invalid={dbName.length == 0}
/>
<Button
variant="default"
size="sm"
color="primary"
onClick={saveDatabaseName}
disabled={isLoading}
disabled={isLoading || dbName.length == 0}
>
{isLoading && <Spinner />}
<Save className="size-3" />
+22 -14
View File
@@ -25,13 +25,14 @@ export type TableProps = Node<{
table: TableType,
overlapping?: boolean,
pulsing?: boolean,
highlightedEdges: Edge[]
highlightedEdges: Edge[],
identifierMaxLength: number
}>
const MAX_FIELDS = 10;
const Table: React.FC<NodeProps<TableProps>> = (props) => {
const { selected, data: { table, overlapping = false, pulsing = false, highlightedEdges = [] } } = props
const { selected, data: { table, overlapping = false, pulsing = false, highlightedEdges = [], identifierMaxLength } } = props
const [editMode, setEditMode] = useState<boolean>(false);
const [tableName, setTableName] = useState<string>(table.name);
@@ -46,9 +47,14 @@ const Table: React.FC<NodeProps<TableProps>> = (props) => {
}, [table.name])
const saveTableName = useCallback(async () => {
await editTable({ id: table.id, name: tableName } as TableInsertType);
if (tableName.trim().length == 0) {
setTableName(table.name);
setEditMode(false);
return;
}
await editTable({ id: table.id, name: tableName.trim() } as TableInsertType);
setEditMode(false);
}, [tableName]);
}, [tableName, table]);
const focus = useCallback(() => {
focusOnTable(table.id, false);
@@ -56,7 +62,7 @@ const Table: React.FC<NodeProps<TableProps>> = (props) => {
const fields: React.ReactNode[] = useMemo(() => {
return table.fields.map((field: FieldType , index : number) => {
return table.fields.map((field: FieldType, index: number) => {
const highlight: boolean = highlightedEdges.find((edge: any) =>
(edge.data?.relationship as RelationshipType).sourceFieldId == field.id ||
@@ -68,10 +74,11 @@ const Table: React.FC<NodeProps<TableProps>> = (props) => {
showHandles={selected}
highlight={highlight}
color={table.color as string}
className={ index == table.fields.length - 1 ? "!rounded-b-md" : undefined }
className={index == table.fields.length - 1 ? "!rounded-b-md" : undefined}
maxLength={identifierMaxLength}
/>)
})
}, [table.fields, selected, highlightedEdges]);
}, [table.fields, selected, highlightedEdges, identifierMaxLength]);
const toggleShowMore = useCallback(() => {
@@ -79,7 +86,7 @@ const Table: React.FC<NodeProps<TableProps>> = (props) => {
}, []);
return (
<Card
<Card
className={cn(
"rounded-lg p-0.5 gap-0 transition-all duration-200 border-none ring-1 ring-slate-300 shadow-xs dark:ring-border ",
overlapping
@@ -116,17 +123,17 @@ const Table: React.FC<NodeProps<TableProps>> = (props) => {
{!editMode ? <>
<label
className=" w-full text-editable truncate py-0.5 text-sm font-bold text-primary"
onDoubleClick={ () => setEditMode(true) }
onDoubleClick={() => setEditMode(true)}
style={{ color: table.color as string }}
>
{tableName}
</label>
<div className="flex gap-1 hidden shrink-0 flex-row group-hover:flex ">
<Button variant="outline" size="icon" className="size-6 shrink-0 shadow-sm rounded-sm" onClick={() => setEditMode(true)}>
<IconPencil className="size-3 text-muted-foreground " />
</Button>
<Button variant="outline" size="icon" className="size-6 shrink-0 shadow-sm rounded-sm" onClick={() => setEditMode(true)}>
<IconPencil className="size-3 text-muted-foreground " />
</Button>
<Button variant="outline" size="icon" className="size-6 shrink-0 shadow-sm rounded-sm" onClick={focus}>
<IconFocus2 className="size-3 text-muted-foreground " />
</Button>
@@ -144,6 +151,7 @@ const Table: React.FC<NodeProps<TableProps>> = (props) => {
onChange={(event: any) => setTableName(event.target.value)}
value={tableName}
onBlur={saveTableName}
maxLength={identifierMaxLength}
style={{ color: table.color as string }}
onKeyDown={(e: any) => {
if (e.key === "Enter") {
@@ -0,0 +1,70 @@
import useToast from "@/hooks/use-toast";
import { DatabaseDialect, DatabaseType, getDatabaseByDialect, IDENTIFIER_ERROR, IDENTIFIER_MAX_LENGTH } from "@/lib/database";
import { isValidIdentifier } from "@/utils/database";
import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
interface databaseInvalidIdentifierType {
identifierError: IDENTIFIER_ERROR,
setIdentifierError: Dispatch<SetStateAction<IDENTIFIER_ERROR>>,
showIdentifierError: () => void,
identifierMaxLength: number;
identifierType? : 'table' | 'constraint' | "index" | "column"
}
export const useDatabaseInvalidIdentifier = (identifier: string, dialect?: DatabaseDialect , identifierType = "table"): databaseInvalidIdentifierType => {
const raise = useToast();
const { t } = useTranslation();
const [identifierError, setIdentifierError] = useState<IDENTIFIER_ERROR>(null);
const identifierMaxLength = useMemo(() => {
return dialect ? IDENTIFIER_MAX_LENGTH[dialect] : 255
}, [dialect])
useEffect(() => {
setIdentifierError(isValidIdentifier(
identifier.trim(),
dialect
))
}, [identifier]);
const showIdentifierError = useCallback(() => {
let title: string | undefined;
let description: string | undefined;
const identifier: string = t(`db_controller.validation.identifiers.${identifierType}`);
if (identifierError == "empty") {
title = t("db_controller.validation.identifier_required").replace("${identifier}", identifier.toLocaleLowerCase())
description = t("db_controller.validation.identifier_required_description").replace("${identifier}", identifier)
}
if (identifierError == "invalid") {
title = t("db_controller.validation.invalid_identifier").replace("${identifier}", identifier.toLocaleLowerCase())
description = t("db_controller.validation.invalid_identifier_description").replace("${identifier}", identifier.toLocaleLowerCase())
}
if (identifierError === "max_length" && dialect) {
const databaseType: DatabaseType = getDatabaseByDialect(dialect)
title = t("db_controller.validation.identifier_length").replace("${identifier}", identifier)
description = t("db_controller.validation.identifier_length_description")
.replace("${identifier}", identifier.toLocaleLowerCase())
.replace("${dialect}", databaseType.name).replace("${length}", String(identifierMaxLength))
}
if (title && description) {
raise(title, description, "ERROR")
}
}, [identifierError, identifierMaxLength, dialect , identifierType]);
return {
identifierError,
setIdentifierError,
showIdentifierError,
identifierMaxLength
}
}
@@ -1,17 +1,22 @@
import { TableType } from "@/lib/schemas/table-schema";
import { Node, useReactFlow } from "@xyflow/react";
import { useEffect } from "react";
import { useEffect, useMemo } from "react";
import { getDefaultTableOverlapping } from "@/utils/tables";
import hash from "object-hash";
import { DatabaseDialect, IDENTIFIER_MAX_LENGTH } from "@/lib/database";
export const useTableToNode = (tables: TableType[]): void => {
export const useTableToNode = (tables: TableType[], dialect?: DatabaseDialect): void => {
const { setNodes } = useReactFlow();
useEffect(() => {
const identifierMaxLength = useMemo(() => {
return dialect ? IDENTIFIER_MAX_LENGTH[dialect] : 255
}, [dialect]);
useEffect(() => {
const tableNodes = tables.map((table: TableType) => {
return {
id: table.id,
@@ -24,29 +29,30 @@ export const useTableToNode = (tables: TableType[]): void => {
table,
overlapping: getDefaultTableOverlapping(table, tables),
pulsing: false,
highlightedEdges: []
highlightedEdges: [] ,
identifierMaxLength
},
style: {
width: 224
}
} as Node
}) ;
});
setNodes((nodes) => {
return tableNodes.map((tableNode) => {
const node: Node | undefined = nodes.find((node: Node) => node.id == tableNode.id);
if (!node)
return tableNode;
else {
const hashNode: string = hash(node.data.table as TableType);
const hashTableNode: string = hash(tableNode.data.table as TableType);
return hashNode == hashTableNode ? node : tableNode;
}
})
});
});
}, [tables])
}
@@ -1,4 +1,4 @@
import Modal, { ModalProps } from "@/components/modal"
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
@@ -32,14 +32,14 @@ export const CreateDatabaseModal: React.FC<ModalProps> = (props) => {
}
const createNewDatabase = useCallback(async (withImport: boolean = false) => {
const databaseId: string = v4() ;
const databaseId: string = v4();
return new Promise(async (res, rej) => {
try {
await createDatabase({
id: databaseId,
name: dbName,
dialect: selectedDbType as any
dialect: selectedDbType as any
});
await switchDatabase(databaseId);
if (withImport)
@@ -50,11 +50,11 @@ export const CreateDatabaseModal: React.FC<ModalProps> = (props) => {
} catch (error) {
rej(error);
}
})
})
}, [selectedDbType, dbName])
useEffect(() => {
setIsValid((selectedDbType && dbName.trim().length > 0) as boolean)
setIsValid((selectedDbType && dbName.trim().length > 0) as boolean)
}, [selectedDbType, dbName]);
return (
@@ -64,7 +64,7 @@ export const CreateDatabaseModal: React.FC<ModalProps> = (props) => {
onOpenChange={onOpenChange}
title={t("modals.pick_database")}
actionName={t("modals.create")}
isDisabled={!isValid}
actionHandler={createNewDatabase}
description={t("modals.create_database_header")}
@@ -81,10 +81,11 @@ export const CreateDatabaseModal: React.FC<ModalProps> = (props) => {
aria-invalid={dbName.trim().length == 0}
onChange={(event: any) => setDbName(event.target.value)}
placeholder={t("modals.db_name")}
maxLength={255}
/>
{
dbName.trim().length == 0 &&
<p className="text-destructive">
<p className="text-destructive text-xs">
{t("modals.db_name_error")}
</p>
}
@@ -99,9 +100,9 @@ export const CreateDatabaseModal: React.FC<ModalProps> = (props) => {
<Label htmlFor={db.dialect}
className={cn("w-full p-3 rounded-md border hover:bg-secondary cursor-pointer",
(selectedDbType == db.dialect && !db.comming )? "border-primary bg-primary/20 " : "" ,
( db.comming )? "opacity-75 hover:bg-background cursor-default" : ""
(selectedDbType == db.dialect && !db.comming) ? "border-primary bg-primary/20 " : "",
(db.comming) ? "opacity-75 hover:bg-background cursor-default" : ""
)}>
<RadioGroupItem disabled={db.comming} value={db.dialect} id={db.dialect} className="sr-only" />
@@ -113,7 +114,7 @@ export const CreateDatabaseModal: React.FC<ModalProps> = (props) => {
{db.name}
</span>
{
db.comming &&
db.comming &&
<Badge className="absolute -top-2 left-[50%] -translate-x-[50%] bg-chart-5 dark:bg-chart-3">
Comming
</Badge>
@@ -123,7 +124,7 @@ export const CreateDatabaseModal: React.FC<ModalProps> = (props) => {
))
}
</RadioGroup>
<div className="py-4 space-y-2">
<Button
variant="ghost"
+5 -2
View File
@@ -44,8 +44,11 @@ i18n.use(LanguageDetector)
.use(initReactI18next)
.init({
resources,
lng: 'en',
detection: {
order: ['localStorage'],
caches: ['localStorage'],
},
interpolation: {
escapeValue: false,
},
+25 -1
View File
@@ -28,6 +28,9 @@ export const ar = {
show_code: "عرض الكود",
fields: "الحقول",
indexes: "الفهارس",
no_index: "لم يتم العثور على فهارس",
create_new_index: "إنشاء فهرس جديد",
note: "ملاحظة",
name: "الاسم",
type: "النوع",
@@ -84,6 +87,12 @@ export const ar = {
text_setting: "إعداد النص",
charset: "ترميز الحروف",
collation: "التجميع",
no_charset: "لا توجد مجموعة أحرف",
no_collation: "لا يوجد ترتيب",
input_tag_placeholder: "أدخل القيم، مفصولة بفواصل...",
multi_select_placeholder: "حدد الحقول...",
date_placeholder: "اختر تاريخًا",
time: "الوقت",
scale: "المقياس",
max_length: "الطول الأقصى",
integer_width: "عرض العدد الصحيح",
@@ -96,7 +105,7 @@ export const ar = {
precision_def: "إجمالي الأرقام المسموحة (قبل + بعد الفاصلة).",
scale_def: "عدد الأرقام بعد الفاصلة.",
no_default: "لا توجد قيمة افتراضية",
random_uuid: "معرّف UUID عشوائي",
random_uuid: "معرّف UUID عشوائي",
time_default_value: {
no_value: "لا توجد قيمة",
custom: "وقت مخصص",
@@ -129,6 +138,21 @@ random_uuid: "معرّف UUID عشوائي",
description: "يحتوي المخطط الخاص بك على علاقة مفتاح خارجي دائرية بين الجداول. لإصلاحها،",
suggestion: "قم بإزالة إحدى العلاقات أدناه التي تسبب الدورة.",
remove_relationship: "إزالة العلاقة"
},
validation: {
identifiers: {
table: "جدول",
column: "عمود",
index: "فهرس",
constraint: "قيد"
},
error_message: "معرّف غير صالح",
identifier_required: "اسم ${identifier} مطلوب",
identifier_required_description: "يرجى إدخال اسم لـ ${identifier}.",
invalid_identifier: "اسم ${identifier} غير صالح",
invalid_identifier_description: "استخدم الأحرف والأرقام والشرطات السفلية فقط. يجب أن يبدأ اسم ${identifier} بحرف.",
identifier_length: "اسم ${identifier} طويل جدًا",
identifier_length_description: "يتجاوز اسم ${identifier} هذا الحد الأقصى للطول وهو ${length} بايت الذي يسمح به ${dialect}."
}
},
table: {
+24 -1
View File
@@ -28,6 +28,8 @@ export const de = {
show_code: "Code anzeigen",
fields: "Felder",
indexes: "Indizes",
no_index: "Keine Indizes gefunden",
create_new_index: "Neuen erstellen",
note: "Notiz",
name: "Name",
type: "Typ",
@@ -84,6 +86,12 @@ export const de = {
text_setting: "Texteinstellung",
charset: "Zeichensatz",
collation: "Sortierung",
no_charset: "Kein Zeichensatz",
no_collation: "Keine Sortierung",
input_tag_placeholder: "Werte eingeben, durch Kommas getrennt...",
multi_select_placeholder: "Felder auswählen...",
date_placeholder: "Datum auswählen",
time: "Uhrzeit",
scale: "Skalierung",
max_length: "Maximale Länge",
integer_width: "Ganzzahlbreite",
@@ -96,7 +104,7 @@ export const de = {
precision_def: "Zulässige Gesamtanzahl an Ziffern (vor + nach dem Dezimalpunkt).",
scale_def: "Ziffern nach dem Dezimalpunkt.",
no_default: "Kein Standardwert",
random_uuid: "Zufällige UUID",
random_uuid: "Zufällige UUID",
time_default_value: {
no_value: "Kein Wert",
custom: "Benutzerdefinierte Zeit",
@@ -129,6 +137,21 @@ random_uuid: "Zufällige UUID",
description: "Dein Schema enthält eine zirkuläre Fremdschlüssel-Beziehung zwischen Tabellen. Um dies zu beheben,",
suggestion: "entferne eine der unten aufgeführten Beziehungen, die den Kreis verursachen.",
remove_relationship: "Beziehung entfernen"
},
validation: {
identifiers: {
table: "Tabelle",
column: "Spalte",
index: "Index",
constraint: "Einschränkung"
},
error_message: "Ungültiger Bezeichner",
identifier_required: "Der Name von ${identifier} ist erforderlich",
identifier_required_description: "Bitte geben Sie einen Namen für diesen ${identifier} an.",
invalid_identifier: "Ungültiger Name für ${identifier}",
invalid_identifier_description: "Verwenden Sie nur Buchstaben, Zahlen und Unterstriche. Der Name von ${identifier} muss mit einem Buchstaben beginnen.",
identifier_length: "Der Name von ${identifier} ist zu lang",
identifier_length_description: "Dieser Name von ${identifier} überschreitet die von ${dialect} zulässige maximale Länge von ${length} Bytes."
}
},
table: {
+23
View File
@@ -31,6 +31,8 @@ export const en = {
show_code: "Show code",
fields: "Fields",
indexes: "Indexes",
no_index: "No Indices found",
create_new_index: "create new one",
note: "Note",
name: "Name",
type: "Type",
@@ -93,6 +95,12 @@ export const en = {
text_setting: "Text Setting",
charset: "Charset",
collation: "Collation",
no_charset: "No charset",
no_collation: "No collation",
input_tag_placeholder: "Enter values, comma separated...",
multi_select_placeholder: "Select fields...",
date_placeholder: "Pick date",
time: "Time",
scale: "Scale",
max_length: "Max length",
integer_width: "Integer Width",
@@ -141,6 +149,21 @@ export const en = {
description: "Your schema contains a circular foreign key relationship between tables. To fix it",
suggestion: "remove one of the relationships listed below that are causing the cycle.",
remove_relationship: "Remove relationship"
},
validation: {
identifiers: {
table: "Table",
column: "Column",
index: "Index",
constraint: "Constraint"
},
error_message: "Invalid identifier",
identifier_required: "${identifier} name is required",
identifier_required_description: "Please provide a name for this ${identifier}.",
invalid_identifier: "Invalid ${identifier} name",
invalid_identifier_description: "Use only letters, numbers, and underscores. a ${identifier} name must start with a letter.",
identifier_length: "${identifier} name is too long",
identifier_length_description: "This ${identifier} name exceeds ${dialect}'s maximum length of ${length} bytes."
}
},
table: {
+29 -6
View File
@@ -5,8 +5,8 @@ export const es = {
translation: {
sidebar: {
tables: "Tablas",
relationships: "Relaciones",
documentation : "Documentación"
relationships: "Relaciones",
documentation: "Documentación"
},
color_picker: {
@@ -27,6 +27,8 @@ export const es = {
show_code: "Mostrar código",
fields: "Campos",
indexes: "Índices",
no_index: "No se encontraron índices",
create_new_index: "Crear uno nuevo",
note: "Nota",
name: "Nombre",
type: "Tipo",
@@ -88,6 +90,12 @@ export const es = {
text_setting: "Configuración de texto",
charset: "Charset",
collation: "Intercalación",
no_charset: "Sin juego de caracteres",
no_collation: "Sin intercalación",
input_tag_placeholder: "Introduzca valores separados por comas...",
multi_select_placeholder: "Seleccione campos...",
date_placeholder: "Seleccione una fecha",
time: "Hora",
scale: "Escala",
max_length: "Longitud máxima",
integer_width: "Ancho de entero",
@@ -100,7 +108,7 @@ export const es = {
precision_def: "Total de dígitos permitidos (antes + después del decimal).",
scale_def: "Dígitos permitidos después del decimal.",
no_default: "Sin valor predeterminado",
random_uuid: "UUID aleatorio",
random_uuid: "UUID aleatorio",
time_default_value: {
no_value: "Sin valor",
custom: "Hora personalizada",
@@ -136,6 +144,21 @@ random_uuid: "UUID aleatorio",
description: "Tu esquema contiene una relación foránea circular entre tablas. Para corregirlo",
suggestion: "elimina una de las relaciones que se indican a continuación y que están causando el ciclo.",
remove_relationship: "Eliminar relación"
},
validation: {
identifiers: {
table: "Tabla",
column: "Columna",
index: "Índice",
constraint: "Restricción"
},
error_message: "Identificador no válido",
identifier_required: "El nombre de ${identifier} es obligatorio",
identifier_required_description: "Proporcione un nombre para este ${identifier}.",
invalid_identifier: "Nombre de ${identifier} no válido",
invalid_identifier_description: "Use únicamente letras, números y guiones bajos. El nombre de ${identifier} debe comenzar con una letra.",
identifier_length: "El nombre de ${identifier} es demasiado largo",
identifier_length_description: "Este nombre de ${identifier} supera la longitud máxima de ${length} bytes permitida por ${dialect}."
}
},
table: {
@@ -202,12 +225,12 @@ random_uuid: "UUID aleatorio",
delete_database_content: "Esta acción es irreversible y eliminará permanentemente el diagrama.",
delete: "Eliminar",
empty_diagram: "Diagrama vacío",
create_and_import : "Crear e importar" ,
create_and_import: "Crear e importar",
import_database: {
title: "Importar tu base de datos",
import: "Importar",
view_docs : "Ver la documentación" ,
view_docs: "Ver la documentación",
import_options: "¿Te gustaría importar usando :",
import_error: "Error al analizar SQL",
import_error_description: "No pudimos importar tu SQL porque contiene sintaxis inválida.",
@@ -275,7 +298,7 @@ random_uuid: "UUID aleatorio",
step5: "Finalmente, copia el contenido del archivo <code>.sql</code> en el <bold>editor de código</bold> a continuación."
},
ssms: {
"step1": "Abra SQL Server Management Studio (SSMS).",
"step2": "Haga clic derecho en su base de datos y luego seleccione Tareas → Generar scripts en el menú contextual.",
+23
View File
@@ -30,6 +30,8 @@ export const fr = {
show_code: "Afficher le code",
fields: "Champs",
indexes: "Index",
no_index: "Aucun index trouvé",
create_new_index: "En créer un nouveau",
note: "Note",
name: "Nom",
type: "Type",
@@ -86,6 +88,12 @@ export const fr = {
text_setting: "Paramètres de texte",
charset: "Jeu de caractères",
collation: "Collation",
no_charset: "Aucun jeu de caractères",
no_collation: "Aucune collation",
input_tag_placeholder: "Saisissez des valeurs, séparées par des virgules...",
multi_select_placeholder: "Sélectionnez des champs...",
date_placeholder: "Sélectionnez une date",
time: "Heure",
scale: "Échelle",
max_length: "Longueur max",
integer_width: "Largeur entière",
@@ -131,6 +139,21 @@ export const fr = {
description: "Votre schéma contient une relation de clé étrangère circulaire entre les tables. Pour corriger cela,",
suggestion: "supprimez l’une des relations ci-dessous qui provoque le cycle.",
remove_relationship: "Supprimer la relation"
},
validation: {
identifiers: {
table: "Table",
column: "Colonne",
index: "Index",
constraint: "Contrainte"
},
error_message: "Identifiant invalide",
identifier_required: "Le nom de ${identifier} est requis",
identifier_required_description: "Veuillez fournir un nom pour ce ${identifier}.",
invalid_identifier: "Nom de ${identifier} invalide",
invalid_identifier_description: "Utilisez uniquement des lettres, des chiffres et des underscores. Un nom de ${identifier} doit commencer par une lettre.",
identifier_length: "Le nom de ${identifier} est trop long",
identifier_length_description: "Ce nom de ${identifier} dépasse la longueur maximale de ${length} octets autorisée par ${dialect}."
}
},
table: {
+28 -6
View File
@@ -6,7 +6,7 @@ export const hi = {
tables: "तालिकाएँ",
relationships: "रिलेशनशिप्स",
database: "डेटाबेस",
documentation : "दस्तावेज़"
documentation: "दस्तावेज़"
},
color_picker: {
@@ -28,6 +28,8 @@ export const hi = {
show_code: "कोड दिखाएं",
fields: "फ़ील्ड्स",
indexes: "इंडेक्सेस",
no_index: "कोई इंडेक्स नहीं मिला",
create_new_index: "नया इंडेक्स बनाएं",
note: "नोट",
name: "नाम",
type: "प्रकार",
@@ -82,13 +84,18 @@ export const hi = {
zeroFill: "शून्य भरें",
autoIncrement: "स्वतः वृद्धि",
note: "नोट",
delete_field: "फ़ील्ड हटाएं",
field_note: "फ़ील्ड नोट",
precision: "सटीकता",
text_setting: "टेक्स्ट सेटिंग",
charset: "अक्षरसेट",
collation: "कोलेशन",
no_charset: "कोई कैरेक्टर सेट नहीं",
no_collation: "कोई कॉलेशन नहीं",
input_tag_placeholder: "मान दर्ज करें, अल्पविराम से अलग करें...",
multi_select_placeholder: "फ़ील्ड चुनें...",
date_placeholder: "तारीख चुनें",
time: "समय",
scale: "स्केल",
max_length: "अधिकतम लंबाई",
integer_width: "पूर्णांक चौड़ाई",
@@ -101,7 +108,7 @@ export const hi = {
precision_def: "कुल अनुमत अंक (दशमलव से पहले और बाद)।",
scale_def: "दशमलव के बाद अनुमत अंक।",
no_default: "कोई डिफ़ॉल्ट मान नहीं",
random_uuid: "रैंडम UUID",
random_uuid: "रैंडम UUID",
time_default_value: {
no_value: "कोई मान नहीं",
custom: "कस्टम समय",
@@ -136,6 +143,21 @@ random_uuid: "रैंडम UUID",
description: "आपकी स्कीमा में तालिकाओं के बीच एक परिपत्र विदेशी कुंजी संबंध है। इसे ठीक करने के लिए",
suggestion: "नीचे दी गई चक्र उत्पन्न करने वाली रिलेशनशिप में से एक को हटाएं।",
remove_relationship: "रिलेशनशिप हटाएं"
},
validation: {
identifiers: {
table: "तालिका",
column: "कॉलम",
index: "इंडेक्स",
constraint: "बाधा"
},
error_message: "अमान्य पहचानकर्ता",
identifier_required: "${identifier} का नाम आवश्यक है",
identifier_required_description: "कृपया इस ${identifier} के लिए एक नाम प्रदान करें।",
invalid_identifier: "${identifier} का नाम अमान्य है",
invalid_identifier_description: "केवल अक्षरों, संख्याओं और अंडरस्कोर का उपयोग करें। ${identifier} का नाम किसी अक्षर से शुरू होना चाहिए।",
identifier_length: "${identifier} का नाम बहुत लंबा है",
identifier_length_description: "यह ${identifier} नाम ${dialect} द्वारा अनुमत अधिकतम ${length} बाइट की लंबाई से अधिक है।"
}
},
table: {
@@ -206,7 +228,7 @@ random_uuid: "रैंडम UUID",
import_database: {
title: "अपना डेटाबेस आयात करें",
import: "आयात करें",
view_docs : "दस्तावेज़ देखें" ,
view_docs: "दस्तावेज़ देखें",
import_options: "क्या आप निम्न के माध्यम से आयात करना चाहेंगे:",
import_error: "SQL पार्सिंग त्रुटि",
import_error_description: "हम आपका SQL आयात नहीं कर सके क्योंकि इसमें अमान्य सिंटैक्स है।",
@@ -270,8 +292,8 @@ random_uuid: "रैंडम UUID",
step3: "शीर्ष मेनू से <bold>File > Export > Database to SQL file</bold> पर जाएं।",
step4: "डायलॉग में <bold>Export schema only</bold> चुनें और <bold>Save</bold> पर क्लिक करें।",
step5: "अंत में, <code>.sql</code> फ़ाइल की सामग्री को नीचे दिए गए <bold>code editor</bold> में कॉपी करें।"
} ,
ssms: {
},
ssms: {
"step1": "SQL Server Management Studio (SSMS) खोलें।",
"step2": "अपना डेटाबेस पर राइट-क्लिक करें, फिर संदर्भ मेनू से Tasks → Generate Scripts चुनें।",
"step3": "Choose Objects चरण में, Choose specific database objects चुनें, फिर सभी टेबल्स को चेक करें।",
+30 -7
View File
@@ -6,7 +6,7 @@ export const pt = {
tables: "Tabelas",
relationships: "Relacionamentos",
database: "Banco de dados",
documentation : "Documentação"
documentation: "Documentação"
},
color_picker: {
@@ -15,7 +15,7 @@ export const pt = {
navbar: {
rename_db: "Renomear banco de dados",
search: "Pesquisar",
command: "Digite um comando ou pesquise...",
},
db_controller: {
@@ -29,10 +29,12 @@ export const pt = {
show_code: "Mostrar código",
fields: "Campos",
indexes: "Índices",
no_index: "Nenhum índice encontrado",
create_new_index: "Criar um novo",
note: "Nota",
name: "Nome",
type: "Tipo",
required: "Obrigatório",
nullable: "Aceita Nulo",
select_fields: "Selecionar campos",
@@ -90,6 +92,12 @@ export const pt = {
text_setting: "Configuração de Texto",
charset: "Charset",
collation: "Colação",
no_charset: "Nenhum conjunto de caracteres",
no_collation: "Nenhuma ordenação",
input_tag_placeholder: "Insira valores, separados por vírgulas...",
multi_select_placeholder: "Selecione os campos...",
date_placeholder: "Escolha uma data",
time: "Hora",
scale: "Escala",
max_length: "Comprimento Máximo",
integer_width: "Largura do Inteiro",
@@ -102,7 +110,7 @@ export const pt = {
precision_def: "Dígitos totais permitidos (antes + depois do decimal).",
scale_def: "Dígitos permitidos após o decimal.",
no_default: "Sem valor padrão",
random_uuid: "UUID aleatório",
random_uuid: "UUID aleatório",
time_default_value: {
no_value: "Sem valor",
custom: "Hora personalizada",
@@ -137,6 +145,21 @@ random_uuid: "UUID aleatório",
description: "Seu esquema contém uma relação circular de chave estrangeira entre tabelas. Para corrigir,",
suggestion: "remova um dos relacionamentos listados abaixo que está causando o ciclo.",
remove_relationship: "Remover relacionamento"
},
validation: {
identifiers: {
table: "Tabela",
column: "Coluna",
index: "Índice",
constraint: "Restrição"
},
error_message: "Identificador inválido",
identifier_required: "O nome de ${identifier} é obrigatório",
identifier_required_description: "Forneça um nome para este ${identifier}.",
invalid_identifier: "Nome de ${identifier} inválido",
invalid_identifier_description: "Use apenas letras, números e sublinhados. O nome de ${identifier} deve começar com uma letra.",
identifier_length: "O nome de ${identifier} é muito longo",
identifier_length_description: "Este nome de ${identifier} excede o comprimento máximo de ${length} bytes permitido pelo ${dialect}."
}
},
table: {
@@ -207,7 +230,7 @@ random_uuid: "UUID aleatório",
import_database: {
title: "Importar seu Banco de Dados",
import: "Importar",
view_docs : "Ver documentação" ,
view_docs: "Ver documentação",
import_options: "Gostaria de importar usando:",
import_error: "Erro de Análise SQL",
import_error_description: "Não foi possível importar seu SQL porque contém sintaxe inválida.",
@@ -271,8 +294,8 @@ random_uuid: "UUID aleatório",
step3: "Vá para <bold>Arquivo > Exportar > Banco de Dados para Arquivo SQL</bold>.",
step4: "Na janela de diálogo, escolha <bold>Exportar apenas o esquema</bold> e clique em <bold>Salvar</bold>.",
step5: "Finalmente, copie o conteúdo do arquivo <code>.sql</code> para o <bold>editor de código</bold> abaixo."
} ,
ssms: {
},
ssms: {
"step1": "Abra o SQL Server Management Studio (SSMS).",
"step2": "Clique com o botão direito no seu banco de dados e selecione Tarefas → Gerar Scripts no menu de contexto.",
"step3": "Na etapa Escolher Objetos, selecione Escolher objetos específicos do banco de dados e marque todas as tabelas.",
+30 -5
View File
@@ -6,7 +6,7 @@ export const ru = {
tables: "Таблицы",
relationships: "Связи",
database: "База данных",
documentation : "Документация"
documentation: "Документация"
},
color_picker: {
default_color: "Цвет по умолчанию"
@@ -27,6 +27,9 @@ export const ru = {
show_code: "Показать код",
fields: "Поля",
indexes: "Индексы",
no_index: "Индексы не найдены",
create_new_index: "Создать новый",
note: "Заметка",
name: "Имя",
type: "Тип",
@@ -82,6 +85,13 @@ export const ru = {
text_setting: "Настройки текста",
charset: "Кодировка",
collation: "Сравнение",
no_charset: "Кодировка не указана",
no_collation: "Сортировка не указана",
input_tag_placeholder: "Введите значения через запятую...",
multi_select_placeholder: "Выберите поля...",
date_placeholder: "Выберите дату",
time: "Время",
scale: "Масштаб",
max_length: "Макс. длина",
integer_width: "Ширина целого числа",
@@ -94,7 +104,7 @@ export const ru = {
precision_def: "Общее количество допустимых цифр (до и после запятой).",
scale_def: "Допустимое количество цифр после запятой.",
no_default: "Нет значения по умолчанию",
random_uuid: "Случайный UUID",
random_uuid: "Случайный UUID",
time_default_value: {
no_value: "Нет значения",
custom: "Произвольное время",
@@ -127,6 +137,21 @@ random_uuid: "Случайный UUID",
description: "В вашей схеме есть циклическая связь между таблицами. Чтобы устранить её,",
suggestion: "удалите одну из перечисленных ниже связей, вызывающих цикл.",
remove_relationship: "Удалить связь"
},
validation: {
identifiers: {
table: "Таблица",
column: "Столбец",
index: "Индекс",
constraint: "Ограничение"
},
error_message: "Недопустимый идентификатор",
identifier_required: "Имя ${identifier} обязательно",
identifier_required_description: "Пожалуйста, укажите имя для этого ${identifier}.",
invalid_identifier: "Недопустимое имя ${identifier}",
invalid_identifier_description: "Используйте только буквы, цифры и символы подчёркивания. Имя ${identifier} должно начинаться с буквы.",
identifier_length: "Имя ${identifier} слишком длинное",
identifier_length_description: "Это имя ${identifier} превышает максимальную длину в ${length} байт, разрешённую ${dialect}."
}
},
table: {
@@ -191,12 +216,12 @@ random_uuid: "Случайный UUID",
delete_database_content: "Это действие необратимо и навсегда удалит диаграмму.",
delete: "Удалить",
empty_diagram: "Пустая диаграмма",
create_and_import : "Создать и импортировать" ,
create_and_import: "Создать и импортировать",
import_database: {
title: "Импортируйте вашу базу данных",
import: "Импорт",
view_docs : "Просмотреть документацию" ,
view_docs: "Просмотреть документацию",
import_options: "Вы хотите импортировать с помощью:",
import_error: "Ошибка разбора SQL",
import_error_description: "Не удалось импортировать SQL, так как он содержит недопустимый синтаксис.",
@@ -260,7 +285,7 @@ random_uuid: "Случайный UUID",
step4: "Выберите <bold>Экспортировать только схему</bold> и нажмите <bold>Сохранить</bold>.",
step5: "Скопируйте содержимое <code>.sql</code> файла в редактор ниже."
},
ssms: {
ssms: {
"step1": "Откройте SQL Server Management Studio (SSMS).",
"step2": "Щёлкните правой кнопкой мыши по вашей базе данных, затем выберите Tasks → Generate Scripts в контекстном меню.",
"step3": "В шаге Choose Objects выберите Select specific database objects и отметьте все таблицы.",
+26 -1
View File
@@ -28,6 +28,9 @@ export const zh = {
show_code: "显示代码",
fields: "字段",
indexes: "索引",
no_index: "未找到索引",
create_new_index: "创建新的索引",
note: "备注",
name: "名称",
type: "类型",
@@ -90,6 +93,13 @@ export const zh = {
text_setting: "文本设置",
charset: "字符集",
collation: "排序规则",
no_charset: "无字符集",
no_collation: "无排序规则",
input_tag_placeholder: "输入值,使用逗号分隔...",
multi_select_placeholder: "选择字段...",
date_placeholder: "选择日期",
time: "时间",
scale: "小数位数",
max_length: "最大长度",
integer_width: "整数宽度",
@@ -102,7 +112,7 @@ export const zh = {
precision_def: "允许的总位数(小数点前+后)。",
scale_def: "小数点后的位数。",
no_default: "无默认值",
random_uuid: "随机 UUID",
random_uuid: "随机 UUID",
time_default_value: {
no_value: "无值",
custom: "自定义时间",
@@ -138,6 +148,21 @@ random_uuid: "随机 UUID",
description: "您的架构中存在表之间的循环外键关系。要修复它,",
suggestion: "请移除以下导致循环的关系之一。",
remove_relationship: "移除关系"
},
validation: {
identifiers: {
table: "表",
column: "列",
index: "索引",
constraint: "约束"
},
error_message: "标识符无效",
identifier_required: "${identifier} 名称是必需的",
identifier_required_description: "请为此 ${identifier} 提供一个名称。",
invalid_identifier: "${identifier} 名称无效",
invalid_identifier_description: "只能使用字母、数字和下划线。${identifier} 名称必须以字母开头。",
identifier_length: "${identifier} 名称过长",
identifier_length_description: "此 ${identifier} 名称超过了 ${dialect} 允许的最大长度 ${length} 字节。"
}
},
+16
View File
@@ -77,6 +77,22 @@ export enum ImportMethodType {
}
export const IDENTIFIER_REGEX = /^[\p{L}_][\p{L}\p{N}_]*$/u;
export const IDENTIFIER_MAX_LENGTH: Record<DatabaseDialect, number> = {
[DatabaseDialect.POSTGRES]: 63, // NAMEDATALEN - 1 (63 bytes)
[DatabaseDialect.MYSQL]: 64, // 64 characters
[DatabaseDialect.MARIADB]: 64, // 64 characters
[DatabaseDialect.MSSQL]: 128, // 128 characters
[DatabaseDialect.ORACLE]: 128, // 128 characters (30 in pre-12c)
[DatabaseDialect.SQLITE]: 255 // Practically unlimited; application budget
};
export type IDENTIFIER_ERROR = null | "max_length" | "empty" | "invalid" ;
export interface ImportDatabaseMethod {
id: string;
name: string;
@@ -51,7 +51,7 @@ interface DatabaseOperationsContextType {
createIndex: (index: IndexInsertType) => Promise<QueryResult>,
editIndex: (index: IndexInsertType) => Promise<QueryResult>,
deleteIndex: (id: string) => Promise<void>,
editFieldIndices: (indexId: string, fieldIds: string[]) => Promise<void>,
editFieldIndices: (indexId: string, fieldIds: string[] , deleteIds : string[]) => Promise<void>,
// relationship operations
createRelationship: (relationship: RelationshipInsertType) => Promise<QueryResult>,
editRelationship: (relationship: RelationshipInsertType) => Promise<QueryResult>,
@@ -256,18 +256,21 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
const editIndex = useCallback(async (index: IndexInsertType): Promise<QueryResult> => {
return await db.update(indices).set(index).where(eq(indices.id, index.id));
}, [db]);
const editFieldIndices = useCallback((indexId: string, fieldIds: string[]): Promise<void> => {
const editFieldIndices = useCallback((indexId: string, fieldIds: string[], deleteIds: string[]): Promise<void> => {
if (!currentDatabaseId) {
throw Error("No Database selected")
}
return db.transaction(async (tx) => {
await tx.delete(field_indices).where(eq(field_indices.indexId, indexId));
await tx.delete(field_indices).where(inArray(field_indices.id, deleteIds));
if (fieldIds.length > 0)
await tx.insert(field_indices).values(fieldIds.map((fieldId: string) => ({
id: v4(),
fieldId,
indexId,
databaseId: currentDatabaseId
})))
})
}, [db])
}, [db, currentDatabaseId])
// CRUD operations for Relationships
const createRelationship = useCallback(async (relationship: RelationshipInsertType): Promise<QueryResult> => {
if (currentDatabaseId) {
+18
View File
@@ -6,6 +6,7 @@ import { TableType } from "@/lib/schemas/table-schema";
import { excludeFields } from "./utils";
import { IndexType } from "@/lib/schemas/index-schema";
import { FieldIndexType } from "@/lib/schemas/field_index-schema";
import { DatabaseDialect, IDENTIFIER_ERROR, IDENTIFIER_MAX_LENGTH, IDENTIFIER_REGEX } from "@/lib/database";
// Define the possible operations that can be performed when diffing databases
export type DBDiffOperation =
@@ -309,3 +310,20 @@ export function normalizeDatabase(db: DatabaseType): any {
export const isValidIdentifier = (value: string , dialect : DatabaseDialect | undefined): IDENTIFIER_ERROR => {
if (value.length == 0)
return "empty" ;
if ( !IDENTIFIER_REGEX.test(value) ) {
return "invalid" ;
}
const max_length = dialect ? IDENTIFIER_MAX_LENGTH[dialect] : 255 ;
if (value.length > max_length)
return "max_length" ;
return null ;
}
+38 -25
View File
@@ -6,7 +6,7 @@ import { DataType } from '@/lib/schemas/data-type-schema';
import { FieldInsertType } from '@/lib/schemas/field-schema';
import { IndexInsertType } from '@/lib/schemas/index-schema';
import { Cardinality, RelationshipInsertType } from '@/lib/schemas/relationship-schema';
import { TableInsertType } from '@/lib/schemas/table-schema';
import { TableInsertType } from '@/lib/schemas/table-schema';
import { init, parse, ReferentialAction } from '@guanmingchiu/sqlparser-ts';
import { v4 } from 'uuid';
@@ -51,7 +51,7 @@ export class BaseSqlImporter {
fk_constraints.push(...tableFkConstraints);
}
} catch (error) {
} catch (error) {
errors.push(error as Error);
}
@@ -145,14 +145,27 @@ export class BaseSqlImporter {
} catch (error) {
errors.push(error as Error);
}
}
}
if (tables.length == 0)
throw Error("Can't parse sql")
throw Error("Can't parse sql")
return {
tables,
relationships,
indexes,
tables: tables.map((table: TableInsertType) => ({
...table,
name: table.name.slice(0, 255),
fields: table.fields?.map((field: FieldInsertType) => ({
...field,
name: field.name.slice(0, 255)
}))
})),
relationships: relationships.map((relationship: RelationshipInsertType) => ({
...relationship,
name: relationship.name?.slice(0, 255)
})),
indexes: indexes.map((index: IndexInsertType) => ({
...index,
name: index.name.slice(0, 255)
})),
errors
}
@@ -258,12 +271,12 @@ export class BaseSqlImporter {
let fk_constraint: any | undefined;
let params: any[] | undefined;
// if it's a basic type like (texts , booleans .. ect) we get the name directly
// if it's a type that take params , the data type become an object
if (typeof ast.data_type === "string") {
typeName = ast.data_type.toLowerCase();
} else if (typeof ast.data_type === "object" && !ast.data_type.Custom) {
const attributeName = Object.keys(ast.data_type)[0];
@@ -289,10 +302,10 @@ export class BaseSqlImporter {
}
typeName = attributeName.toLowerCase();
if (typeName == "timestamp" && this.dialect == DatabaseDialect.ORACLE && ast.data_type[attributeName].length >= 2 && ast.data_type[attributeName][1] == "WithTimeZone") {
typeName += "withtimezone" ;
}
if (typeName == "timestamp" && this.dialect == DatabaseDialect.ORACLE && ast.data_type[attributeName].length >= 2 && ast.data_type[attributeName][1] == "WithTimeZone") {
typeName += "withtimezone";
}
}
else if (ast.data_type.Custom && Array.isArray(ast.data_type.Custom)) {
@@ -300,9 +313,9 @@ export class BaseSqlImporter {
extraParam = Number(ast.data_type.Custom[1]?.[0]);
typeName = ast.data_type.Custom[0]?.[0]?.Identifier?.value.toLowerCase();
}
const dataType: DataType | undefined = this.processDataType(typeName as string);
if (!dataType) {
@@ -478,9 +491,9 @@ export class BaseSqlImporter {
return ForeignKeyActions.NO_ACTION;
}
protected processDataType(typeName: string): DataType | undefined {
protected processDataType(typeName: string): DataType | undefined {
// get data type from the supported ones
return this.data_types.find((dataType: DataType) => {
let synonyms: string[] = dataType.synonyms ? JSON.parse(dataType.synonyms) : [];
@@ -491,28 +504,28 @@ export class BaseSqlImporter {
}
protected processDefaultValue(ast: any, dataType: DataType): string | undefined {
if (ast.Value && ast.Value.value) {
const value: any = ast.Value.value;
if (value.Number && Array.isArray(value.Number) && value.Number.length > 0 && !isNaN(Number(value.Number[0]))) {
return value.Number[0];
}
else if (value.SingleQuotedString || value.DoubleQuotedString) {
const valuesArray = Object.values(value);
if (valuesArray.length > 0)
return valuesArray[0] as string;
}
else if (value.Boolean) {
return "true";
}
else if (value.NationalStringLiteral) {
return value.NationalStringLiteral
}
} else if (ast.Function) {
if (dataType.type == DataTypes.TIME) {
if (ast.Function.name?.[0]?.Identifier?.value && this.isCurrentTimesTampFunction(ast.Function.name?.[0]?.Identifier?.value))
@@ -529,7 +542,7 @@ export class BaseSqlImporter {
return valuesArray[0] as string;
}
}
return undefined;
}
+11 -10
View File
@@ -68,11 +68,11 @@ export abstract class BaseSQLRenderer extends BaseDatabaseRenderer {
return {
column: {
type: "column_ref",
column: {
column: (this.dialect == DatabaseDialect.MYSQL || this.dialect == DatabaseDialect.MARIADB) ? field.name : ({
expr: {
type: "default", value: field.name,
type: "double_quote_string", value: field.name,
}
},
}),
},
default_val,
unique,
@@ -139,12 +139,12 @@ export abstract class BaseSQLRenderer extends BaseDatabaseRenderer {
{
type: "column_ref",
table: null,
column: {
column: (this.dialect == DatabaseDialect.MYSQL || this.dialect == DatabaseDialect.MARIADB) ? foreignKey.name : ({
expr: {
type: "default",
type: "double_quote_string",
value: foreignKey.name
}
},
}),
}
],
constraint_type: "FOREIGN KEY",
@@ -155,12 +155,12 @@ export abstract class BaseSQLRenderer extends BaseDatabaseRenderer {
{
type: "column_ref",
table: null,
column: {
column: (this.dialect == DatabaseDialect.MYSQL || this.dialect == DatabaseDialect.MARIADB) ? primaryKey.name : ({
expr: {
type: "default",
type: "double_quote_string",
value: primaryKey.name
}
},
})
}
],
table: [
@@ -292,8 +292,9 @@ export abstract class BaseSQLRenderer extends BaseDatabaseRenderer {
}
protected startTransaction(): string {
return "BEGIN TRANSACTION;" ;
return "START TRANSACTION;";
}
protected commit(): string {
@@ -9,7 +9,7 @@ import { DataType } from "@/lib/schemas/data-type-schema";
import { DatabaseDialect } from "@/lib/database";
import { format } from 'sql-formatter';
import { AST } from "node-sql-parser";
export default class MSSqlRenderer extends BaseDatabaseRenderer {
@@ -33,7 +33,7 @@ ${this.commit()}
protected createTableAst(table: TableType): ASTStatment[] | ASTStatment {
const ast: any = super.createTableAst(table);
let indexes: string = ast.indices_ast.filter((index: string) => index != null).join("\n")
// get primary keys and check if we hame more than one
@@ -43,7 +43,7 @@ ${this.commit()}
constraints = this.getPrimaryKeyContraint(primaryKeys, table)
}
return `
CREATE TABLE ${table.name} (
CREATE TABLE "${table.name}" (
${(ast as any).field_definitions.join(",\n")}
${constraints ? ',\n' + constraints : ""}
);
@@ -80,21 +80,21 @@ ${this.commit()}
let defaultValue: any = "";
if (ast.default_value !== undefined && ast.default_value !== null && !dropDefaultValue) {
defaultValue = `CONSTRAINT DF_${table.name}_${field.name} DEFAULT ${ast.default_value}`;
defaultValue = `CONSTRAINT "DF_${table.name}_${field.name}" DEFAULT ${ast.default_value}`;
}
if (!ast.primary_key) {
unique = field.unique ? `CONSTRAINT UQ_${table.name}_${field.name} UNIQUE` : "";
unique = field.unique ? `CONSTRAINT "UQ_${table.name}_${field.name}" UNIQUE` : "";
}
const dataType = field.type.name == "uuid" ? "RAW(16)" : ast.dataType;
return `${field.name} ${dataType}${options} ${autoIncrement} ${nullable} ${defaultValue} ${unique} `;
return `"${field.name}" ${dataType}${options} ${autoIncrement} ${nullable} ${defaultValue} ${unique} `;
}
protected processDefaultValue(field: FieldType): AST | null {
const ast: any = super.processDefaultValue(field);
if (ast && ast.default_value_type) {
if (ast && ast.default_value_type) {
if (ast.default_value_type == "single_quote_string") {
ast.default_value = `'${ast.default_value}'`
}
@@ -128,25 +128,25 @@ ${this.commit()}
else
ast.default_value = "NEWID()"
}
}
if ( ast.default_value_type == "bool") {
if (ast.default_value) {
ast.default_value = 1 ;
}else {
ast.default_value = 0 ;
if (ast.default_value_type == "bool") {
if (ast.default_value) {
ast.default_value = 1;
} else {
ast.default_value = 0;
}
}
}
return ast;
}
protected createRelationshipAst(relationship: RelationshipType): ASTStatment {
const { primaryKey, foreignKey, sourceTable, targetTable } = super.createRelationshipAst(relationship) as any;
const constraintName: string = relationship.name ? " CONSTRAINT " + relationship.name : "";
const constraintName: string = relationship.name ? " CONSTRAINT \"" + relationship.name + "\"" : "";
const onDeleteFKAction: string | null = this.foreignKeyActionToAst(relationship.onDelete as ForeignKeyActions);
const onUpdateFKAction: string | null = this.foreignKeyActionToAst(relationship.onUpdate as ForeignKeyActions);
@@ -157,9 +157,9 @@ ${this.commit()}
const FKActions: string = [onDeleteAction, onUpdateAction].join(" ");
return `
ALTER TABLE ${targetTable.name}
ADD ${constraintName} FOREIGN KEY (${foreignKey.name})
REFERENCES ${sourceTable.name}(${primaryKey.name}) ${FKActions};
ALTER TABLE "${targetTable.name}"
ADD ${constraintName} FOREIGN KEY ("${foreignKey.name}")
REFERENCES "${sourceTable.name}"("${primaryKey.name}") ${FKActions};
`
}
@@ -175,29 +175,30 @@ ${this.commit()}
}
return null;
}
protected createIndexAst(table: TableType, index: IndexType): ASTStatment {
const unique: string = index.unique ? " UNIQUE" : "";
let columns: string[] | string = index.fields.map((field: FieldType) => field.name);
let columns: string[] | string = index.fields.map((field: FieldType) => `"${field.name}"`);
if (columns.length == 0)
return null;
columns = `(${columns.join(",")})`
return `CREATE${unique} INDEX ${index.name} ON ${table.name} ${columns} ;`
return `CREATE${unique} INDEX "${index.name}" ON "${table.name}" ${columns} ;`
}
protected getPrimaryKeyContraint(fields: FieldType[], table?: TableType): ASTStatment {
const pks: string[] = fields.map((field: FieldType) => field.name);
return `CONSTRAINT PK_${table?.name} PRIMARY KEY (${pks.join(",")})`
}
const pks: string[] = fields.map((field: FieldType) => `"${field.name}"`);
return `CONSTRAINT "PK_${table?.name}" PRIMARY KEY (${pks.join(",")})`
}
protected startTransaction(): string {
return `BEGIN TRANSACTION;`
}
protected commit(): string {
return "COMMIT;"
}
}
+17 -17
View File
@@ -33,7 +33,7 @@ export default class OracleRenderer extends BaseDatabaseRenderer {
pk_constraint = "";
let indexes: string = ast.indices_ast.filter((index: string) => index != null).join("\n")
return `
CREATE TABLE ${table.name} (
CREATE TABLE "${table.name}" (
${(ast as any).field_definitions.join(",\n")}
${pk_constraint}
);
@@ -88,19 +88,19 @@ export default class OracleRenderer extends BaseDatabaseRenderer {
}
else if (dataType === "INTERVAL DAY TO SECOND" && options) {
dataType = `INTERVAL DAY${options} TO SECOND`
}
else if (dataType === "INTERVAL YEAR TO MONTH" && options) {
dataType = `INTERVAL YEAR${options} TO MONTH`
}
else
else if (dataType === "INTERVAL YEAR TO MONTH" && options) {
dataType = `INTERVAL YEAR${options} TO MONTH`
}
else
dataType = dataType + options
//const dataType = field.type.name == "uuid" ? "RAW(16)" : ast.dataType;
return `${field.name} ${dataType} ${defaultValue} ${nullable} ${unique} ${autoIncrement} ${primaryKey}`;
return `"${field.name}" ${dataType} ${defaultValue} ${nullable} ${unique} ${autoIncrement} ${primaryKey}`;
}
protected processDefaultValue(field: FieldType): AST | null {
@@ -144,26 +144,26 @@ export default class OracleRenderer extends BaseDatabaseRenderer {
protected createIndexAst(table: TableType, index: IndexType): ASTStatment {
const unique: string = index.unique ? " UNIQUE" : "";
let columns: string[] | string = index.fields.map((field: FieldType) => field.name);
let columns: string[] | string = index.fields.map((field: FieldType) => `"${field.name}"`);
if (columns.length == 0)
return null;
columns = `(${columns.join(",")})`
return `CREATE${unique} INDEX ${index.name} ON ${table.name} ${columns} ;`
return `CREATE${unique} INDEX "${index.name}" ON "${table.name}" ${columns} ;`
}
protected createRelationshipAst(relationship: RelationshipType): ASTStatment {
const { primaryKey, foreignKey, sourceTable, targetTable } = super.createRelationshipAst(relationship) as any;
const constraintName: string = relationship.name ? " CONSTRAINT " + relationship.name : "";
const constraintName: string = relationship.name ? " CONSTRAINT \"" + relationship.name + "\"" : "";
const FKAction: string | null = this.foreignKeyActionToAst(relationship.onDelete as ForeignKeyActions);
const onDeleteAction: string = FKAction ? `ON DELETE ${FKAction}` : ""
return `
ALTER TABLE ${targetTable.name}
ADD ${constraintName} FOREIGN KEY (${foreignKey.name})
REFERENCES ${sourceTable.name}(${primaryKey.name}) ${onDeleteAction};
ALTER TABLE "${targetTable.name}"
ADD ${constraintName} FOREIGN KEY ("${foreignKey.name}")
REFERENCES "${sourceTable.name}"("${primaryKey.name}") ${onDeleteAction};
`
}
@@ -180,9 +180,9 @@ export default class OracleRenderer extends BaseDatabaseRenderer {
return null;
}
protected getPrimaryKeyContraint(fields: FieldType[]): ASTStatment {
protected getPrimaryKeyContraint(fields: FieldType[]): ASTStatment {
//throw new Error("Method not implemented.");
const pks: string[] = fields.map((field: FieldType) => field.name);
const pks: string[] = fields.map((field: FieldType) => `"${field.name}"`);
return `
PRIMARY KEY (${pks.join(",")})
`
@@ -107,4 +107,9 @@ ${this.commit()}
throw new Error("Method not implemented.");
}
protected startTransaction(): string {
return "BEGIN;";
}
}