Relationship controller integrated with the api

This commit is contained in:
KarimTamani
2025-05-10 15:17:29 +01:00
parent 2ad2105d9d
commit 69b49b2b2b
20 changed files with 578 additions and 157 deletions
+10 -7
View File
@@ -4,18 +4,18 @@ import { Key } from "react";
interface AutocompleteProps {
items: any[]
items?: any[]
label?: string,
defaultSelection: string | undefined
defaultSelection?: string | undefined
onSelectionChange?: (item: any) => void,
placeholder?: string
isDisabled?: boolean
selectedItem?: Key
}
const Autocomplete: React.FC<AutocompleteProps> = ({ items, label = "name", onSelectionChange, defaultSelection, placeholder }) => {
const Autocomplete: React.FC<AutocompleteProps> = ({ items, label = "name", onSelectionChange, defaultSelection, placeholder, isDisabled, selectedItem }) => {
const onItemChange = (item: Key | null) => {
onSelectionChange && onSelectionChange(item);
@@ -23,18 +23,21 @@ const Autocomplete: React.FC<AutocompleteProps> = ({ items, label = "name", onSe
return (
<HeroUiAutocomplete
radius="sm"
isDisabled={isDisabled}
className="w-full"
defaultItems={items}
defaultItems={items || []}
size="sm"
onSelectionChange={onItemChange}
defaultSelectedKey={defaultSelection as any}
variant="bordered"
aria-label={placeholder}
placeholder={placeholder}
selectedKey={selectedItem as any }
>
{(item: any) => <AutocompleteItem key={item.id}>{item[label]}</AutocompleteItem>}
</HeroUiAutocomplete>
)
}
export default Autocomplete ;
export default Autocomplete;
@@ -0,0 +1,14 @@
import { Cardinality, RelationshipType } from "@/lib/schemas/relationship-schema";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
export const useRelationshipName = (relationship: RelationshipType) => {
const { t } = useTranslation();
const name: string = useMemo(() => {
return `${relationship.sourceTable.name}_${relationship.sourceField.name} - ${relationship.targetTable.name}_${relationship.targetField.name}_fk`
}, [relationship, t])
return {
name
}
}
+78
View File
@@ -0,0 +1,78 @@
import React, { ReactNode } from "react";
import {
Modal as HeroUiModal,
ModalContent,
ModalHeader,
ModalBody,
ModalFooter,
Button,
useDraggable,
} from "@heroui/react";
import { useTranslation } from "react-i18next";
interface ModalProps {
isOpen?: boolean,
onOpenChange: (open: boolean) => void,
className?: string,
backdrop?: "blur" | "transparent" | "opaque",
title: string,
children: ReactNode,
actionName?: string,
actionHandler?: () => void ,
isDisabled? : boolean
}
const Modal: React.FC<ModalProps> = ({ isOpen, onOpenChange, className, backdrop = "opaque", title, children, actionName = "Action" , actionHandler , isDisabled}) => {
const targetRef = React.useRef(null);
const { moveProps } = useDraggable({ targetRef, canOverflow: true, isDisabled: !isOpen });
const {t} = useTranslation() ;
const handleAction = (onClose: () => void) => {
actionHandler && actionHandler() ;
onClose()
}
return (
<HeroUiModal
ref={targetRef}
isOpen={isOpen}
onOpenChange={onOpenChange}
className={className}
backdrop={backdrop}
radius="sm"
>
<ModalContent>
{(onClose) => (
<>
<ModalHeader {...moveProps} className="flex flex-row gap-1" >
{title}
</ModalHeader>
<ModalBody>
{children}
</ModalBody>
<ModalFooter>
<div className="flex w-full justify-between">
<Button color="danger" variant="light" onPress={onClose} size="sm">
{t("modal.close")}
</Button>
<Button color="primary" onPress={() => handleAction(onClose)} size="sm" isDisabled={isDisabled}>
{actionName}
</Button>
</div>
</ModalFooter>
</>
)}
</ModalContent>
</HeroUiModal>
)
}
export default Modal;
+62 -45
View File
@@ -1,56 +1,73 @@
import { Language } from "../types";
export const en = {
export const en = {
translation : {
sidebar : {
tables : "Tables" ,
relationships : "Relationships"
} ,
db_controller : {
filter : "Filter" ,
add_table : "Add Table" ,
add_field : "Add Field" ,
add_index : "Add Index" ,
add_relationship : "Add Relationship" ,
show_code : "Show code" ,
fields : "Fields" ,
indexes : "Indexes" ,
note : "Note" ,
name : "Name" ,
type : "Type" ,
primary_key : "Primary Key" ,
nullable : "Nullable" ,
select_fields : "Select fields" ,
unique : "Unique" ,
table_note : "Table note" ,
collapse : "Collapse All" ,
primary_table : "Primary Table" ,
referenced_table : "Referenced Table" ,
cardinality :{
name : "Cardinality" ,
one_to_one : "One to One" ,
one_to_many : "One to Many" ,
many_to_one : "Many to One" ,
many_to_many : "Many to Many" ,
} ,
delete : "Delete" ,
field_setting : "Field Setting" ,
table_actions : "Field Actions" ,
field_note : "Field note" ,
delete_field : "Delete Field" ,
},
table : {
double_click : "Double click to edit"
translation: {
sidebar: {
tables: "Tables",
relationships: "Relationships"
},
modal: {
close: "Close",
create: "Create"
},
db_controller: {
filter: "Filter",
add_table: "Add Table",
add_field: "Add Field",
add_index: "Add Index",
add_relationship: "Add Relationship",
show_code: "Show code",
fields: "Fields",
indexes: "Indexes",
note: "Note",
name: "Name",
type: "Type",
nullable: "Nullable",
select_fields: "Select fields",
unique: "Unique",
table_note: "Table note",
collapse: "Collapse All",
primary_key: "Primary Key",
foreign_key: "Foreign Key",
source_table: "Source Table",
target_table: "Target Table",
select_table: "Select table",
select_field: "Select field",
cardinality: {
name: "Cardinality",
one_to_one: "One to One",
one_to_many: "One to Many",
many_to_one: "Many to One",
many_to_many: "Many to Many",
},
delete: "Delete",
field_setting: "Field Setting",
table_actions: "Field Actions",
field_note: "Field note",
delete_field: "Delete Field",
create_relationship: "Create Relationship",
relationship_error: "To create a relationship, the primary key and foreign key must be of the same type.",
},
table: {
double_click: "Double click to edit"
}
}
}
}
export const enLanguage : Language = {
name : "English" ,
export const enLanguage: Language = {
name: "English",
nativeName: 'English',
code: 'en',
}
-22
View File
@@ -1,22 +0,0 @@
export interface Relationship {
id: string;
name: string;
sourceSchema?: string;
sourceTableId: string;
targetSchema?: string;
targetTableId: string;
sourceFieldId: string;
targetFieldId: string;
sourceCardinality: Cardinality;
targetCardinality: Cardinality;
createdAt: number;
}
export type RelationshipType =
| 'one_to_one'
| 'one_to_many'
| 'many_to_one'
| 'many_to_many';
export type Cardinality = 'one' | 'many';
+4 -2
View File
@@ -3,16 +3,18 @@ import { DrizzleAppSchema, wrapPowerSyncWithDrizzle } from '@powersync/drizzle-d
import { data_types } from './data-type-schema';
import { tables, tablesRelations } from './table-schema';
import { fields, fieldsRelations } from './field-schema';
import { relationshipRelations, relationships } from './relationship-schema';
export const drizzleSchema = {
data_types ,
tables ,
fields ,
relationships ,
// relationships
tablesRelations ,
fieldsRelations
fieldsRelations ,
relationshipRelations ,
};
// Infer the PowerSync schema from your Drizzle schema
+5
View File
@@ -2,6 +2,7 @@ import { sqliteTable, text, real, integer } from 'drizzle-orm/sqlite-core';
import { tables } from './table-schema';
import { data_types } from './data-type-schema';
import { InferInsertModel, InferSelectModel, relations } from 'drizzle-orm';
import { relationships } from './relationship-schema';
export const fields = sqliteTable("fields", {
@@ -24,7 +25,11 @@ export const fieldsRelations = relations(fields, ({ one }) => ({
table: one(tables, {
fields: [fields.tableId],
references: [tables.id],
}),
sourceRelations: one(relationships),
targetRelations: one(relationships),
}));
+83
View File
@@ -0,0 +1,83 @@
import {
sqliteTable,
text,
} from 'drizzle-orm/sqlite-core';
import { InferInsertModel, InferSelectModel, relations } from 'drizzle-orm';
import { tables, TableType } from './table-schema';
import { fields, FieldType } from './field-schema';
export const relationships = sqliteTable('relationships', {
id: text('id').primaryKey().notNull(),
name: text('name'),
sourceTableId: text('sourceTableId')
.notNull()
.references(() => tables.id, { onDelete: 'cascade' }),
targetTableId: text('targetTableId')
.notNull()
.references(() => tables.id, { onDelete: 'cascade' }),
sourceFieldId: text('sourceFieldId')
.notNull()
.references(() => fields.id, { onDelete: 'cascade' }),
targetFieldId: text('targetFieldId')
.notNull()
.references(() => fields.id, { onDelete: 'cascade' }),
cardinality: text('cardinality', {
enum: [
'one_to_one',
'one_to_many',
'many_to_one',
'many_to_many',
],
})
.notNull()
.default('one_to_many'),
sourceAliasName: text('sourceAliasName'),
targetAliasName: text('targetAliasName'),
createdAt: text('createdAt'),
});
export const relationshipRelations = relations(relationships, ({ one }) => ({
sourceTable: one(tables, {
fields: [relationships.sourceTableId],
references: [tables.id],
}),
targetTable: one(tables, {
fields: [relationships.targetTableId],
references: [tables.id],
}),
sourceField: one(fields, {
fields: [relationships.sourceFieldId],
references: [fields.id],
}),
targetField: one(fields, {
fields: [relationships.targetFieldId],
references: [fields.id],
}),
}));
export interface RelationshipType extends InferSelectModel<typeof relationships> {
sourceTable : TableType ,
targetTable : TableType ,
sourceField : FieldType ,
targetField : FieldType ,
};
export interface RelationshipInsertType extends InferInsertModel<typeof relationships> { };
export enum Cardinality {
one_to_one = "one_to_one",
one_to_many = "one_to_many",
many_to_one = "many_to_one",
many_to_many = "many_to_many"
}
+8 -5
View File
@@ -1,6 +1,7 @@
import { InferInsertModel, InferSelectModel, relations } from 'drizzle-orm';
import { sqliteTable, text, real, integer } from 'drizzle-orm/sqlite-core';
import { fields, FieldType } from './field-schema';
import { relationships } from './relationship-schema';
export const tables = sqliteTable('tables', {
id: text('id')
@@ -25,15 +26,17 @@ export const tables = sqliteTable('tables', {
export const tablesRelations = relations(tables, ({ many }) => ({
fields: many(fields),
sourceRelations: many(relationships),
targetRelations: many(relationships),
}));
export interface TableType extends InferSelectModel<typeof tables> {
fields : FieldType[]
};
export interface TableType extends InferSelectModel<typeof tables> {
fields: FieldType[]
};
export interface TableInsertType extends InferInsertModel<typeof tables> {
fields? : FieldType[]
export interface TableInsertType extends InferInsertModel<typeof tables> {
fields?: FieldType[]
};
@@ -0,0 +1,133 @@
import Autocomplete from "@/components/auto-complete/auto-complete";
import { FieldType } from "@/lib/schemas/field-schema";
import { RelationshipInsertType } from "@/lib/schemas/relationship-schema";
import { TableType } from "@/lib/schemas/table-schema";
import { useDatabase } from "@/providers/database-provider/database-provider";
import { FileKey, FileMinus2, FileOutput, Key, KeyRound } from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
interface CreateRelationshipFormProps {
onRelationshipChanges: (relationship: RelationshipInsertType) => void,
onValidationChanges?: (isValid: boolean) => void
}
const CreateRelationshipForm: React.FC<CreateRelationshipFormProps> = ({ onRelationshipChanges, onValidationChanges }) => {
const [relationship, setRelationship] = useState<RelationshipInsertType>({
sourceTableId: "",
targetTableId: "",
sourceFieldId: "",
targetFieldId: ""
} as RelationshipInsertType)
const { tables } = useDatabase();
const { t } = useTranslation();
const sourceFields: FieldType[] = useMemo(() => {
const sourceTable: TableType | undefined = tables.find((table: TableType) => table.id == relationship.sourceTableId);
return sourceTable?.fields ? sourceTable.fields : [];
}, [tables, relationship.sourceTableId]);
const targetFields: FieldType[] = useMemo(() => {
const targetTable: TableType | undefined = tables.find((table: TableType) => table.id == relationship.targetTableId);
return targetTable?.fields ? targetTable.fields : [];
}, [tables, relationship.targetTableId]);
const fieldTypesMatches: boolean = useMemo(() => {
const sourceField: FieldType | undefined = sourceFields.find((field: FieldType) => field.id == relationship.sourceFieldId);
const targetField: FieldType | undefined = targetFields.find((field: FieldType) => field.id == relationship.targetFieldId);
if (sourceField && targetField)
return sourceField.typeId == targetField.typeId;
else
return true;
}, [relationship, sourceFields, targetFields])
useEffect(() => setRelationship({ ...relationship, sourceFieldId: "" }), [sourceFields]);
useEffect(() => setRelationship({ ...relationship, targetFieldId: "" }), [targetFields]);
useEffect(() => {
onRelationshipChanges && onRelationshipChanges(relationship)
}, [relationship , sourceFields , targetFields]);
useEffect(() => {
onValidationChanges && onValidationChanges ((relationship.sourceFieldId && relationship.targetFieldId && fieldTypesMatches) as boolean)
}, [relationship, fieldTypesMatches])
return (
<>
<div className="grid grid-cols-2 grid-rows-2 gap-4">
<div className="space-y-2">
<label className="font-medium flex text-slate-700 flex items-center gap-1 text-sm">
<FileOutput className="size-4 text-icon" />
{t("db_controller.source_table")}
</label>
<Autocomplete
items={tables}
placeholder={t("db_controller.select_table")}
onSelectionChange={(key) => setRelationship({ ...relationship, sourceTableId: key })}
/>
</div>
<div className="space-y-2">
<label className="font-medium flex text-slate-700 flex items-center gap-1 text-sm">
<FileMinus2 className="size-4 text-icon" />
{t("db_controller.target_table")}
</label>
<Autocomplete
items={tables}
placeholder={t("db_controller.select_table")}
onSelectionChange={(key) => setRelationship({ ...relationship, targetTableId: key })}
/>
</div>
<div className="space-y-2">
<label className="font-medium flex text-slate-700 flex items-center gap-1 text-sm">
<KeyRound className="size-4 text-icon" />
{t("db_controller.primary_key")}
</label>
<Autocomplete
placeholder={t("db_controller.select_field")}
items={sourceFields}
onSelectionChange={(key) => setRelationship({ ...relationship, sourceFieldId: key })}
isDisabled={!relationship.sourceTableId}
selectedItem={relationship.sourceFieldId}
/>
</div>
<div className="space-y-2">
<label className="font-medium flex text-slate-700 flex items-center gap-1 text-sm">
<FileKey className="size-4 text-icon" />
{t("db_controller.foreign_key")}
</label>
<Autocomplete
placeholder={t("db_controller.select_field")}
onSelectionChange={(key) => setRelationship({ ...relationship, targetFieldId: key })}
isDisabled={!relationship.targetTableId}
items={targetFields}
selectedItem={relationship.targetFieldId}
/>
</div>
</div>
{
!fieldTypesMatches &&
<div className="w-full text-danger text-sm ">
{t("db_controller.relationship_error")}
</div>
}
</>
)
}
export default CreateRelationshipForm;
@@ -1,45 +1,59 @@
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/tooltip/tooltip";
import { Button, Select, SelectItem } from "@heroui/react";
import { Cardinality, RelationshipInsertType, RelationshipType } from "@/lib/schemas/relationship-schema";
import { useDatabase } from "@/providers/database-provider/database-provider";
import { Button, Select, SelectItem, SharedSelection } from "@heroui/react";
import { ChevronsLeftRightEllipsis, FileMinus2, FileOutput, SquareArrowLeft, SquareArrowRight, Trash2 } from "lucide-react";
import { Key, useState } from "react";
import { useTranslation } from "react-i18next";
interface Props {
interface RelationshipAccordionBodyProps {
relationship: RelationshipType
}
export const cardinalities = [
{ key: "one2one", label: "One to One" },
{ key: "one2many", label: "One to Many" },
{ key: "many2one", label: "Many to One" },
{ key: "many2many", label: "Many 2 many" },
];
const RelationshipAccordionBody: React.FC<Props> = ({ }) => {
const RelationshipAccordionBody: React.FC<RelationshipAccordionBodyProps> = ({ relationship }) => {
const [cardinality, setCardinality] = useState(new Set([relationship.cardinality]));
const { editRelationship, deleteRelationship } = useDatabase();
const {t } = useTranslation() ;
const { t } = useTranslation();
const changeCardinality = (keys: SharedSelection) => {
if (keys.anchorKey != relationship.cardinality)
editRelationship({
id: relationship.id,
cardinality: keys.anchorKey as Cardinality
} as RelationshipInsertType);
setCardinality(keys as any);
}
const removeRelationship = () => {
deleteRelationship(relationship.id);
}
return (
<div className="w-full p-2 space-y-4">
<div className="flex">
<div className="w-full space-y-1">
<label className="font-medium flex text-slate-700 flex items-center gap-1 text-sm">
<FileOutput className="size-4" />
{t("db_controller.primary_table")}
{t("db_controller.source_table")}
</label>
<Tooltip>
<TooltipTrigger>
<span className="truncate text-left text-sm">
users(id)
{relationship.sourceTable.name}({relationship.sourceField.name})
</span>
</TooltipTrigger>
<TooltipContent>
users(id)
{relationship.sourceTable.name}({relationship.sourceField.name})
</TooltipContent>
</Tooltip>
</div>
@@ -47,18 +61,18 @@ const RelationshipAccordionBody: React.FC<Props> = ({ }) => {
<div className="w-full space-y-1">
<label className="font-medium flex text-slate-700 flex items-center gap-1 text-sm">
<FileMinus2 className="size-4" />
{t("db_controller.referenced_table")}
{t("db_controller.target_table")}
</label>
<Tooltip>
<TooltipTrigger>
<span className="truncate text-left text-sm ">
products(user_id)
{relationship.targetTable.name}({relationship.targetField.name})
</span>
</TooltipTrigger>
<TooltipContent>
products(user_id)
{relationship.targetTable.name}({relationship.targetField.name})
</TooltipContent>
</Tooltip>
</div>
@@ -72,16 +86,14 @@ const RelationshipAccordionBody: React.FC<Props> = ({ }) => {
className="w-full"
size="sm"
variant="bordered"
selectedKeys={cardinality}
onSelectionChange={changeCardinality}
>
<SelectItem key={"one2one"}>{t("db_controller.cardinality.one_to_one")}</SelectItem>
<SelectItem key={"one2many"}>{t("db_controller.cardinality.one_to_many")}</SelectItem>
<SelectItem key={"many2one"}>{t("db_controller.cardinality.many_to_one")}</SelectItem>
<SelectItem key={"many2many"}>{t("db_controller.cardinality.many_to_many")}</SelectItem>
<SelectItem key={Cardinality.one_to_one}>{t("db_controller.cardinality.one_to_one")}</SelectItem>
<SelectItem key={Cardinality.one_to_many}>{t("db_controller.cardinality.one_to_many")}</SelectItem>
<SelectItem key={Cardinality.many_to_one}>{t("db_controller.cardinality.many_to_one")}</SelectItem>
<SelectItem key={Cardinality.many_to_many}>{t("db_controller.cardinality.many_to_many")}</SelectItem>
</Select>
</div>
<div className="flex justify-center">
@@ -90,6 +102,7 @@ const RelationshipAccordionBody: React.FC<Props> = ({ }) => {
className="bg-transparent text-xs"
radius="sm"
size="sm"
onPressEnd={removeRelationship}
>
<Trash2 className="mr-1 size-3.5 text-danger" />
<div className="text-danger font-semibold">
@@ -1,18 +1,38 @@
import { useRelationshipName } from "@/components/hooks/use-relationship-name";
import { randomColor } from "@/lib/colors";
import { RelationshipInsertType, RelationshipType } from "@/lib/schemas/relationship-schema";
import { useDatabase } from "@/providers/database-provider/database-provider";
import { Button, cn, Input } from "@heroui/react";
import { Check, ChevronRight, EllipsisVertical, Focus, Pencil } from "lucide-react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
interface RelationshipAccordionHeaderProps {
isOpen?: boolean;
relationship: RelationshipType
}
const RelationshipAccordionHeader: React.FC<RelationshipAccordionHeaderProps> = ({ isOpen }) => {
const RelationshipAccordionHeader: React.FC<RelationshipAccordionHeaderProps> = ({ isOpen, relationship }) => {
const [editMode, setEditMode] = useState<boolean>(false);
const { name: defaultName } = useRelationshipName(relationship);
const { editRelationship } = useDatabase();
const [name, setName] = useState<string>(relationship.name ? relationship.name : defaultName)
const editRelationshipName = () => {
if (relationship.name || name.trim().toLocaleLowerCase() != defaultName)
editRelationship({
id: relationship.id,
name
} as RelationshipInsertType);
setEditMode(false);
}
return (
<div className="group w-full flex h-12 gap-1 flex p-2 items-center" >
<div className={cn(
@@ -21,8 +41,6 @@ const RelationshipAccordionHeader: React.FC<RelationshipAccordionHeaderProps> =
)}>
<ChevronRight className="size-4 text-icon" />
</div>
{
editMode && <>
<Input
@@ -30,7 +48,9 @@ const RelationshipAccordionHeader: React.FC<RelationshipAccordionHeaderProps> =
autoFocus
size="sm"
variant="bordered"
onBlur={() => setEditMode(false)}
value={name}
onValueChange={setName}
onBlur={editRelationshipName}
type="text"
className="rounded-md px-2 py-0.5 w-full border-blue-400 focus-visible:ring-0 dark:bg-slate-900 text-sm "
/>
@@ -38,7 +58,7 @@ const RelationshipAccordionHeader: React.FC<RelationshipAccordionHeaderProps> =
variant="light"
className="size-6 p-0 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200"
size="sm"
onPress={() => setEditMode(false)}
onPress={editRelationshipName}
isIconOnly
>
<Check className="size-4 text-icon" />
@@ -52,7 +72,7 @@ const RelationshipAccordionHeader: React.FC<RelationshipAccordionHeaderProps> =
<label
className="w-full truncate px-2 py-1 text-sm font-semibold text-black"
>
users one or many posts
{relationship.name ? relationship.name : defaultName}
</label>
<div className="hidden shrink-0 flex-row group-hover:flex">
@@ -1,10 +1,16 @@
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/tooltip/tooltip"
import { Accordion, AccordionItem, Button, Input } from "@heroui/react"
import { Accordion, AccordionItem, Button, Input, useDisclosure } from "@heroui/react"
import { Code, List, ListCollapse, Table, Workflow } from "lucide-react"
import { useState } from "react";
import { useCallback, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import RelationshipAccordionHeader from "./relationship-accordion-item/relationship-accordion-header";
import RelationshipAccordionBody from "./relationship-accordion-item/relationship-accordion-body";
import Modal from "@/components/modal/modal";
import CreateRelationshipForm from "./create-relationship-form/create-relationship-form";
import { RelationshipInsertType, RelationshipType } from "@/lib/schemas/relationship-schema";
import { useDatabase } from "@/providers/database-provider/database-provider";
import { v4 } from "uuid";
@@ -14,10 +20,26 @@ interface Props {
const RelationshipController: React.FC<Props> = ({ }) => {
const [items, setItems] = useState(["Item 1", "Item 2", "Item 3", "Item 4"]);
const [relationship, setRelationship] = useState<RelationshipInsertType | undefined>(undefined);
const [isValid, setIsValid] = useState<boolean>(false);
const { isOpen, onOpen, onOpenChange } = useDisclosure();
const { createRelationship, relationships } = useDatabase();
const { t } = useTranslation();
const [selectedRelationship, setSelectedRelationship] = useState(new Set([]));
const addRelationship = useCallback(() => {
const newRelationshipId: string = v4();
createRelationship({
id: newRelationshipId,
...relationship,
createdAt: new Date().toISOString()
} as RelationshipInsertType);
setSelectedRelationship(new Set([newRelationshipId]) as any);
}, [relationship]);
return (
<div className="w-full h-full flex flex-col gap-2">
@@ -34,6 +56,7 @@ const RelationshipController: React.FC<Props> = ({ }) => {
//setShowDBML((value) => !value)
console.log("hello world ")
}
>
<ListCollapse className="size-4" />
</Button>
@@ -62,6 +85,7 @@ const RelationshipController: React.FC<Props> = ({ }) => {
variant="solid"
radius="sm"
color="primary"
onPressEnd={onOpen}
startContent={
<Workflow className="h-4 w-4 " />
}
@@ -75,26 +99,44 @@ const RelationshipController: React.FC<Props> = ({ }) => {
<Accordion
hideIndicator
dividerProps={{
className : "bg-default-200"
className: "bg-default-200"
}}
isCompact
selectedKeys={selectedRelationship}
onSelectionChange={setSelectedRelationship as any}
>
{items.map(item => (
{relationships.map((relationship: RelationshipType) => (
<AccordionItem
key={item}
aria-label={item}
key={relationship.id}
aria-label={relationship.id}
classNames={{
trigger: "w-full hover:bg-default transition-all duration-200",
base: "rounded-md p-0 overflow-hidden",
trigger: "w-full hover:bg-default transition-all duration-200 h-12",
base: "rounded-md p-0 overflow-hidden",
}}
subtitle={
<RelationshipAccordionHeader/>
<RelationshipAccordionHeader relationship={relationship} />
}
>
<RelationshipAccordionBody/>
<RelationshipAccordionBody relationship={relationship} />
</AccordionItem>
))}
</Accordion>
</div>
<Modal
isOpen={isOpen}
onOpenChange={onOpenChange}
title={t("db_controller.create_relationship")}
actionName={t("modal.create")}
className="min-w-[520px]"
isDisabled={!isValid}
actionHandler={addRelationship}
>
<CreateRelationshipForm
onRelationshipChanges={setRelationship}
onValidationChanges={setIsValid}
/>
</Modal>
</div>
)
}
@@ -63,7 +63,6 @@ const FieldItem: React.FC<Props> = ({ field }) => {
} as FieldType);
}
const toggleNullable = (nullable: boolean) => {
editField({
id: field.id,
@@ -50,6 +50,7 @@ const TableAccordionBody: React.FC<TableAccordionBodyProps> = ({ table }) => {
return (
<div className="w-full">
<Accordion
isCompact
variant={"light"} selectionMode="multiple" selectedKeys={selectedKeys} onSelectionChange={setSelectedKeys as any}>
<AccordionItem key="fields" aria-label="Fields"
indicator={({ isOpen }) => (
@@ -92,7 +93,7 @@ const TableAccordionBody: React.FC<TableAccordionBodyProps> = ({ table }) => {
</div>
)}
classNames={{
trigger: "hover:bg-default"
trigger: "hover:bg-default h-6"
}}
subtitle={
<div className="flex gap-2 items-center font-medium p-1 w-full hover:underline text-slate-500 hover:text-slate-600 transition-all duration-200">
@@ -114,7 +115,7 @@ const TableAccordionBody: React.FC<TableAccordionBodyProps> = ({ table }) => {
</div>
)}
classNames={{
trigger: "hover:bg-default"
trigger: "hover:bg-default h-6"
}}
subtitle={
<div className="flex gap-2 items-center font-medium p-1 w-full hover:underline text-slate-500 hover:text-slate-600 transition-all duration-200">
@@ -101,13 +101,15 @@ const TablesController: React.FC<Props> = ({ }) => {
variant="splitted"
selectedKeys={selectedTable}
onSelectionChange={setSelectedTable as any}
isCompact
>
{tables.map((table: TableType) => (
<AccordionItem
key={table.id}
aria-label={table.name}
classNames={{
trigger: "w-full hover:bg-default transition-all duration-200",
trigger: "w-full h-12 hover:bg-default transition-all duration-200",
base: "rounded-md shadow p-0 overflow-hidden",
}}
subtitle={
@@ -1,6 +1,7 @@
import { DataType } from "@/lib/schemas/data-type-schema";
import { FieldInsertType, FieldType } from "@/lib/schemas/field-schema";
import { RelationshipInsertType, RelationshipType } from "@/lib/schemas/relationship-schema";
import { TableInsertType, TableType } from "@/lib/schemas/table-schema";
import { QueryResult } from "@powersync/web";
import { createContext } from "react";
@@ -10,17 +11,21 @@ import { createContext } from "react";
export interface DatabaseContextType {
tables: TableType[],
data_types: DataType[]
data_types: DataType[] ,
relationships : RelationshipType[] ,
// table operations
createTable: (table: TableInsertType) => Promise<QueryResult>,
editTable: (table: TableInsertType) => Promise<QueryResult>,
deleteTable: (id: string) => Promise<QueryResult>,
// field operations
createField: (field: FieldInsertType) => Promise<QueryResult>,
editField: (field: FieldInsertType) => Promise<QueryResult>,
deleteField: (id: string) => Promise<QueryResult>,
orderTableFields: (fields: FieldType[]) => Promise<QueryResult>
orderTableFields: (fields: FieldType[]) => Promise<QueryResult> ,
// relationship operations
createRelationship : ( relationship : RelationshipInsertType) => Promise<QueryResult> ,
editRelationship : ( relationship : RelationshipInsertType) => Promise<QueryResult> ,
deleteRelationship : ( id : string) => Promise<QueryResult> ,
}
@@ -28,12 +33,3 @@ export interface DatabaseContextType {
export default createContext<DatabaseContextType>({} as DatabaseContextType);
/*
return Promise.all(
fields.map((field : FieldType , index : number) => editField({id : field.id , sequence : index} as FieldType))
)
*/
@@ -8,6 +8,7 @@ import { toCompilableQuery } from "@powersync/drizzle-driver";
import { asc, desc, eq, sql } from "drizzle-orm";
import { QueryResult } from "@powersync/web";
import { FieldInsertType, fields, FieldType } from "@/lib/schemas/field-schema";
import { RelationshipInsertType, relationships, RelationshipType } from "@/lib/schemas/relationship-schema";
interface Props { children: React.ReactNode }
@@ -26,6 +27,18 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
})
));
const { data: relationshipsList } = useQuery(toCompilableQuery(
db.query.relationships.findMany({
with: {
sourceTable: true,
targetTable: true,
sourceField: true,
targetField: true,
},
orderBy: desc(tables.createdAt)
})
));
const { data: data_types } = useQuery(toCompilableQuery(
db.query.data_types.findMany()
));
@@ -52,24 +65,32 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
}
const orderTableFields = async (fields: FieldType[]): Promise<QueryResult> => {
const caseStatements = fields
.map((field: FieldType, index: number) => `WHEN '${field.id}' THEN ${index}`)
.join('\n ');
const ids = fields.map(u => `'${u.id}'`).join(',\n ');
const sql = `
UPDATE fields
SET sequence = CASE id
${caseStatements}
END
WHERE id IN (
${ids}
);`;
UPDATE fields
SET sequence = CASE id
${caseStatements}
END
WHERE id IN (
${ids}
);`;
return await powerSyncDb.execute(sql)
}
const createRelationship = async (relationship: RelationshipInsertType): Promise<QueryResult> => {
return await db.insert(relationships).values(relationship);
}
const editRelationship = async (relationship: RelationshipInsertType): Promise<QueryResult> => {
return await db.update(relationships).set(relationship).where(eq(relationships.id, relationship.id))
}
const deleteRelationship = async (id: string): Promise<QueryResult> => {
return await db.delete(relationships).where(eq(relationships.id, id));
}
return (
<DatabaseContext.Provider value={{
@@ -81,7 +102,13 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
editField,
deleteField,
orderTableFields,
createRelationship,
editRelationship,
deleteRelationship,
tables: tablesList as TableType[],
relationships : relationshipsList as RelationshipType[] ,
data_types
}}>
{children}
+5
View File
@@ -48,4 +48,9 @@ div[data-slot="mainWrapper"] button[data-slot="trigger"] {
div[data-slot="content"] hr[role="separator"] {
display: none ;
}
button[data-slot="trogger"] {
background-color: red !important;
}
+1 -1
View File
@@ -6,7 +6,7 @@ export default {
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
"./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}",
"./node_modules/@heroui/theme/dist/components/(button|code|dropdown|input|kbd|link|navbar|snippet|toggle|popover|ripple|spinner|menu|divider|form).js",
"./node_modules/@heroui/theme/dist/components/(button|code|dropdown|input|kbd|link|navbar|snippet|toggle|popover|ripple|spinner|menu|divider|form|modal).js",
],
darkMode: "class",