diff --git a/src/components/empty-list/empty-list.tsx b/src/components/empty-list/empty-list.tsx new file mode 100644 index 0000000..8b2e967 --- /dev/null +++ b/src/components/empty-list/empty-list.tsx @@ -0,0 +1,46 @@ +import { Image } from "@heroui/react"; +import { useTheme } from "next-themes"; + + + + +interface EmptyListProps { + title?: string; + description?: string; + +} + + + +const EmptyList: React.FC = ({ title, description }) => { + + + + return ( +
+ { + + } + { + title && +

+ {title} +

+ } + { + + description && +

+ {description} +

+ } +
+ ) +} + + +export default EmptyList; \ No newline at end of file diff --git a/src/components/menu/menu.tsx b/src/components/menu/menu.tsx index a61d249..94e6f8e 100644 --- a/src/components/menu/menu.tsx +++ b/src/components/menu/menu.tsx @@ -21,7 +21,7 @@ const Menu: React.FC = ({ }) => { const { canRedo, canUndo, undo, redo } = useDatabaseHistory(); const { deleteMultiTables } = useDatabaseOperations(); const { database } = useDatabase(); - const { setShowController, showController, cardinalityStyle, setCardinalityStyle } = useDiagramOps(); + const { openController, showController, cardinalityStyle, changeCardinalityStyle } = useDiagramOps(); const menu: MenuDropdownProps[] = useMemo(() => [ { @@ -94,21 +94,21 @@ const Menu: React.FC = ({ }) => { id: "menu.symbolic", selected: cardinalityStyle == CardinalityStyle.SYMBOLIC, title: t("menu.symbolic"), clickHandler: () => { - setCardinalityStyle(CardinalityStyle.SYMBOLIC) + changeCardinalityStyle(CardinalityStyle.SYMBOLIC) } }, { id: "menu.numeric", selected: cardinalityStyle == CardinalityStyle.NUMERIC, title: t("menu.numeric"), clickHandler: () => { - setCardinalityStyle(CardinalityStyle.NUMERIC) + changeCardinalityStyle(CardinalityStyle.NUMERIC) } }, { id: "menu.hidden", selected: cardinalityStyle == CardinalityStyle.HIDDEN, title: t("menu.hidden"), clickHandler: () => { - setCardinalityStyle(CardinalityStyle.HIDDEN) + changeCardinalityStyle(CardinalityStyle.HIDDEN) } }, @@ -117,7 +117,7 @@ const Menu: React.FC = ({ }) => { { id: "menu.controller_visibility", title: showController ? t("menu.hide_controller") : t("menu.show_controller"), shortcut: "Ctnl + B", divide: true, clickHandler: () => { - setShowController(!showController) + openController(!showController) } }, @@ -151,7 +151,7 @@ const Menu: React.FC = ({ }) => { { id: "menu.join_discord", title: t("menu.join_discord") }, ], }, - ], [t, canRedo, canUndo, undo, redo, deleteMultiTables, database, showController, setShowController, cardinalityStyle, setCardinalityStyle, resolvedTheme]); + ], [t, canRedo, canUndo, undo, redo, deleteMultiTables, database, showController, openController ,changeCardinalityStyle , cardinalityStyle, resolvedTheme]); return
{ diff --git a/src/components/navbar/connection-status.tsx b/src/components/navbar/connection-status.tsx index ad159ad..001c770 100644 --- a/src/components/navbar/connection-status.tsx +++ b/src/components/navbar/connection-status.tsx @@ -16,8 +16,6 @@ const ConnectionStatus: React.FC = () => { const clearProgressHandler: any = useRef(undefined); const { t } = useTranslation(); - - const [lastUploadAt, setLastUploadAt] = useState(() => { const last_upload_at = localStorage.getItem("last_upload_at"); if (last_upload_at) @@ -47,6 +45,7 @@ const ConnectionStatus: React.FC = () => { return () => l?.(); }, [powerSync]); + useEffect(() => { if (syncStatus.downloadProgress?.downloadedOperations && syncStatus.downloadProgress?.totalOperations) { @@ -93,7 +92,7 @@ const ConnectionStatus: React.FC = () => {
-
+
{ syncStatus.connected ? @@ -117,9 +116,9 @@ const ConnectionStatus: React.FC = () => { { : - + {t("connection_status.saved")} {deltaUploadTime} diff --git a/src/i18/languages/en.ts b/src/i18/languages/en.ts index ac9946a..9854c5f 100644 --- a/src/i18/languages/en.ts +++ b/src/i18/languages/en.ts @@ -43,6 +43,13 @@ export const en = { select_table: "Select table", select_field: "Select field", + empty_list : { + no_tables : "No Tables" , + no_tables_description : "Create a Table to get started." , + + no_relationships : "No Relationships" , + no_relationships_description : "Create relationships to connect two tables" + } , cardinality: { name: "Cardinality", @@ -99,12 +106,8 @@ export const en = { index_setting: "Index Setting", table_actions: "Table Actions", actions: "Actions", - - 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.", diff --git a/src/pages/database/database-page.tsx b/src/pages/database/database-page.tsx index 2393219..c789538 100644 --- a/src/pages/database/database-page.tsx +++ b/src/pages/database/database-page.tsx @@ -55,7 +55,7 @@ const DatabasePage: React.FC = () => { const { t } = useTranslation(); const { resolvedTheme } = useTheme(); // Extract database state and operations - const { database, getField, databases, isSwitchingDatabase, isLoading, isFetching } = useDatabase(); + const { database, getField, databases, isSwitchingDatabase, isLoading, isFetching, isSyncing } = useDatabase(); const { updateTablePositions, deleteMultiTables, deleteMultiRelationships, createRelationship } = useDatabaseOperations(); @@ -64,7 +64,7 @@ const DatabasePage: React.FC = () => { const [edges, setEdges, onEdgesChange] = useEdgesState([]); // Diagram-related state (e.g. connection in progress) - const { setIsConnectionInProgress, cardinalityStyle, setCardinalityStyle } = useDiagramOps(); + const { setIsConnectionInProgress, cardinalityStyle } = useDiagramOps(); // Destructure tables and relationships from database const { tables, relationships } = database || { tables: [], relationships: [] }; @@ -78,7 +78,7 @@ const DatabasePage: React.FC = () => { useEffect(() => { - if (isLoading || isFetching || !(syncStatus.currentStatus as any).options.hasSynced) + if (isSyncing) return; // no database selected , open (open database) Modal @@ -94,7 +94,7 @@ const DatabasePage: React.FC = () => { closable: false }); } - }, [database?.id, isLoading, isFetching, (syncStatus.currentStatus as any).options.hasSynced]) + }, [database?.id, isSyncing]) useEffect(() => { if (isSwitchingDatabase) { @@ -254,11 +254,15 @@ const DatabasePage: React.FC = () => { diff --git a/src/pages/database/db-controller/relationship-controller/relationship-controller.tsx b/src/pages/database/db-controller/relationship-controller/relationship-controller.tsx index 012c440..7ea2273 100644 --- a/src/pages/database/db-controller/relationship-controller/relationship-controller.tsx +++ b/src/pages/database/db-controller/relationship-controller/relationship-controller.tsx @@ -6,14 +6,16 @@ import { useTranslation } from "react-i18next"; import RelationshipAccordionHeader from "./relationship-accordion-item/relationship-accordion-header"; import RelationshipAccordionBody from "./relationship-accordion-item/relationship-accordion-body"; -import CreateRelationshipForm from "../../modals/create-relationship-modal"; -import { RelationshipInsertType, RelationshipType } from "@/lib/schemas/relationship-schema"; -import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider"; + +import { RelationshipType } from "@/lib/schemas/relationship-schema"; +import { useDatabase } from "@/providers/database-provider/database-provider"; import { useDiagram } from "@/providers/diagram-provider/diagram-provider"; import { useModal } from "@/providers/modal-provider/modal-provider"; import { Modals } from "@/providers/modal-provider/modal-contxet"; import { getDefaultRelationshipName } from "@/utils/relationship"; +import EmptyList from "@/components/empty-list/empty-list"; + @@ -25,7 +27,7 @@ const RelationshipController: React.FC = ({ }) => { const { open } = useModal(); - const { database } = useDatabase(); + const { database, isSyncing } = useDatabase(); const { relationships: allRelationships } = database || { relationships: [] }; const [relationships, setRelationships] = useState(allRelationships); @@ -49,7 +51,7 @@ const RelationshipController: React.FC = ({ }) => { const onOpen = useCallback(() => { open(Modals.CREATE_RELATIONSHIP, { - onRlationshipCreated: (id: string) => setSelectedRelationship(new Set([id]) as any) + onRlationshipCreated: (id: string) => setSelectedRelationship(new Set([id]) as any) }) }, []) @@ -130,37 +132,48 @@ const RelationshipController: React.FC = ({ }) => { >{t("db_controller.add_relationship")}
-
+ { + allRelationships.length > 0 && +
+ - {relationships.map((relationship: RelationshipType) => ( - + {relationships.map((relationship: RelationshipType) => ( + - } - > - - - ))} - -
+ base: "rounded-md p-0 overflow-hidden", + }} + subtitle={ + + } + > + + + ))} + +
+ } + + + { + !isSyncing && allRelationships.length == 0 && + + }
) diff --git a/src/pages/database/db-controller/tables-controller/tables-controller.tsx b/src/pages/database/db-controller/tables-controller/tables-controller.tsx index 18c40d4..0edc9f0 100644 --- a/src/pages/database/db-controller/tables-controller/tables-controller.tsx +++ b/src/pages/database/db-controller/tables-controller/tables-controller.tsx @@ -11,6 +11,7 @@ import { v4 } from "uuid"; import { useDiagram } from "@/providers/diagram-provider/diagram-provider"; import { useReactFlow } from "@xyflow/react"; import SqlPreview from "../sql-preview"; +import EmptyList from "@/components/empty-list/empty-list"; const PADDING_X = 40; @@ -19,19 +20,19 @@ const PADDING_Y = 80; const TablesController: React.FC = ({ }) => { - const { database } = useDatabase(); + const { database , isSyncing } = useDatabase(); const { createTable, getInteger } = useDatabaseOperations(); const { getViewport } = useReactFlow(); const { tables: allTables } = database || { tables: [] }; const [tables, setTables] = useState(allTables); - const { t } = useTranslation(); const [selectedTable, setSelectedTable] = useState(new Set([])); const [showSqlPreview, setShowSqlPreview] = useState(false); const { focusedTableId } = useDiagram(); const nameRef: Ref = useRef(null); const accordionRef = useRef(null); - + + useEffect(() => searchTables(), [allTables]); const addNewTable = useCallback(async () => { @@ -65,7 +66,7 @@ const TablesController: React.FC = ({ }) => { useEffect(() => { if (focusedTableId) { setSelectedTable(new Set([focusedTableId]) as any); - setShowSqlPreview(false) ; + setShowSqlPreview(false); const accordionItem = document.getElementById(focusedTableId) if (accordionItem) accordionItem?.scrollIntoView({ @@ -145,7 +146,7 @@ const TablesController: React.FC = ({ }) => {
{ - !showSqlPreview && + allTables.length > 0 && !showSqlPreview &&
{
} { - showSqlPreview && + allTables.length > 0 && showSqlPreview &&
-
} + { + !isSyncing && allTables.length == 0 && + + }
) } diff --git a/src/providers/database-history/database-history-provider.tsx b/src/providers/database-history/database-history-provider.tsx index be2e4d5..3e4d232 100644 --- a/src/providers/database-history/database-history-provider.tsx +++ b/src/providers/database-history/database-history-provider.tsx @@ -12,8 +12,8 @@ interface Props { children: React.ReactNode }; const DatabaseHistoryProvider: React.FC = ({ children }) => { const udpateDbFlag = useRef(false); - const { database , isLoading , isFetching } = useDatabase() ; - const { executeDbDiffOps } = useDatabaseOperations(); + const { database, isLoading, isFetching } = useDatabase(); + const { executeDbDiffOps } = useDatabaseOperations(); const [isProcessing, setIsProcessing] = useState(false); const [datatbaseState, { set, undo: undoChanges, redo: redoChanges, canUndo, canRedo, reset }] = useUndo(database as DatabaseType); @@ -26,12 +26,12 @@ const DatabaseHistoryProvider: React.FC = ({ children }) => { useEffect(() => { if (database && datatbaseState.present && !isLoading && !isFetching) { udpateDbFlag.current = false; - + const presentHash: string | undefined = hash(datatbaseState.present, { algorithm: 'sha1' }); const databaseHash: string = hash(database, { algorithm: 'sha1' }); - if (presentHash != databaseHash) { - + if (presentHash != databaseHash) { + set(database); } } @@ -39,18 +39,18 @@ const DatabaseHistoryProvider: React.FC = ({ children }) => { const undo = useCallback(() => { - if (!isProcessing) { + if (!isProcessing && !isLoading && !isFetching) { udpateDbFlag.current = true; undoChanges(); } - }, [undoChanges, udpateDbFlag, isProcessing]); + }, [undoChanges, udpateDbFlag, isProcessing , isLoading , isFetching]); const redo = useCallback(() => { - if (!isProcessing) { + if (!isProcessing && !isLoading && !isFetching) { udpateDbFlag.current = true; redoChanges(); } - }, [redoChanges, udpateDbFlag, isProcessing]); + }, [redoChanges, udpateDbFlag, isProcessing, isLoading , isFetching]); useEffect(() => { @@ -66,8 +66,8 @@ const DatabaseHistoryProvider: React.FC = ({ children }) => { const normalizedPresent = normalizeDatabase(datatbaseState.present); const differences = compare(normalizedDatabase, normalizedPresent); - //console.log ( differences ) + if (differences && differences.length > 0) { setIsProcessing(true); @@ -75,13 +75,13 @@ const DatabaseHistoryProvider: React.FC = ({ children }) => { (async () => { try { - // console.log ( operations) ; + console.log(operations); await executeDbDiffOps(operations) setIsProcessing(false); } catch (error) { - console.log ( error ) ; - /// set(database); + console.log(error); + setIsProcessing(false); } })() diff --git a/src/providers/database-provider/database-context.ts b/src/providers/database-provider/database-context.ts index 0731074..e809a5f 100644 --- a/src/providers/database-provider/database-context.ts +++ b/src/providers/database-provider/database-context.ts @@ -20,6 +20,7 @@ interface DatabaseDataContextType { databases: DatabaseType[], isLoading: boolean, isFetching : boolean , + isSyncing : boolean , isSwitchingDatabase: boolean, getField: (tableId: string, id: string) => FieldType | undefined, diff --git a/src/providers/database-provider/database-provider.tsx b/src/providers/database-provider/database-provider.tsx index e4d3766..cbe7ba7 100644 --- a/src/providers/database-provider/database-provider.tsx +++ b/src/providers/database-provider/database-provider.tsx @@ -2,7 +2,7 @@ import { DatabaseDataContext, DatabaseOperationsContext } from "./database-conte import { useCallback, useContext, useEffect, useMemo, useState } from "react"; import { db } from "../sync-provider/sync-provider"; import { TableInsertType, tables, TableType } from "@/lib/schemas/table-schema"; -import { useQuery } from "@powersync/react"; +import { usePowerSync, useQuery } from "@powersync/react"; import { toCompilableQuery } from "@powersync/drizzle-driver"; import { asc, count, desc, eq, inArray, or } from "drizzle-orm"; import { QueryResult } from "@powersync/web"; @@ -23,7 +23,7 @@ import { deleteFieldsWithCascade, deleteTablesWithCascade } from "@/utils/cascad interface Props { children: React.ReactNode } const DatabaseProvider: React.FC = ({ children }) => { - + const syncStatus = usePowerSync(); const [currentDatabaseId, setCurrentDatabaseId] = useState(localStorage.getItem("database_id") as string | undefined); // Fetch all databases const { data: databases, isLoading: loadingDatabases, isFetching: fetchingDatabases } = useQuery(toCompilableQuery( @@ -101,7 +101,7 @@ const DatabaseProvider: React.FC = ({ children }) => { const isLoading: boolean = loadingDataTypes || loadingDatabases || loadingCurrentDatabase; const isFetching: boolean = fetchingDatabase || fetchingDatatypes || fetchingDatabases; - + const isSyncing : boolean = isLoading ||isFetching || !(syncStatus.currentStatus as any).options.hasSynced ; const isSwitchingDatabase: boolean = useMemo(() => { return fetchingDatabase && currentDatabaseId != (database as any)?.id @@ -136,7 +136,6 @@ const DatabaseProvider: React.FC = ({ children }) => { }).where(eq(databaseModel.id, databaseId)) }, []); - // CRUD operations for Tables const createTable = useCallback(async (table: TableInsertType): Promise => { if (currentDatabaseId) { @@ -153,8 +152,6 @@ const DatabaseProvider: React.FC = ({ children }) => { ); } }); - - } else { throw Error("No Database selected"); } @@ -302,8 +299,7 @@ const DatabaseProvider: React.FC = ({ children }) => { name: operation.chnages.name }).where(eq(databaseModel.id, currentDatabaseId))); } - else if (operation.type == "UPDATE_NUM_TABLES") { - console.log("update number of tables with ", operation.value); + else if (operation.type == "UPDATE_NUM_TABLES") { if (currentDatabaseId) operations.push( tx.update(databaseModel).set({ @@ -394,12 +390,10 @@ const DatabaseProvider: React.FC = ({ children }) => { } } } - return await Promise.all(operations); - }) } catch (error) { - console.log(error); + throw error } }, [db, currentDatabaseId]); @@ -520,6 +514,7 @@ const DatabaseProvider: React.FC = ({ children }) => { isSwitchingDatabase, isFetching, getField, + isSyncing }}> diff --git a/src/providers/diagram-provider/diagram-context.tsx b/src/providers/diagram-provider/diagram-context.tsx index a084fb3..4d536e8 100644 --- a/src/providers/diagram-provider/diagram-context.tsx +++ b/src/providers/diagram-provider/diagram-context.tsx @@ -1,5 +1,5 @@ import { CardinalityStyle } from "@/lib/database"; -import { createContext, Dispatch } from "react"; +import { createContext, Dispatch } from "react"; interface DiagramDataContextType { @@ -9,14 +9,13 @@ interface DiagramDataContextType { interface DiagramOpsContextType { focusOnTable: (id: string, transition?: boolean) => void, - focusOnRelationship: (id: string, transition?: boolean , withNavigate? : boolean) => void, + focusOnRelationship: (id: string, transition?: boolean, withNavigate?: boolean) => void, setIsConnectionInProgress: Dispatch, - isConnectionInProgress: boolean , - showController : boolean , - setShowController : ( value : boolean ) => void , - cardinalityStyle : CardinalityStyle , - setCardinalityStyle : (style : CardinalityStyle) => void - + isConnectionInProgress: boolean, + showController: boolean, + openController: (value: boolean) => void, + cardinalityStyle: CardinalityStyle, + changeCardinalityStyle: (style: CardinalityStyle) => void } diff --git a/src/providers/diagram-provider/diagram-provider.tsx b/src/providers/diagram-provider/diagram-provider.tsx index 8d90a85..9bef846 100644 --- a/src/providers/diagram-provider/diagram-provider.tsx +++ b/src/providers/diagram-provider/diagram-provider.tsx @@ -7,6 +7,8 @@ import { DiagramDataContext, DiagramOpsContext } from "./diagram-context"; import { CardinalityStyle } from "@/lib/database"; +const CONTROLLER_VISIBILITY_KEY = "is_controller_open"; +const CARDINALITY_STYLE_KEY = "cardinality_style"; interface Props { children: React.ReactNode } @@ -17,8 +19,20 @@ const DiagramProvider: React.FC = ({ children }) => { const [focusedTableId, setFocusedTableId] = useState(undefined) const [focusedRelationshipId, setFocusedRelationshipId] = useState(undefined) const [isConnectionInProgress, setIsConnectionInProgress] = useState(false); - const [showController, setShowController] = useState(true); - const [cardinalityStyle, setCardinalityStyle] = useState(CardinalityStyle.SYMBOLIC); + + const [showController, setShowController] = useState(() => { + const value: string | null = localStorage.getItem(CONTROLLER_VISIBILITY_KEY) ; + if (value) + return JSON.parse(value); + return true; + }); + const [cardinalityStyle, setCardinalityStyle] = useState(() => { + const value: string | null = localStorage.getItem(CARDINALITY_STYLE_KEY); + + if (value) + return value as CardinalityStyle; + return CardinalityStyle.SYMBOLIC; + }); const focusOnTable = useCallback((id: string, transition: boolean = false) => { navigate("/database/tables"); @@ -26,8 +40,8 @@ const DiagramProvider: React.FC = ({ children }) => { setNodes((nodes) => nodes.map((node) => { const selected: boolean = node.id === id; - - + + if (selected && transition) { fitView({ duration: 500, @@ -60,7 +74,7 @@ const DiagramProvider: React.FC = ({ children }) => { setEdges((edges) => edges.map((edge) => { - + const selected: boolean = edge.id === id; if (selected && transition) { @@ -87,6 +101,16 @@ const DiagramProvider: React.FC = ({ children }) => { }, [setFocusedRelationshipId]); + const changeCardinalityStyle = useCallback((style : CardinalityStyle) => { + localStorage.setItem(CARDINALITY_STYLE_KEY , style) ; + setCardinalityStyle( style) ; + } , []) ; + + const openController = useCallback((isOpen : boolean) => { + localStorage.setItem( CONTROLLER_VISIBILITY_KEY , JSON.stringify(isOpen) ) ; + setShowController(isOpen) ; + } , []) + const contextDatatValue = useMemo(() => ({ focusedTableId, focusedRelationshipId, @@ -99,9 +123,9 @@ const DiagramProvider: React.FC = ({ children }) => { setIsConnectionInProgress, isConnectionInProgress, showController, - setShowController, cardinalityStyle, - setCardinalityStyle + changeCardinalityStyle , + openController }), [focusOnTable, focusOnRelationship, setIsConnectionInProgress, isConnectionInProgress, showController, setShowController, cardinalityStyle, setCardinalityStyle]) diff --git a/src/providers/sync-provider/sync-provider.tsx b/src/providers/sync-provider/sync-provider.tsx index 3cfba89..f69e3d9 100644 --- a/src/providers/sync-provider/sync-provider.tsx +++ b/src/providers/sync-provider/sync-provider.tsx @@ -1,25 +1,25 @@ import { PowerSyncDatabase } from '@powersync/web'; -import { PowerSyncContext } from "@powersync/react"; +import { PowerSyncContext, usePowerSync } from "@powersync/react"; import { createContext, Suspense, useContext, useEffect, useState } from 'react'; 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 = wrapPowerSyncWithDrizzle(powerSyncDb, { schema: drizzleSchema, }); + + const ConnectorContext = createContext(null); export const useConnector = () => useContext(ConnectorContext); @@ -39,7 +39,11 @@ export const SyncProvider: React.FC = ({ children }) => { powerSync.connect(connector); }; setup(); - }, [powerSync, connector]) + }, [powerSync, connector]); + + + + return ( }> diff --git a/src/utils/cascade.ts b/src/utils/cascade.ts index 5355e49..0629bbf 100644 --- a/src/utils/cascade.ts +++ b/src/utils/cascade.ts @@ -1,3 +1,15 @@ +// ───────────────────────────────────────────────────────────────────────────── +// ❗ PowerSync Limitation: Manual Cascade Deletes Required +// +// PowerSync uses SQLite *views* instead of real tables, which disables support +// for standard SQLite features like FOREIGN KEY constraints and ON DELETE CASCADE. +// +// Because of this, any time we delete a parent record (like a table, field, or index), +// we must manually delete all dependent child records to maintain data integrity. +// +// These functions perform **manual cascade deletes** to avoid orphaned records. +// ───────────────────────────────────────────────────────────────────────────── + import { fields } from "@/lib/schemas/field-schema"; import { QueryResult } from "@powersync/web"; import { ExtractTablesWithRelations, inArray, or } from "drizzle-orm"; @@ -7,45 +19,92 @@ import { relationships } from "@/lib/schemas/relationship-schema"; import { field_indices } from "@/lib/schemas/field_index-schema"; import { indices } from "@/lib/schemas/index-schema"; - - -export const deleteFieldsWithCascade = async (ids: string[], tx: SQLiteTransaction<'async', QueryResult, any, ExtractTablesWithRelations>): Promise => { +// ───────────────────────────────────────────────────────────────────────────── +// deleteFieldsWithCascade +// +// Manually deletes all related data when a field is removed. +// Removes associated relationships (source/target), field-index mappings, then the field itself. +// Used when deleting one or more fields by ID. +// ───────────────────────────────────────────────────────────────────────────── +export const deleteFieldsWithCascade = async ( + ids: string[], + tx: SQLiteTransaction<'async', QueryResult, any, ExtractTablesWithRelations> +): Promise => { return Promise.all([ - tx.delete(relationships).where(or(inArray(relationships.sourceFieldId, ids), inArray(relationships.targetFieldId, ids))), + tx.delete(relationships).where(or( + inArray(relationships.sourceFieldId, ids), + inArray(relationships.targetFieldId, ids) + )), tx.delete(field_indices).where(inArray(field_indices.fieldId, ids)), tx.delete(fields).where(inArray(fields.id, ids)), ]); -} +}; -export const deleteTablesWithCascade = async (ids: string[], tx: SQLiteTransaction<'async', QueryResult, any, ExtractTablesWithRelations>): Promise => { +// ───────────────────────────────────────────────────────────────────────────── +// deleteTablesWithCascade +// +// Handles deletion of entire tables, and manually cascades to: +// - all fields belonging to the table +// - all indices attached to the table +// - all relationships and field-index mappings tied to those fields/indices +// +// This ensures full cleanup when a table is removed in PowerSync context. +// ───────────────────────────────────────────────────────────────────────────── +export const deleteTablesWithCascade = async ( + ids: string[], + tx: SQLiteTransaction<'async', QueryResult, any, ExtractTablesWithRelations> +): Promise => { + // Fetch all field IDs belonging to the tables let fieldIds: { id: string }[] | string[] = await tx.select({ id: fields.id }).from(fields).where(inArray(fields.tableId, ids)); + // Fetch all index IDs belonging to the tables let indicesIds: { id: string }[] | string[] = await tx.select({ id: indices.id }).from(indices).where(inArray(indices.tableId, ids)); - fieldIds = fieldIds.map((fieldId: { id: string }) => fieldId.id); - indicesIds = indicesIds.map((indexId: { id: string }) => indexId.id); + // Normalize into string arrays + fieldIds = fieldIds.map((fieldId) => fieldId.id); + indicesIds = indicesIds.map((indexId) => indexId.id); return Promise.all([ - tx.delete(relationships).where(or(inArray(relationships.sourceFieldId, fieldIds), inArray(relationships.targetFieldId, fieldIds))), + // Delete relationships where fields are involved + tx.delete(relationships).where(or( + inArray(relationships.sourceFieldId, fieldIds), + inArray(relationships.targetFieldId, fieldIds) + )), + + // Delete field-index links for those fields tx.delete(field_indices).where(inArray(field_indices.fieldId, fieldIds)), + + // Delete all fields in the table tx.delete(fields).where(inArray(fields.id, fieldIds)), + + // Delete field-index links for indices tx.delete(field_indices).where(inArray(field_indices.indexId, indicesIds)), + + // Delete all indices belonging to the table tx.delete(indices).where(inArray(indices.id, indicesIds)), + + // Finally, delete the table(s) themselves tx.delete(tables).where(inArray(tables.id, ids)), + ]); +}; - ]) -} - - -export const deleteIndicesWithCascade = async (ids: string[], tx: SQLiteTransaction<'async', QueryResult, any, ExtractTablesWithRelations>): Promise => { +// ───────────────────────────────────────────────────────────────────────────── +// deleteIndicesWithCascade +// +// When removing an index, this ensures that any field-index links are removed +// before the index itself is deleted. Prevents dangling associations. +// ───────────────────────────────────────────────────────────────────────────── +export const deleteIndicesWithCascade = async ( + ids: string[], + tx: SQLiteTransaction<'async', QueryResult, any, ExtractTablesWithRelations> +): Promise => { return Promise.all([ tx.delete(field_indices).where(inArray(field_indices.indexId, ids)), tx.delete(indices).where(inArray(indices.id, ids)) - ]) - -} \ No newline at end of file + ]); +}; diff --git a/src/utils/database.ts b/src/utils/database.ts index 0217358..43a374f 100644 --- a/src/utils/database.ts +++ b/src/utils/database.ts @@ -279,7 +279,10 @@ export function normalizeDatabase(db: DatabaseType): any { { ...table, fields: Object.fromEntries( - table.fields.map(field => [field.id, field]) + table.fields.map(field => [field.id, { + ...field , + type : undefined + }]) ), indices: Object.fromEntries( table.indices.map(index => [index.id, {