import React, { useCallback, useMemo, useState } from 'react'; import { DBTable } from '@/lib/domain/db-table'; import { generateId, randomHSLA } from '@/lib/utils'; import { ChartDBContext, chartDBContext } from './chartdb-context'; import { DatabaseType } from '@/lib/domain/database-type'; import { DBField } from '@/lib/domain/db-field'; import { DBIndex } from '@/lib/domain/db-index'; import { DBRelationship } from '@/lib/domain/db-relationship'; import { useStorage } from '@/hooks/use-storage'; import { useRedoUndoStack } from '@/hooks/use-redo-undo-stack'; import { Diagram } from '@/lib/domain/diagram'; export const ChartDBProvider: React.FC = ({ children, }) => { const db = useStorage(); const { addUndoAction, resetRedoStack } = useRedoUndoStack(); const [diagramId, setDiagramId] = useState(''); const [diagramName, setDiagramName] = useState(''); const [diagramCreatedAt, setDiagramCreatedAt] = useState(new Date()); const [diagramUpdatedAt, setDiagramUpdatedAt] = useState(new Date()); const [databaseType, setDatabaseType] = useState( DatabaseType.GENERIC ); const [tables, setTables] = useState([]); const [relationships, setRelationships] = useState([]); const currentDiagram: Diagram = useMemo( () => ({ id: diagramId, name: diagramName, createdAt: diagramCreatedAt, updatedAt: diagramUpdatedAt, databaseType, tables, relationships, }), [ diagramId, diagramName, databaseType, tables, relationships, diagramCreatedAt, diagramUpdatedAt, ] ); const updateDiagramUpdatedAt: ChartDBContext['updateDiagramUpdatedAt'] = useCallback(async () => { const updatedAt = new Date(); setDiagramUpdatedAt(updatedAt); await db.updateDiagram({ id: diagramId, attributes: { updatedAt }, }); }, [db, diagramId, setDiagramUpdatedAt]); const updateDatabaseType: ChartDBContext['updateDatabaseType'] = useCallback( async (databaseType) => { setDatabaseType(databaseType); await db.updateDiagram({ id: diagramId, attributes: { databaseType, }, }); }, [db, diagramId, setDatabaseType] ); const updateDiagramId: ChartDBContext['updateDiagramId'] = useCallback( async (id) => { const prevId = diagramId; setDiagramId(id); await db.updateDiagram({ id: prevId, attributes: { id } }); }, [db, diagramId, setDiagramId] ); const updateDiagramName: ChartDBContext['updateDiagramName'] = useCallback( async (name, options = { updateHistory: true }) => { const prevName = diagramName; setDiagramName(name); const updatedAt = new Date(); setDiagramUpdatedAt(updatedAt); await db.updateDiagram({ id: diagramId, attributes: { name, updatedAt }, }); if (options.updateHistory) { addUndoAction({ action: 'updateDiagramName', redoData: { name }, undoData: { name: prevName }, }); resetRedoStack(); } }, [ db, diagramId, setDiagramName, addUndoAction, diagramName, resetRedoStack, ] ); const addTable: ChartDBContext['addTable'] = useCallback( async (table: DBTable, options = { updateHistory: true }) => { setTables((tables) => [...tables, table]); const updatedAt = new Date(); setDiagramUpdatedAt(updatedAt); await Promise.all([ db.updateDiagram({ id: diagramId, attributes: { updatedAt } }), db.addTable({ diagramId, table }), ]); if (options.updateHistory) { addUndoAction({ action: 'addTable', redoData: { table }, undoData: { tableId: table.id }, }); resetRedoStack(); } }, [db, diagramId, setTables, addUndoAction, resetRedoStack] ); const createTable: ChartDBContext['createTable'] = useCallback(async () => { const table: DBTable = { id: generateId(), name: `table_${tables.length + 1}`, x: 0, y: 0, fields: [ { id: generateId(), name: 'id', type: databaseType === DatabaseType.SQLITE ? 'integer' : 'bigint', unique: true, nullable: false, primaryKey: true, createdAt: Date.now(), }, ], indexes: [], color: randomHSLA(), createdAt: Date.now(), isView: false, }; await addTable(table); return table; }, [addTable, tables, databaseType]); const getTable: ChartDBContext['getTable'] = useCallback( (id: string) => tables.find((table) => table.id === id) ?? null, [tables] ); const removeTable: ChartDBContext['removeTable'] = useCallback( async (id: string, options = { updateHistory: true }) => { const table = getTable(id); setTables((tables) => tables.filter((table) => table.id !== id)); const updatedAt = new Date(); setDiagramUpdatedAt(updatedAt); await Promise.all([ db.updateDiagram({ id: diagramId, attributes: { updatedAt } }), db.deleteTable({ diagramId, id }), ]); if (!!table && options.updateHistory) { addUndoAction({ action: 'removeTable', redoData: { tableId: id }, undoData: { table }, }); resetRedoStack(); } }, [db, diagramId, setTables, addUndoAction, resetRedoStack, getTable] ); const updateTable: ChartDBContext['updateTable'] = useCallback( async ( id: string, table: Partial, options = { updateHistory: true } ) => { const prevTable = getTable(id); setTables((tables) => tables.map((t) => (t.id === id ? { ...t, ...table } : t)) ); const updatedAt = new Date(); setDiagramUpdatedAt(updatedAt); await Promise.all([ db.updateDiagram({ id: diagramId, attributes: { updatedAt } }), db.updateTable({ id, attributes: table }), ]); if (!!prevTable && options.updateHistory) { addUndoAction({ action: 'updateTable', redoData: { tableId: id, table }, undoData: { tableId: id, table: prevTable }, }); resetRedoStack(); } }, [db, setTables, addUndoAction, resetRedoStack, getTable, diagramId] ); const updateTablesState: ChartDBContext['updateTablesState'] = useCallback( async ( updateFn: (tables: DBTable[]) => PartialExcept[], options = { updateHistory: true, forceOverride: false } ) => { const updateTables = (prevTables: DBTable[]) => { const updatedTables = updateFn(prevTables); if (options.forceOverride) { return updatedTables as DBTable[]; } return prevTables .map((prevTable) => { const updatedTable = updatedTables.find( (t) => t.id === prevTable.id ); return updatedTable ? { ...prevTable, ...updatedTable } : prevTable; }) .filter((prevTable) => updatedTables.some((t) => t.id === prevTable.id) ); }; const prevTables = [...tables]; // const updatedTablesAttrs = updateFn(tables); const updatedTables = updateTables(tables); setTables(updateTables); const promises = []; for (const updatedTable of updatedTables) { promises.push( db.updateTable({ id: updatedTable.id, attributes: updatedTable, }) ); } const tablesToDelete = tables.filter( (table) => !updatedTables.some((t) => t.id === table.id) ); for (const table of tablesToDelete) { promises.push(db.deleteTable({ diagramId, id: table.id })); } const updatedAt = new Date(); setDiagramUpdatedAt(updatedAt); promises.push( db.updateDiagram({ id: diagramId, attributes: { updatedAt } }) ); await Promise.all(promises); if (options.updateHistory) { addUndoAction({ action: 'updateTablesState', redoData: { tables: updatedTables }, undoData: { tables: prevTables }, }); resetRedoStack(); } }, [db, tables, setTables, diagramId, addUndoAction, resetRedoStack] ); const getField: ChartDBContext['getField'] = useCallback( (tableId: string, fieldId: string) => { const table = getTable(tableId); return table?.fields.find((f) => f.id === fieldId) ?? null; }, [getTable] ); const updateField: ChartDBContext['updateField'] = useCallback( async ( tableId: string, fieldId: string, field: Partial, options = { updateHistory: true } ) => { const prevField = getField(tableId, fieldId); setTables((tables) => tables.map((table) => table.id === tableId ? { ...table, fields: table.fields.map((f) => f.id === fieldId ? { ...f, ...field } : f ), } : table ) ); const table = await db.getTable({ diagramId, id: tableId }); if (!table) { return; } const updatedAt = new Date(); setDiagramUpdatedAt(updatedAt); await Promise.all([ db.updateDiagram({ id: diagramId, attributes: { updatedAt } }), db.updateTable({ id: tableId, attributes: { ...table, fields: table.fields.map((f) => f.id === fieldId ? { ...f, ...field } : f ), }, }), ]); if (!!prevField && options.updateHistory) { addUndoAction({ action: 'updateField', redoData: { tableId, fieldId, field: { ...prevField, ...field }, }, undoData: { tableId, fieldId, field: prevField }, }); resetRedoStack(); } }, [db, diagramId, setTables, addUndoAction, resetRedoStack, getField] ); const removeField: ChartDBContext['removeField'] = useCallback( async ( tableId: string, fieldId: string, options = { updateHistory: true } ) => { const prevField = getField(tableId, fieldId); setTables((tables) => tables.map((table) => table.id === tableId ? { ...table, fields: table.fields.filter( (f) => f.id !== fieldId ), } : table ) ); const table = await db.getTable({ diagramId, id: tableId }); if (!table) { return; } const updatedAt = new Date(); setDiagramUpdatedAt(updatedAt); await Promise.all([ db.updateDiagram({ id: diagramId, attributes: { updatedAt } }), db.updateTable({ id: tableId, attributes: { ...table, fields: table.fields.filter((f) => f.id !== fieldId), }, }), ]); if (!!prevField && options.updateHistory) { addUndoAction({ action: 'removeField', redoData: { tableId, fieldId }, undoData: { tableId, field: prevField }, }); resetRedoStack(); } }, [db, diagramId, setTables, addUndoAction, resetRedoStack, getField] ); const addField: ChartDBContext['addField'] = useCallback( async ( tableId: string, field: DBField, options = { updateHistory: true } ) => { setTables((tables) => tables.map((table) => table.id === tableId ? { ...table, fields: [...table.fields, field] } : table ) ); const table = await db.getTable({ diagramId, id: tableId }); if (!table) { return; } const updatedAt = new Date(); setDiagramUpdatedAt(updatedAt); await Promise.all([ db.updateDiagram({ id: diagramId, attributes: { updatedAt } }), db.updateTable({ id: tableId, attributes: { ...table, fields: [...table.fields, field], }, }), ]); if (options.updateHistory) { addUndoAction({ action: 'addField', redoData: { tableId, field }, undoData: { tableId, fieldId: field.id }, }); resetRedoStack(); } }, [db, diagramId, setTables, addUndoAction, resetRedoStack] ); const createField: ChartDBContext['createField'] = useCallback( async (tableId: string) => { const table = getTable(tableId); const field: DBField = { id: generateId(), name: `field_${(table?.fields?.length ?? 0) + 1}`, type: 'bigint', unique: false, nullable: true, primaryKey: false, createdAt: Date.now(), }; await addField(tableId, field); return field; }, [addField, getTable] ); const getIndex: ChartDBContext['getIndex'] = useCallback( (tableId: string, indexId: string) => { const table = getTable(tableId); return table?.indexes.find((i) => i.id === indexId) ?? null; }, [getTable] ); const addIndex: ChartDBContext['addIndex'] = useCallback( async ( tableId: string, index: DBIndex, options = { updateHistory: true } ) => { setTables((tables) => tables.map((table) => table.id === tableId ? { ...table, indexes: [...table.indexes, index] } : table ) ); const dbTable = await db.getTable({ diagramId, id: tableId }); if (!dbTable) { return; } const updatedAt = new Date(); setDiagramUpdatedAt(updatedAt); await Promise.all([ db.updateDiagram({ id: diagramId, attributes: { updatedAt } }), db.updateTable({ id: tableId, attributes: { ...dbTable, indexes: [...dbTable.indexes, index], }, }), ]); if (options.updateHistory) { addUndoAction({ action: 'addIndex', redoData: { tableId, index }, undoData: { tableId, indexId: index.id }, }); resetRedoStack(); } }, [db, diagramId, setTables, addUndoAction, resetRedoStack] ); const removeIndex: ChartDBContext['removeIndex'] = useCallback( async ( tableId: string, indexId: string, options = { updateHistory: true } ) => { const prevIndex = getIndex(tableId, indexId); setTables((tables) => tables.map((table) => table.id === tableId ? { ...table, indexes: table.indexes.filter( (i) => i.id !== indexId ), } : table ) ); const dbTable = await db.getTable({ diagramId, id: tableId, }); if (!dbTable) { return; } const updatedAt = new Date(); setDiagramUpdatedAt(updatedAt); await Promise.all([ db.updateDiagram({ id: diagramId, attributes: { updatedAt } }), db.updateTable({ id: tableId, attributes: { ...dbTable, indexes: dbTable.indexes.filter( (i) => i.id !== indexId ), }, }), ]); if (!!prevIndex && options.updateHistory) { addUndoAction({ action: 'removeIndex', redoData: { indexId, tableId }, undoData: { tableId, index: prevIndex }, }); resetRedoStack(); } }, [db, diagramId, setTables, addUndoAction, resetRedoStack, getIndex] ); const createIndex: ChartDBContext['createIndex'] = useCallback( async (tableId: string) => { const table = getTable(tableId); const index: DBIndex = { id: generateId(), name: `index_${(table?.indexes?.length ?? 0) + 1}`, fieldIds: [], unique: false, createdAt: Date.now(), }; await addIndex(tableId, index); return index; }, [addIndex, getTable] ); const updateIndex: ChartDBContext['updateIndex'] = useCallback( async ( tableId: string, indexId: string, index: Partial, options = { updateHistory: true } ) => { const prevIndex = getIndex(tableId, indexId); setTables((tables) => tables.map((table) => table.id === tableId ? { ...table, indexes: table.indexes.map((i) => i.id === indexId ? { ...i, ...index } : i ), } : table ) ); const dbTable = await db.getTable({ diagramId, id: tableId }); if (!dbTable) { return; } const updatedAt = new Date(); setDiagramUpdatedAt(updatedAt); await Promise.all([ db.updateDiagram({ id: diagramId, attributes: { updatedAt } }), db.updateTable({ id: tableId, attributes: { ...dbTable, indexes: dbTable.indexes.map((i) => i.id === indexId ? { ...i, ...index } : i ), }, }), ]); if (!!prevIndex && options.updateHistory) { addUndoAction({ action: 'updateIndex', redoData: { tableId, indexId, index }, undoData: { tableId, indexId, index: prevIndex }, }); resetRedoStack(); } }, [db, diagramId, setTables, addUndoAction, resetRedoStack, getIndex] ); const addRelationship: ChartDBContext['addRelationship'] = useCallback( async ( relationship: DBRelationship, options = { updateHistory: true } ) => { setRelationships((relationships) => [ ...relationships, relationship, ]); const updatedAt = new Date(); setDiagramUpdatedAt(updatedAt); await Promise.all([ db.updateDiagram({ id: diagramId, attributes: { updatedAt } }), db.addRelationship({ diagramId, relationship }), ]); if (options.updateHistory) { addUndoAction({ action: 'addRelationship', redoData: { relationship }, undoData: { relationshipId: relationship.id }, }); resetRedoStack(); } }, [db, diagramId, setRelationships, addUndoAction, resetRedoStack] ); const addRelationships: ChartDBContext['addRelationships'] = useCallback( async ( relationships: DBRelationship[], options = { updateHistory: true } ) => { setRelationships((currentRelationships) => [ ...currentRelationships, ...relationships, ]); const updatedAt = new Date(); setDiagramUpdatedAt(updatedAt); await Promise.all([ ...relationships.map((relationship) => db.addRelationship({ diagramId, relationship }) ), db.updateDiagram({ id: diagramId, attributes: { updatedAt } }), ]); if (options.updateHistory) { addUndoAction({ action: 'addRelationships', redoData: { relationships }, undoData: { relationshipIds: relationships.map((r) => r.id), }, }); resetRedoStack(); } }, [db, diagramId, setRelationships, addUndoAction, resetRedoStack] ); const createRelationship: ChartDBContext['createRelationship'] = useCallback( async ({ sourceTableId, targetTableId, sourceFieldId, targetFieldId, }) => { const sourceTableName = getTable(sourceTableId)?.name ?? ''; const targetTableName = getTable(targetTableId)?.name ?? ''; const relationship: DBRelationship = { id: generateId(), name: `${sourceTableName}_${targetTableName}_fk`, sourceTableId, targetTableId, sourceFieldId, targetFieldId, type: 'one_to_one', createdAt: Date.now(), }; await addRelationship(relationship); return relationship; }, [addRelationship, getTable] ); const getRelationship: ChartDBContext['getRelationship'] = useCallback( (id: string) => relationships.find((relationship) => relationship.id === id) ?? null, [relationships] ); const removeRelationship: ChartDBContext['removeRelationship'] = useCallback( async (id: string, options = { updateHistory: true }) => { const relationship = getRelationship(id); setRelationships((relationships) => relationships.filter( (relationship) => relationship.id !== id ) ); const updatedAt = new Date(); setDiagramUpdatedAt(updatedAt); await Promise.all([ db.updateDiagram({ id: diagramId, attributes: { updatedAt }, }), db.deleteRelationship({ diagramId, id }), ]); if (!!relationship && options.updateHistory) { addUndoAction({ action: 'removeRelationship', redoData: { relationshipId: id }, undoData: { relationship }, }); resetRedoStack(); } }, [ db, diagramId, setRelationships, addUndoAction, resetRedoStack, getRelationship, ] ); const removeRelationships: ChartDBContext['removeRelationships'] = useCallback( async (ids: string[], options = { updateHistory: true }) => { const prevRelationships = [ ...relationships.filter((relationship) => ids.includes(relationship.id) ), ]; setRelationships((relationships) => relationships.filter( (relationship) => !ids.includes(relationship.id) ) ); const updatedAt = new Date(); setDiagramUpdatedAt(updatedAt); await Promise.all([ ...ids.map((id) => db.deleteRelationship({ diagramId, id }) ), db.updateDiagram({ id: diagramId, attributes: { updatedAt }, }), ]); if (prevRelationships.length > 0 && options.updateHistory) { addUndoAction({ action: 'removeRelationships', redoData: { relationshipsIds: ids }, undoData: { relationships: prevRelationships }, }); resetRedoStack(); } }, [ db, diagramId, setRelationships, relationships, addUndoAction, resetRedoStack, ] ); const updateRelationship: ChartDBContext['updateRelationship'] = useCallback( async ( id: string, relationship: Partial, options = { updateHistory: true } ) => { const prevRelationship = getRelationship(id); setRelationships((relationships) => relationships.map((r) => r.id === id ? { ...r, ...relationship } : r ) ); const updatedAt = new Date(); setDiagramUpdatedAt(updatedAt); await Promise.all([ db.updateDiagram({ id: diagramId, attributes: { updatedAt }, }), db.updateRelationship({ id, attributes: relationship }), ]); if (!!prevRelationship && options.updateHistory) { addUndoAction({ action: 'updateRelationship', redoData: { relationshipId: id, relationship }, undoData: { relationshipId: id, relationship: prevRelationship, }, }); resetRedoStack(); } }, [ db, setRelationships, addUndoAction, getRelationship, resetRedoStack, diagramId, ] ); const loadDiagram: ChartDBContext['loadDiagram'] = useCallback( async (diagramId: string) => { const diagram = await db.getDiagram(diagramId, { includeRelationships: true, includeTables: true, }); if (diagram) { setDiagramId(diagram.id); setDiagramName(diagram.name); setDatabaseType(diagram.databaseType); setTables(diagram?.tables ?? []); setRelationships(diagram?.relationships ?? []); setDiagramCreatedAt(diagram.createdAt); setDiagramUpdatedAt(diagram.updatedAt); } return diagram; }, [ db, setDiagramId, setDiagramName, setDatabaseType, setTables, setRelationships, setDiagramCreatedAt, setDiagramUpdatedAt, ] ); return ( {children} ); };