Files
chartdb/src/context/chartdb-context/chartdb-provider.tsx
2024-08-23 01:00:15 +03:00

952 lines
31 KiB
TypeScript

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<React.PropsWithChildren> = ({
children,
}) => {
const db = useStorage();
const { addUndoAction, resetRedoStack } = useRedoUndoStack();
const [diagramId, setDiagramId] = useState('');
const [diagramName, setDiagramName] = useState('');
const [diagramCreatedAt, setDiagramCreatedAt] = useState<Date>(new Date());
const [diagramUpdatedAt, setDiagramUpdatedAt] = useState<Date>(new Date());
const [databaseType, setDatabaseType] = useState<DatabaseType>(
DatabaseType.GENERIC
);
const [tables, setTables] = useState<DBTable[]>([]);
const [relationships, setRelationships] = useState<DBRelationship[]>([]);
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<DBTable>,
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<DBTable, 'id'>[],
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<DBField>,
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<DBIndex>,
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<DBRelationship>,
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 (
<chartDBContext.Provider
value={{
diagramId,
diagramName,
databaseType,
tables,
relationships,
currentDiagram,
updateDiagramId,
updateDiagramName,
loadDiagram,
updateDatabaseType,
updateDiagramUpdatedAt,
createTable,
addTable,
getTable,
removeTable,
updateTable,
updateTablesState,
updateField,
removeField,
createField,
addField,
addIndex,
createIndex,
removeIndex,
getField,
getIndex,
updateIndex,
addRelationship,
addRelationships,
createRelationship,
getRelationship,
removeRelationship,
removeRelationships,
updateRelationship,
}}
>
{children}
</chartDBContext.Provider>
);
};