mirror of
https://github.com/stackrender/stackrender.git
synced 2026-09-10 03:05:42 +00:00
integrate the indices with the Api
This commit is contained in:
+11
-7
@@ -20,8 +20,8 @@ export const en = {
|
||||
add_table: "Add Table",
|
||||
add_field: "Add Field",
|
||||
add_index: "Add Index",
|
||||
delete_table : "Delete Table" ,
|
||||
duplicate : "Duplicate" ,
|
||||
delete_table: "Delete Table",
|
||||
duplicate: "Duplicate",
|
||||
add_relationship: "Add Relationship",
|
||||
show_code: "Show code",
|
||||
fields: "Fields",
|
||||
@@ -57,25 +57,29 @@ export const en = {
|
||||
},
|
||||
delete: "Delete",
|
||||
field_setting: "Field Setting",
|
||||
index_setting: "Index Setting",
|
||||
table_actions: "Table Actions",
|
||||
actions: "Actions",
|
||||
|
||||
field_note: "Field note",
|
||||
delete_field: "Delete Field",
|
||||
delete_index: "Delete Index",
|
||||
index_name: "Index name",
|
||||
|
||||
|
||||
create_relationship: "Create Relationship",
|
||||
relationship_error: "To create a relationship, the primary key and foreign key must be of the same type.",
|
||||
|
||||
|
||||
invalid_relationship: {
|
||||
title: "Invalid Relationship",
|
||||
description: "The source key type does not match the referenced key type. Please ensure both keys have the same data type."
|
||||
}
|
||||
},
|
||||
table: {
|
||||
double_click: "Double click to edit" ,
|
||||
overlapping_tables : "Overlapping Tables" ,
|
||||
show_more : "Show more" ,
|
||||
show_less : "Show less"
|
||||
double_click: "Double click to edit",
|
||||
overlapping_tables: "Overlapping Tables",
|
||||
show_more: "Show more",
|
||||
show_less: "Show less"
|
||||
},
|
||||
control_buttons: {
|
||||
redo: "Redo",
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
|
||||
|
||||
import { sqliteTable, text } from 'drizzle-orm/sqlite-core';
|
||||
|
||||
import { sqliteTable, text, primaryKey } from 'drizzle-orm/sqlite-core';
|
||||
import { indices } from './index-schema';
|
||||
import { fields } from './field-schema';
|
||||
import { InferInsertModel, InferSelectModel, relations } from 'drizzle-orm';
|
||||
|
||||
export const field_indices = sqliteTable("field_indices", {
|
||||
fieldId: text("fieldId").references(() => fields.id, { onDelete: "cascade" }),
|
||||
indexId: text("indexId").references(() => indices.id, { onDelete: "cascade" }),
|
||||
id: text('id')
|
||||
.primaryKey()
|
||||
.notNull()
|
||||
.unique(),
|
||||
fieldId: text("fieldId").notNull().references(() => fields.id, { onDelete: "cascade" }),
|
||||
indexId: text("indexId").notNull().references(() => indices.id, { onDelete: "cascade" }),
|
||||
});
|
||||
|
||||
|
||||
@@ -25,7 +26,7 @@ export const fieldIndicesRelationships = relations(field_indices, ({ one }) => (
|
||||
}))
|
||||
|
||||
|
||||
export interface FieldIndexType extends InferSelectModel<typeof field_indices> {};
|
||||
export interface FieldIndexType extends InferSelectModel<typeof field_indices> { };
|
||||
|
||||
|
||||
export interface FieldIndexInsertType extends InferInsertModel<typeof field_indices> { };
|
||||
@@ -1,11 +1,9 @@
|
||||
|
||||
|
||||
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
|
||||
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
|
||||
import { tables } from './table-schema';
|
||||
import { data_types, DataType } from './data-type-schema';
|
||||
import { InferInsertModel, InferSelectModel, relations } from 'drizzle-orm';
|
||||
import { relationships } from './relationship-schema';
|
||||
import { field_indices, FieldIndexType } from './field_index-schema';
|
||||
import { field_indices, FieldIndexInsertType, FieldIndexType } from './field_index-schema';
|
||||
import { fields } from './field-schema';
|
||||
|
||||
|
||||
@@ -19,22 +17,23 @@ export const indices = sqliteTable("indices", {
|
||||
name: text("name").notNull(),
|
||||
unique: integer("unique", { mode: "boolean" }),
|
||||
createdAt: text('createdAt'),
|
||||
|
||||
});
|
||||
|
||||
|
||||
export const indicesRlationships = relations(indices, ({ one , many}) => ({
|
||||
table : one(tables , {
|
||||
references : [tables.id] ,
|
||||
fields : [indices.tableId]
|
||||
}) ,
|
||||
fieldIndices : many(field_indices) ,
|
||||
fields : many(fields)
|
||||
export const indicesRlationships = relations(indices, ({ one, many }) => ({
|
||||
table: one(tables, {
|
||||
references: [tables.id],
|
||||
fields: [indices.tableId]
|
||||
}),
|
||||
fieldIndices: many(field_indices),
|
||||
fields: many(fields)
|
||||
}))
|
||||
|
||||
|
||||
export interface IndexType extends InferSelectModel<typeof indices> {
|
||||
fieldIndices : FieldIndexType[]
|
||||
export interface IndexType extends InferSelectModel<typeof indices> {
|
||||
fieldIndices: FieldIndexType[]
|
||||
};
|
||||
export interface IndexInsertType extends InferInsertModel<typeof indices> {
|
||||
|
||||
export interface IndexInsertType extends InferInsertModel<typeof indices> {
|
||||
fieldIndices? : FieldIndexInsertType[]
|
||||
};
|
||||
+113
-39
@@ -1,34 +1,69 @@
|
||||
import ToggleButton from "@/components/toggle/toggle";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/tooltip/tooltip";
|
||||
import { Button, Select, SelectItem } from "@heroui/react";
|
||||
import { EllipsisVertical } from "lucide-react";
|
||||
import { FieldType } from "@/lib/schemas/field-schema";
|
||||
import { FieldIndexType } from "@/lib/schemas/field_index-schema";
|
||||
import { IndexInsertType, IndexType } from "@/lib/schemas/index-schema";
|
||||
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
|
||||
import { areArraysEqual } from "@/utils/utils";
|
||||
import { Button, Input, Popover, PopoverContent, PopoverTrigger, Select, SelectItem, SharedSelection } from "@heroui/react";
|
||||
import { EllipsisVertical, Trash2 } from "lucide-react";
|
||||
import { Key, useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
|
||||
|
||||
export const animals = [
|
||||
{ key: "cat", label: "Cat" },
|
||||
{ key: "dog", label: "Dog" },
|
||||
{ key: "elephant", label: "Elephant" },
|
||||
{ key: "lion", label: "Lion" },
|
||||
{ key: "tiger", label: "Tiger" },
|
||||
{ key: "giraffe", label: "Giraffe" },
|
||||
{ key: "dolphin", label: "Dolphin" },
|
||||
{ key: "penguin", label: "Penguin" },
|
||||
{ key: "zebra", label: "Zebra" },
|
||||
{ key: "shark", label: "Shark" },
|
||||
{ key: "whale", label: "Whale" },
|
||||
{ key: "otter", label: "Otter" },
|
||||
{ key: "crocodile", label: "Crocodile" },
|
||||
];
|
||||
|
||||
|
||||
interface Props {
|
||||
|
||||
index: IndexType;
|
||||
fields: FieldType[]
|
||||
}
|
||||
|
||||
|
||||
const IndexItem: React.FC<Props> = ({ }) => {
|
||||
const IndexItem: React.FC<Props> = ({ index, fields }) => {
|
||||
const { t } = useTranslation();
|
||||
const { editIndex, deleteIndex, editFieldIndices } = useDatabaseOperations();
|
||||
const [popOverOpen, setPopOverOpen] = useState<boolean>(false);
|
||||
const [fieldIndices, setFieldIndices] = useState<Set<string>>(new Set());
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
setFieldIndices(
|
||||
new Set( index.fieldIndices.map((fieldIndex : FieldIndexType) => fieldIndex.fieldId))
|
||||
)
|
||||
} , [index.fieldIndices])
|
||||
|
||||
const toggleUnique = (unique: boolean) => {
|
||||
|
||||
editIndex({
|
||||
id: index.id,
|
||||
unique
|
||||
} as IndexInsertType);
|
||||
}
|
||||
|
||||
const editIndexName = (event: any) => {
|
||||
editIndex({
|
||||
id: index.id,
|
||||
name: event.target.value
|
||||
} as IndexInsertType);
|
||||
|
||||
}
|
||||
const removeIndex = () => {
|
||||
deleteIndex(index.id)
|
||||
}
|
||||
|
||||
|
||||
const onInexFieldChange = (keys: SharedSelection | Set<string>) => {
|
||||
setFieldIndices(keys as Set<string>)
|
||||
}
|
||||
|
||||
|
||||
const openChange = useCallback((isOpen: boolean) => {
|
||||
if (isOpen == false) {
|
||||
if (!areArraysEqual(Array.from(fieldIndices), index.fieldIndices.map((fieldIndex: FieldIndexType) => fieldIndex.fieldId)))
|
||||
editFieldIndices(index.id, Array.from(fieldIndices));
|
||||
}
|
||||
}, [fieldIndices])
|
||||
return (
|
||||
<div className="flex gap-2 w-full">
|
||||
<Select
|
||||
@@ -36,33 +71,72 @@ const IndexItem: React.FC<Props> = ({ }) => {
|
||||
placeholder={t("db_controller.select_fields")}
|
||||
selectionMode="multiple"
|
||||
size="sm"
|
||||
aria-label={t("db_controller.select_fields")}
|
||||
variant="bordered"
|
||||
onSelectionChange={onInexFieldChange}
|
||||
onOpenChange={openChange}
|
||||
selectedKeys={fieldIndices}
|
||||
>
|
||||
{animals.map((animal) => (
|
||||
<SelectItem key={animal.key}>{animal.label}</SelectItem>
|
||||
{fields.map((field: FieldType) => (
|
||||
<SelectItem aria-label={field.name} key={field.id}>{field.name}</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
<div className="flex gap-2 ml-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button className="p-1 px-3 transition-all hover:bg-default rounded duration-200" >
|
||||
<span className="text-icon text-sm">
|
||||
U
|
||||
</span>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{t("db_controller.unique")}?
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
isIconOnly
|
||||
variant="light"
|
||||
<ToggleButton
|
||||
className="px-3"
|
||||
onToggle={toggleUnique}
|
||||
active={index.unique as boolean}
|
||||
label={`${t("db_controller.unique")}?`}
|
||||
>
|
||||
<EllipsisVertical className="size-4 text-icon" />
|
||||
</Button>
|
||||
U
|
||||
</ToggleButton>
|
||||
<Popover placement="bottom" radius="sm" shadow="sm" showArrow isOpen={popOverOpen} onOpenChange={setPopOverOpen}>
|
||||
<PopoverTrigger>
|
||||
<Button
|
||||
size="sm"
|
||||
isIconOnly
|
||||
variant="light"
|
||||
>
|
||||
<EllipsisVertical className="size-4 text-slate-500 dark:text-default-600" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[210px]" >
|
||||
<div className="w-full flex flex-col gap-2 p-2">
|
||||
<h3 className="font-semibold text-sm text-gray">
|
||||
{t("db_controller.index_setting")}
|
||||
</h3>
|
||||
<hr className="text-default-200" />
|
||||
|
||||
<label className="text-sm font-medium text-slate-500">
|
||||
{t("db_controller.name")}
|
||||
</label>
|
||||
<Input
|
||||
variant="bordered"
|
||||
aria-label={t("db_controller.index_name")}
|
||||
placeholder={t("db_controller.index_name")}
|
||||
onBlur={editIndexName}
|
||||
size="sm"
|
||||
defaultValue={index.name}
|
||||
/>
|
||||
|
||||
|
||||
<hr className="text-default-200" />
|
||||
|
||||
<Button
|
||||
className="bg-default"
|
||||
radius="sm" variant="faded"
|
||||
color="danger"
|
||||
size="sm"
|
||||
onPressEnd={removeIndex}>
|
||||
<span className="font-medium text-sm">
|
||||
{t("db_controller.delete_index")}
|
||||
</span>
|
||||
<Trash2 className="mr-1 size-3.5 text-danger" />
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
+37
-10
@@ -1,21 +1,43 @@
|
||||
import { Button, } from "@heroui/react";
|
||||
import { Button, } from "@heroui/react";
|
||||
import IndexItem from "./index-item";
|
||||
import { Plus } from "lucide-react";
|
||||
import { Plus } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { IndexInsertType, IndexType } from "@/lib/schemas/index-schema";
|
||||
import hash from "object-hash";
|
||||
import React from "react";
|
||||
import { FieldType } from "@/lib/schemas/field-schema";
|
||||
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
|
||||
import { v4 } from "uuid";
|
||||
|
||||
interface Props {
|
||||
interface IndexesListProps {
|
||||
indices: IndexType[],
|
||||
fields: FieldType[],
|
||||
tableId: string
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
const IndexesList: React.FC<Props> = ({ }) => {
|
||||
const { t} = useTranslation() ;
|
||||
const IndexesList: React.FC<IndexesListProps> = ({ indices, fields, tableId }) => {
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { createIndex } = useDatabaseOperations();
|
||||
|
||||
const addIndex = (event: any) => {
|
||||
event.stopPropagation && event.stopPropagation();
|
||||
createIndex({
|
||||
id: v4(),
|
||||
name: `index_${indices.length + 1}`,
|
||||
unique: true,
|
||||
tableId: tableId
|
||||
} as IndexInsertType);
|
||||
}
|
||||
return (
|
||||
<div className="w-full flex-col space-y-2">
|
||||
<div>
|
||||
<IndexItem />
|
||||
</div>
|
||||
{
|
||||
indices.map((index: IndexType) => <IndexItem index={index} fields={fields} />)
|
||||
}
|
||||
|
||||
<Button
|
||||
variant="flat"
|
||||
radius="sm"
|
||||
@@ -23,7 +45,7 @@ const IndexesList: React.FC<Props> = ({ }) => {
|
||||
<Plus className="size-4 text-icon" />
|
||||
}
|
||||
className="h-8 p-2 text-xs bg-transparent hover:bg-default text-gray font-semibold"
|
||||
//onClick={handleCreateTable}
|
||||
onPressEnd={addIndex}
|
||||
>
|
||||
{t("db_controller.add_index")}
|
||||
</Button>
|
||||
@@ -33,4 +55,9 @@ const IndexesList: React.FC<Props> = ({ }) => {
|
||||
}
|
||||
|
||||
|
||||
export default IndexesList;
|
||||
|
||||
|
||||
|
||||
export default React.memo(IndexesList, (prevState, newState) => {
|
||||
return hash(prevState) == hash(newState);
|
||||
});
|
||||
+26
-9
@@ -1,16 +1,17 @@
|
||||
|
||||
import { Accordion, AccordionItem, Button, cn, Textarea } from "@heroui/react";
|
||||
import { ChevronLeft, FileKey, FileType, MessageSquareQuote, Plus } from "lucide-react";
|
||||
import { ChevronLeft, FileKey, FileType, MessageSquareQuote, Plus } from "lucide-react";
|
||||
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import ColorPicker from "@/components/color-picker/color-picker";
|
||||
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";
|
||||
|
||||
|
||||
export interface TableAccordionBodyProps {
|
||||
@@ -22,7 +23,7 @@ const TableAccordionBody: React.FC<TableAccordionBodyProps> = ({ table }) => {
|
||||
const [selectedKeys, setSelectedKeys] = useState(new Set(["fields"]));
|
||||
const [note, setNote] = useState<string>(table.note ? table.note : "");
|
||||
const { t } = useTranslation();
|
||||
const { editTable, createField } = useDatabaseOperations();
|
||||
const { editTable, createField, createIndex } = useDatabaseOperations();
|
||||
|
||||
const onColorChange = useCallback((color: string | undefined) => {
|
||||
editTable({ id: table.id, color: color ? color : null } as TableType);
|
||||
@@ -40,6 +41,15 @@ const TableAccordionBody: React.FC<TableAccordionBodyProps> = ({ table }) => {
|
||||
})
|
||||
}
|
||||
|
||||
const addIndex = (event: any) => {
|
||||
event.stopPropagation && event.stopPropagation();
|
||||
createIndex({
|
||||
id: v4(),
|
||||
name: `index_${table.indices.length + 1}`,
|
||||
unique: true,
|
||||
tableId: table.id
|
||||
} as IndexInsertType);
|
||||
}
|
||||
const saveNote = () => {
|
||||
editTable({
|
||||
id: table.id,
|
||||
@@ -50,10 +60,10 @@ const TableAccordionBody: React.FC<TableAccordionBodyProps> = ({ table }) => {
|
||||
|
||||
useEffect(() => {
|
||||
setNote(table.note ? table.note : "");
|
||||
}, [table.note]) ;
|
||||
}, [table.note]);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className="w-full dark:bg-background">
|
||||
@@ -87,7 +97,7 @@ const TableAccordionBody: React.FC<TableAccordionBodyProps> = ({ table }) => {
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<FieldList tableFields={table.fields} tableId = { table.id } />
|
||||
<FieldList tableFields={table.fields} tableId={table.id} />
|
||||
</AccordionItem>
|
||||
<AccordionItem key="indexes" aria-label="Indexes"
|
||||
indicator={({ isOpen }) => (
|
||||
@@ -102,14 +112,20 @@ const TableAccordionBody: React.FC<TableAccordionBodyProps> = ({ table }) => {
|
||||
trigger: "hover:bg-default h-6 dark:hover:bg-background"
|
||||
}}
|
||||
subtitle={
|
||||
<div className="flex gap-2 dark:text-default-600 items-center font-medium p-1 w-full hover:underline text-slate-500 hover:text-slate-600 transition-all duration-200 dark:hover:text-white">
|
||||
<div className="group flex gap-2 dark:text-default-600 items-center font-medium p-1 w-full hover:underline text-slate-500 hover:text-slate-600 transition-all duration-200 dark:hover:text-white">
|
||||
<FileKey className="size-4 " />
|
||||
<label className="text-sm w-full cursor-pointer">
|
||||
{t("db_controller.indexes")}
|
||||
</label>
|
||||
<button
|
||||
className="size-4 p-0 text-xs opacity-0 group-hover:opacity-100 transition-all duration-200 hover:text-slate-700 text-icon"
|
||||
onClick={addIndex}
|
||||
>
|
||||
<Plus className="size-4 dark:text-default-600" />
|
||||
</button>
|
||||
</div>
|
||||
}>
|
||||
<IndexesList />
|
||||
<IndexesList indices={table.indices} fields={table.fields} tableId={table.id} />
|
||||
</AccordionItem>
|
||||
<AccordionItem key="note" aria-label="Note"
|
||||
indicator={({ isOpen }) => (
|
||||
@@ -154,6 +170,7 @@ const TableAccordionBody: React.FC<TableAccordionBodyProps> = ({ table }) => {
|
||||
<Button
|
||||
|
||||
variant="ghost"
|
||||
onPressEnd={addIndex}
|
||||
size="sm"
|
||||
className="font-semibold p-4 border-default-50"
|
||||
startContent={
|
||||
|
||||
+20
-7
@@ -1,8 +1,8 @@
|
||||
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/tooltip/tooltip";
|
||||
|
||||
import { Button, cn, Input, Listbox, ListboxItem, Popover, PopoverContent, PopoverTrigger } from "@heroui/react";
|
||||
import { Button, cn, Input, Listbox, ListboxItem, Popover, PopoverContent, PopoverTrigger } from "@heroui/react";
|
||||
import { Check, ChevronRight, Copy, EllipsisVertical, FileKey, FileType, Focus, Pencil, Trash } from "lucide-react";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TableInsertType, TableType } from "@/lib/schemas/table-schema";
|
||||
@@ -12,6 +12,7 @@ import { getNextSequence } from "@/utils/field";
|
||||
import { useDiagramOps } from "@/providers/diagram-provider/diagram-provider";
|
||||
import hash from "object-hash";
|
||||
import { cloneTable } from "@/utils/tables";
|
||||
import { IndexInsertType } from "@/lib/schemas/index-schema";
|
||||
|
||||
export interface TableAccordionHeaderProps {
|
||||
table: TableType,
|
||||
@@ -20,7 +21,7 @@ export interface TableAccordionHeaderProps {
|
||||
|
||||
|
||||
const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOpen }) => {
|
||||
const { editTable, deleteTable, createField, createTable } = useDatabaseOperations();
|
||||
const { editTable, deleteTable, createField, createTable , createIndex } = useDatabaseOperations();
|
||||
|
||||
const [popOverOpen, setPopOverOpen] = useState<boolean>(false);
|
||||
const [tableName, setTableName] = useState<string>(table.name);
|
||||
@@ -55,6 +56,16 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
|
||||
})
|
||||
}
|
||||
|
||||
const addIndex = (event: any) => {
|
||||
setPopOverOpen(false)
|
||||
|
||||
createIndex({
|
||||
id: v4(),
|
||||
name: `index_${table.indices.length + 1}`,
|
||||
unique: true,
|
||||
tableId: table.id
|
||||
} as IndexInsertType);
|
||||
}
|
||||
|
||||
const duplicate = async () => {
|
||||
setPopOverOpen(false)
|
||||
@@ -176,8 +187,10 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
|
||||
<ListboxItem
|
||||
key="add_index"
|
||||
endContent={<FileKey className="size-4 text-icon dark:text-white" />}
|
||||
showDivider>
|
||||
|
||||
showDivider
|
||||
onPressEnd={addIndex}
|
||||
>
|
||||
|
||||
{t("db_controller.add_index")}
|
||||
</ListboxItem>
|
||||
|
||||
@@ -186,7 +199,7 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
|
||||
showDivider
|
||||
onPressEnd={duplicate}
|
||||
endContent={<Copy className="size-4 text-icon" />}>
|
||||
|
||||
|
||||
{t("db_controller.duplicate")}
|
||||
</ListboxItem>
|
||||
<ListboxItem
|
||||
@@ -195,7 +208,7 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
|
||||
color="danger"
|
||||
onPressEnd={onDeleteTable}
|
||||
endContent={<Trash className="size-4" />}
|
||||
>
|
||||
>
|
||||
{t("db_controller.delete_table")}
|
||||
</ListboxItem>
|
||||
</Listbox>
|
||||
|
||||
@@ -47,14 +47,16 @@ const DatabaseHistoryProvider: React.FC<Props> = ({ children }) => {
|
||||
return;
|
||||
}
|
||||
const normalizedDatabase = normalizeDatabase(database);
|
||||
console.log (normalizedDatabase)
|
||||
const normalizedPresent = normalizeDatabase(datatbaseState.present);
|
||||
const differences = compare(normalizedDatabase, normalizedPresent);
|
||||
|
||||
|
||||
if (differences && differences.length > 0) {
|
||||
setIsProcessing(true);
|
||||
|
||||
const operations: DBDiffOperation[] = mapDiffToDBDiffOperation(differences);
|
||||
|
||||
console.log ( operations ) ;
|
||||
/*
|
||||
(async () => {
|
||||
try {
|
||||
await executeDbDiffOps(operations)
|
||||
@@ -65,7 +67,7 @@ const DatabaseHistoryProvider: React.FC<Props> = ({ children }) => {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
})()
|
||||
|
||||
*/
|
||||
}
|
||||
|
||||
}, [datatbaseState.present]);
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
import { DataType } from "@/lib/schemas/data-type-schema";
|
||||
import { DatabaseType } from "@/lib/schemas/database-schema";
|
||||
import { FieldInsertType, FieldType } from "@/lib/schemas/field-schema";
|
||||
import { IndexInsertType } from "@/lib/schemas/index-schema";
|
||||
import { RelationshipInsertType } from "@/lib/schemas/relationship-schema";
|
||||
import { TableInsertType } from "@/lib/schemas/table-schema";
|
||||
import { TableInsertType } from "@/lib/schemas/table-schema";
|
||||
import { DBDiffOperation } from "@/utils/database";
|
||||
import { QueryResult } from "@powersync/web";
|
||||
import { createContext } from "react";
|
||||
@@ -33,6 +34,12 @@ interface DatabaseOperationsContextType {
|
||||
editField: (field: FieldInsertType) => Promise<QueryResult>,
|
||||
deleteField: (id: string) => Promise<void>,
|
||||
orderTableFields: (fields: FieldType[]) => Promise<void>,
|
||||
|
||||
// index operations
|
||||
createIndex : ( index : IndexInsertType) => Promise<QueryResult> ,
|
||||
editIndex : ( index : IndexInsertType) => Promise<QueryResult> ,
|
||||
deleteIndex : ( id : string) => Promise<QueryResult> ,
|
||||
editFieldIndices:( indexId : string , fieldIds : string[])=> Promise<void> ,
|
||||
// relationship operations
|
||||
createRelationship: (relationship: RelationshipInsertType) => Promise<QueryResult>,
|
||||
editRelationship: (relationship: RelationshipInsertType) => Promise<QueryResult>,
|
||||
|
||||
@@ -12,8 +12,9 @@ import DatabaseHistoryProvider from "../database-history/database-history-provid
|
||||
import { DBDiffOperation } from "@/utils/database";
|
||||
import { DatabaseType } from "@/lib/schemas/database-schema";
|
||||
import { getTimestamp } from "@/utils/utils";
|
||||
import { indices } from "@/lib/schemas/index-schema";
|
||||
import { IndexInsertType, indices } from "@/lib/schemas/index-schema";
|
||||
import { field_indices } from "@/lib/schemas/field_index-schema";
|
||||
import { v4 } from "uuid";
|
||||
|
||||
interface Props { children: React.ReactNode }
|
||||
|
||||
@@ -68,7 +69,7 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
|
||||
|
||||
// Normalize result to single object
|
||||
if (database.length == 1)
|
||||
@@ -150,6 +151,37 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
})
|
||||
}, [db]);
|
||||
|
||||
|
||||
// CRUD operations for indices
|
||||
|
||||
const createIndex = useCallback(async (index: IndexInsertType): Promise<QueryResult> => {
|
||||
return await db.insert(indices).values({
|
||||
...index,
|
||||
createdAt: index.createdAt ? index.createdAt : getTimestamp()
|
||||
});
|
||||
}, [db]);
|
||||
|
||||
|
||||
const deleteIndex = useCallback(async (id: string): Promise<QueryResult> => {
|
||||
return await db.delete(indices).where(eq(indices.id, id))
|
||||
}, [db])
|
||||
|
||||
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> => {
|
||||
return db.transaction(async (tx) => {
|
||||
await tx.delete(field_indices).where(eq(field_indices.indexId, indexId));
|
||||
if (fieldIds.length > 0)
|
||||
await tx.insert(field_indices).values(fieldIds.map((fieldId: string) => ({
|
||||
id: v4(),
|
||||
fieldId,
|
||||
indexId,
|
||||
|
||||
})))
|
||||
})
|
||||
}, [db])
|
||||
// CRUD operations for Relationships
|
||||
const createRelationship = useCallback(async (relationship: RelationshipInsertType): Promise<QueryResult> => {
|
||||
if (currentDatabaseId) {
|
||||
@@ -243,6 +275,10 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
deleteRelationship,
|
||||
deleteMultiRelationships,
|
||||
executeDbDiffOps,
|
||||
createIndex,
|
||||
editIndex,
|
||||
deleteIndex,
|
||||
editFieldIndices,
|
||||
data_types
|
||||
}), [
|
||||
createTable,
|
||||
@@ -259,6 +295,10 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
deleteRelationship,
|
||||
deleteMultiRelationships,
|
||||
executeDbDiffOps,
|
||||
createIndex,
|
||||
editIndex,
|
||||
deleteIndex,
|
||||
editFieldIndices,
|
||||
data_types
|
||||
]);
|
||||
|
||||
|
||||
@@ -6,14 +6,14 @@ import { AppSchema, drizzleSchema } from '@/lib/schemas/app-schema';
|
||||
import { StackRenderConnector } from '@/utils/stackrender-connector';
|
||||
import { CircularProgress } from '@heroui/react';
|
||||
import { PowerSyncSQLiteDatabase, wrapPowerSyncWithDrizzle } from '@powersync/drizzle-driver';
|
||||
|
||||
|
||||
|
||||
export const powerSyncDb = new PowerSyncDatabase({
|
||||
database: {
|
||||
dbFilename: 'stackrender.sqlite'
|
||||
},
|
||||
schema: AppSchema,
|
||||
|
||||
|
||||
});
|
||||
|
||||
export const db: PowerSyncSQLiteDatabase<typeof drizzleSchema> = wrapPowerSyncWithDrizzle(powerSyncDb, {
|
||||
@@ -35,12 +35,15 @@ export const SyncProvider: React.FC<SyncProviderProps> = ({ children }) => {
|
||||
useEffect(() => {
|
||||
powerSync.init();
|
||||
powerSync.connect(connector);
|
||||
|
||||
|
||||
(async () => {
|
||||
await powerSync.execute("PRAGMA foreign_keys = ON;");
|
||||
})()
|
||||
|
||||
|
||||
}, [powerSync, connector])
|
||||
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<Suspense fallback={<CircularProgress />}>
|
||||
<PowerSyncContext.Provider value={powerSync}>
|
||||
|
||||
+113
-2
@@ -4,6 +4,8 @@ import { FieldType } from "@/lib/schemas/field-schema";
|
||||
import { RelationshipType } from "@/lib/schemas/relationship-schema";
|
||||
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";
|
||||
|
||||
// Define the possible operations that can be performed when diffing databases
|
||||
export type DBDiffOperation =
|
||||
@@ -15,7 +17,13 @@ export type DBDiffOperation =
|
||||
| { type: 'UPDATE_FIELD'; tableId: string; fieldId: string; changes: Partial<FieldType> }
|
||||
| { type: 'CREATE_RELATIONSHIP'; relationship: RelationshipType }
|
||||
| { type: 'DELETE_RELATIONSHIP'; relationshipId: string }
|
||||
| { type: 'UPDATE_RELATIONSHIP'; relationshipId: string; changes: Partial<RelationshipType> };
|
||||
| { type: 'UPDATE_RELATIONSHIP'; relationshipId: string; changes: Partial<RelationshipType> }
|
||||
// New index-related operations:
|
||||
| { type: 'CREATE_INDEX'; tableId: string; index: IndexType }
|
||||
| { type: 'DELETE_INDEX'; tableId: string; indexId: string }
|
||||
| { type: 'UPDATE_INDEX'; tableId: string; indexId: string; changes: Partial<IndexType> }
|
||||
| { type: 'UPDATE_FIELD_INDICES'; tableId: string; indexId: string; create: FieldIndexType[]; delete: string[] }
|
||||
|
||||
|
||||
/**
|
||||
* Convert a list of low-level JSON patch operations into higher-level database diff operations
|
||||
@@ -32,6 +40,15 @@ export function mapDiffToDBDiffOperation(patch: any[]): DBDiffOperation[] {
|
||||
const relationshipCreates: RelationshipType[] = [];
|
||||
const relationshipDeletes: string[] = [];
|
||||
|
||||
// Keep track of index changes
|
||||
const indexChanges: Record<string, Record<string, Partial<IndexType>>> = {};
|
||||
const indexCreates: Record<string, IndexType[]> = {};
|
||||
const indexDeletes: Record<string, string[]> = {};
|
||||
const fieldIndexMutations: Record<string, Record<string, {
|
||||
create: FieldIndexType[];
|
||||
delete: string[];
|
||||
}>> = {};
|
||||
|
||||
// Loop through each patch operation
|
||||
for (const op of patch) {
|
||||
const parts = op.path.split('/').filter(Boolean); // Split JSON path into parts
|
||||
@@ -66,6 +83,53 @@ export function mapDiffToDBDiffOperation(patch: any[]): DBDiffOperation[] {
|
||||
fieldChanges[tableId][fieldId][attr as keyof FieldType] = op.value;
|
||||
}
|
||||
}
|
||||
// Handle indices inside tables
|
||||
else if (parts[2] === 'indices') {
|
||||
|
||||
const indexId = parts[3];
|
||||
if (parts.length === 3) {
|
||||
// Whole indices array replaced? Treat as table attribute update
|
||||
if (op.op === 'replace') {
|
||||
tableChanges[tableId] ??= {};
|
||||
tableChanges[tableId]['indices' as keyof TableType] = op.value;
|
||||
}
|
||||
} else if (parts.length === 4) {
|
||||
// index-level add/remove/replace
|
||||
if (op.op === 'add') {
|
||||
indexCreates[tableId] ??= [];
|
||||
indexCreates[tableId].push(op.value);
|
||||
} else if (op.op === 'remove') {
|
||||
indexDeletes[tableId] ??= [];
|
||||
indexDeletes[tableId].push(indexId);
|
||||
} else if (op.op === 'replace') {
|
||||
// full index replaced? treat as update with full new index?
|
||||
indexChanges[tableId] ??= {};
|
||||
indexChanges[tableId][indexId] = op.value;
|
||||
}
|
||||
}
|
||||
|
||||
// Inside the 'indices' handler
|
||||
else if (parts.length === 6 && parts[4] === 'fieldIndices') {
|
||||
const fieldIndexId = parts[5];
|
||||
|
||||
fieldIndexMutations[tableId] ??= {};
|
||||
fieldIndexMutations[tableId][indexId] ??= { create: [], delete: [] };
|
||||
|
||||
if (op.op === 'add') {
|
||||
fieldIndexMutations[tableId][indexId].create.push(op.value);
|
||||
} else if (op.op === 'remove') {
|
||||
fieldIndexMutations[tableId][indexId].delete.push(fieldIndexId);
|
||||
}
|
||||
}
|
||||
|
||||
else if (op.op === 'replace') {
|
||||
// partial index attribute change
|
||||
const attr = parts.slice(4).join('/');
|
||||
indexChanges[tableId] ??= {};
|
||||
indexChanges[tableId][indexId] ??= {};
|
||||
indexChanges[tableId][indexId][attr as keyof IndexType] = op.value;
|
||||
}
|
||||
}
|
||||
|
||||
// Updating table attributes (e.g., name, position, etc.)
|
||||
else if (op.op === 'replace') {
|
||||
@@ -140,6 +204,44 @@ export function mapDiffToDBDiffOperation(patch: any[]): DBDiffOperation[] {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Index creates
|
||||
for (const [tableId, indices] of Object.entries(indexCreates)) {
|
||||
for (const index of indices) {
|
||||
operations.push({ type: 'CREATE_INDEX', tableId, index });
|
||||
}
|
||||
}
|
||||
|
||||
// Index deletes
|
||||
for (const [tableId, indexIds] of Object.entries(indexDeletes)) {
|
||||
for (const indexId of indexIds) {
|
||||
operations.push({ type: 'DELETE_INDEX', tableId, indexId });
|
||||
}
|
||||
}
|
||||
|
||||
// Index updates
|
||||
for (const [tableId, indices] of Object.entries(indexChanges)) {
|
||||
for (const [indexId, changes] of Object.entries(indices)) {
|
||||
operations.push({ type: 'UPDATE_INDEX', tableId, indexId, changes });
|
||||
}
|
||||
}
|
||||
|
||||
for (const tableId in fieldIndexMutations) {
|
||||
for (const indexId in fieldIndexMutations[tableId]) {
|
||||
const { create, delete: del } = fieldIndexMutations[tableId][indexId];
|
||||
if (create.length || del.length) {
|
||||
operations.push({
|
||||
type: 'UPDATE_FIELD_INDICES',
|
||||
tableId,
|
||||
indexId,
|
||||
create,
|
||||
delete: del,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Push relationship creation operations
|
||||
for (const relationship of relationshipCreates) {
|
||||
operations.push({ type: 'CREATE_RELATIONSHIP', relationship });
|
||||
@@ -173,7 +275,16 @@ export function normalizeDatabase(db: DatabaseType): any {
|
||||
...table,
|
||||
fields: Object.fromEntries(
|
||||
table.fields.map(field => [field.id, field])
|
||||
)
|
||||
),
|
||||
indices: Object.fromEntries(
|
||||
table.indices.map(index => [index.id, {
|
||||
...index,
|
||||
|
||||
fieldIndices: Object.fromEntries(
|
||||
index.fieldIndices.map(fieldIndex => [fieldIndex.id, fieldIndex])
|
||||
),
|
||||
}])
|
||||
),
|
||||
}
|
||||
])
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user