integrate the indices with the Api

This commit is contained in:
KarimTamani
2025-05-28 00:31:25 +01:00
parent 1c831c9902
commit f7b382990d
12 changed files with 405 additions and 107 deletions
+11 -7
View File
@@ -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",
+8 -7
View File
@@ -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> { };
+14 -15
View File
@@ -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[]
};
@@ -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>
@@ -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);
});
@@ -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={
@@ -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
View File
@@ -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])
),
}])
),
}
])
),