Skip to content

persist arrows to db #222

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Feb 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions api/prisma/migrations/20230227022751_refine_edge/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
Warnings:

- The primary key for the `Edge` table will be changed. If it partially fails, the table could be left without primary key constraint.
- You are about to drop the column `fromId` on the `Edge` table. All the data in the column will be lost.
- You are about to drop the column `toId` on the `Edge` table. All the data in the column will be lost.
- You are about to drop the column `podsId` on the `Repo` table. All the data in the column will be lost.
- Added the required column `sourceId` to the `Edge` table without a default value. This is not possible if the table is not empty.
- Added the required column `targetId` to the `Edge` table without a default value. This is not possible if the table is not empty.

*/
-- DropForeignKey
ALTER TABLE "Edge" DROP CONSTRAINT "Edge_fromId_fkey";

-- DropForeignKey
ALTER TABLE "Edge" DROP CONSTRAINT "Edge_toId_fkey";

-- AlterTable
ALTER TABLE "Edge" DROP CONSTRAINT "Edge_pkey",
DROP COLUMN "fromId",
DROP COLUMN "toId",
ADD COLUMN "repoId" TEXT,
ADD COLUMN "sourceId" TEXT NOT NULL,
ADD COLUMN "targetId" TEXT NOT NULL,
ADD CONSTRAINT "Edge_pkey" PRIMARY KEY ("sourceId", "targetId");

-- AlterTable
ALTER TABLE "Repo" DROP COLUMN "podsId";

-- AddForeignKey
ALTER TABLE "Edge" ADD CONSTRAINT "Edge_sourceId_fkey" FOREIGN KEY ("sourceId") REFERENCES "Pod"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "Edge" ADD CONSTRAINT "Edge_targetId_fkey" FOREIGN KEY ("targetId") REFERENCES "Pod"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "Edge" ADD CONSTRAINT "Edge_repoId_fkey" FOREIGN KEY ("repoId") REFERENCES "Repo"("id") ON DELETE SET NULL ON UPDATE CASCADE;
18 changes: 10 additions & 8 deletions api/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ model Repo {
owner User @relation("OWNER", fields: [userId], references: [id])
userId String
pods Pod[] @relation("BELONG")
podsId String[]
edges Edge[]
public Boolean @default(false)
collaborators User[] @relation("COLLABORATOR")
createdAt DateTime @default(now())
Expand All @@ -88,12 +88,14 @@ enum PodType {
}

model Edge {
from Pod @relation("FROM", fields: [fromId], references: [id])
fromId String
to Pod @relation("TO", fields: [toId], references: [id])
toId String
source Pod @relation("SOURCE", fields: [sourceId], references: [id])
sourceId String
target Pod @relation("TARGET", fields: [targetId], references: [id])
targetId String
repo Repo? @relation(fields: [repoId], references: [id])
repoId String?

@@id([fromId, toId])
@@id([sourceId, targetId])
}

model Pod {
Expand Down Expand Up @@ -146,6 +148,6 @@ model Pod {

repoId String
// this is just a place holder. Not useful
to Edge[] @relation("TO")
from Edge[] @relation("FROM")
source Edge[] @relation("SOURCE")
target Edge[] @relation("TARGET")
}
62 changes: 61 additions & 1 deletion api/src/resolver_repo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,11 +151,69 @@ async function repo(_, { id }, { userId }) {
index: "asc",
},
},
edges: true,
},
});
if (!repo) throw Error("Repo not found");
await updateUserRepoData({ userId, repoId: id });
return repo;
return {
...repo,
edges: repo.edges.map((edge) => ({
source: edge.sourceId,
target: edge.targetId,
})),
};
}

async function addEdge(_, { source, target }, { userId }) {
if (!userId) throw new Error("Not authenticated.");
const sourcePod = await prisma.pod.findFirst({ where: { id: source } });
const targetPod = await prisma.pod.findFirst({ where: { id: target } });
if (!sourcePod || !targetPod) throw new Error("Pods not found.");
if (sourcePod.repoId !== targetPod.repoId)
throw new Error("Pods are not in the same repo.");
await ensureRepoEditAccess({ repoId: sourcePod.repoId, userId });
await prisma.edge.create({
data: {
source: {
connect: {
id: source,
},
},
target: {
connect: {
id: target,
},
},
repo: {
connect: {
id: sourcePod.repoId,
},
},
},
});
return true;
}

async function deleteEdge(_, { source, target }, { userId }) {
if (!userId) throw new Error("Not authenticated.");
const sourcePod = await prisma.pod.findFirst({ where: { id: source } });
const targetPod = await prisma.pod.findFirst({ where: { id: target } });
if (!sourcePod || !targetPod) throw new Error("Pods not found.");
if (sourcePod.repoId !== targetPod.repoId)
throw new Error("Pods are not in the same repo.");
await ensureRepoEditAccess({ repoId: sourcePod.repoId, userId });
await prisma.edge.deleteMany({
where: {
source: {
id: source,
},
target: {
id: target,
},
},
});
return true;
}

async function createRepo(_, { id, name, isPublic }, { userId }) {
Expand Down Expand Up @@ -435,6 +493,8 @@ export default {
deleteRepo,
updatePod,
deletePod,
addEdge,
deleteEdge,
addCollaborator,
updateVisibility,
deleteCollaborator,
Expand Down
8 changes: 8 additions & 0 deletions api/src/typedefs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,19 @@ export const typeDefs = gql`
id: ID!
name: String
pods: [Pod]
edges: [Edge]
userId: ID!
collaborators: [User]
public: Boolean
createdAt: String
updatedAt: String
}

type Edge {
source: String!
target: String!
}

type Pod {
id: ID!
type: String
Expand Down Expand Up @@ -121,6 +127,8 @@ export const typeDefs = gql`
deletePod(id: String!, toDelete: [String]): Boolean
addPods(repoId: String!, pods: [PodInput]): Boolean
updatePod(id: String!, repoId: String!, input: PodInput): Boolean
addEdge(source: ID!, target: ID!): Boolean
deleteEdge(source: ID!, target: ID!): Boolean
clearUser: Boolean
clearRepo: Boolean
clearPod: Boolean
Expand Down
94 changes: 82 additions & 12 deletions ui/src/components/Canvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import ReactFlow, {
MarkerType,
Node,
ReactFlowProvider,
Edge,
} from "reactflow";
import "reactflow/dist/style.css";

Expand All @@ -33,7 +34,7 @@ import { useStore } from "zustand";

import { RepoContext } from "../lib/store";
import { dbtype2nodetype, nodetype2dbtype } from "../lib/utils";
import { useYjsObserver } from "../lib/nodes";
import { useEdgesYjsObserver, useYjsObserver } from "../lib/nodes";

import { useApolloClient } from "@apollo/client";
import { CanvasContextMenu } from "./CanvasContextMenu";
Expand Down Expand Up @@ -149,14 +150,60 @@ function verifyConsistency(nodes: Node[], nodesMap: YMap<Node>) {
return true;
}

function verifyEdgeConsistency(edges: Edge[], edgesMap: YMap<Edge>) {
let keys = new Set(edgesMap.keys());
let edgesMap2 = new Map<string, Edge>();
edges.forEach((edge) => edgesMap2.set(edge.id, edge));
let keys2 = new Set(edgesMap2.keys());
if (keys.size !== keys2.size) {
console.error("key sizes are not the same", keys, keys2);
return false;
}
for (let i = 0; i < keys.size; i++) {
if (keys[i] !== keys2[i]) {
console.error("keys are not the same", keys, keys2);
return false;
}
}
// verify the values
for (let key of Array.from(keys)) {
let edge1 = edgesMap.get(key);
let edge2 = edgesMap2.get(key);
if (!edge1) {
console.error("edge1 is undefined");
return false;
}
if (!edge2) {
console.error("edge2 is undefined");
return false;
}
if (edge1.id !== edge2.id) {
console.error("edge id are not the same", edge1.id, edge2.id, "key", key);
return false;
}
if (edge1.source !== edge2.source) {
console.error("edge source are not the same", edge1.source, edge2.source);
return false;
}
if (edge1.target !== edge2.target) {
console.error("edge target are not the same", edge1.target, edge2.target);
return false;
}
}
return true;
}

function useInitNodes() {
const store = useContext(RepoContext)!;
const getPod = useStore(store, (state) => state.getPod);
const nodesMap = useStore(store, (state) => state.ydoc.getMap<Node>("pods"));
const edgesMap = useStore(store, (state) => state.ydoc.getMap<Edge>("edges"));
const arrows = useStore(store, (state) => state.arrows);
const getId2children = useStore(store, (state) => state.getId2children);
const provider = useStore(store, (state) => state.provider);
const [loading, setLoading] = useState(true);
const updateView = useStore(store, (state) => state.updateView);
const updateEdgeView = useStore(store, (state) => state.updateEdgeView);
const adjustLevel = useStore(store, (state) => state.adjustLevel);
useEffect(() => {
const init = () => {
Expand All @@ -173,15 +220,7 @@ function useInitNodes() {
let nodesMap2 = new Map<string, Node>();
nodes.forEach((node) => nodesMap2.set(node.id, node));
// Not only should we set nodes, but also delete.
nodesMap.forEach((node, key) => {
if (!nodesMap2.has(key)) {
console.error(`Yjs has key ${key} that is not in database.`);
// FIXME CAUTION This will delete the node in the database! Be
// careful! For now, just log errors and do not delete.
//
nodesMap.delete(key);
}
});
nodesMap.clear();
// add the nodes, so that the nodesMap is consistent with the database.
nodes.forEach((node) => {
nodesMap.set(node.id, node);
Expand All @@ -190,8 +229,36 @@ function useInitNodes() {
// NOTE we have to trigger an update here, otherwise the nodes are not
// rendered.
// triggerUpdate();
// adjust level and update view
adjustLevel();
updateView();
// handling the arrows
isConsistent = verifyEdgeConsistency(
arrows.map(({ source, target }) => ({
source,
target,
id: `${source}_${target}`,
})),
edgesMap
);
if (!isConsistent) {
console.warn("The yjs server is not consistent with the database.");
// delete the old keys
edgesMap.clear();
arrows.forEach(({ target, source }) => {
const edge: Edge = {
id: `${source}_${target}`,
source,
sourceHandle: "top",
target,
targetHandle: "top",
};
edgesMap.set(edge.id, edge);
// This isn't working. I need to set {edges} manually (from edgesMap)
// reactFlowInstance.addEdges(edge);
});
}
updateEdgeView();
setLoading(false);
};

Expand Down Expand Up @@ -391,6 +458,7 @@ function CanvasImplWrap() {
const reactFlowWrapper = useRef<any>(null);

useYjsObserver();
useEdgesYjsObserver();
usePaste(reactFlowWrapper);
useCut(reactFlowWrapper);

Expand Down Expand Up @@ -419,8 +487,10 @@ function CanvasImpl() {
const onNodesChange = useStore(store, (state) =>
state.onNodesChange(apolloClient)
);
const onEdgesChange = useStore(store, (state) => state.onEdgesChange);
const onConnect = useStore(store, (state) => state.onConnect);
const onEdgesChange = useStore(store, (state) =>
state.onEdgesChange(apolloClient)
);
const onConnect = useStore(store, (state) => state.onConnect(apolloClient));
const moveIntoScope = useStore(store, (state) => state.moveIntoScope);
const setDragHighlight = useStore(store, (state) => state.setDragHighlight);
const removeDragHighlight = useStore(
Expand Down
11 changes: 9 additions & 2 deletions ui/src/components/nodes/FloatingEdge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,14 @@ import { useStore, getStraightPath, EdgeProps } from "reactflow";

import { getEdgeParams } from "./utils";

function FloatingEdge({ id, source, target, markerEnd, style }: EdgeProps) {
function FloatingEdge({
id,
source,
target,
markerEnd,
style,
selected,
}: EdgeProps) {
const sourceNode = useStore(
useCallback((store) => store.nodeInternals.get(source), [source])
);
Expand All @@ -30,7 +37,7 @@ function FloatingEdge({ id, source, target, markerEnd, style }: EdgeProps) {
className="react-flow__edge-path"
d={edgePath}
markerEnd={markerEnd}
style={style}
style={selected ? { ...style, stroke: "red" } : style}
/>
);
}
Expand Down
7 changes: 7 additions & 0 deletions ui/src/lib/fetch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ export async function doRemoteLoadRepo(client: ApolloClient<any>, id: string) {
lastname
}
public
edges {
source
target
}
pods {
id
type
Expand Down Expand Up @@ -70,8 +74,10 @@ export async function doRemoteLoadRepo(client: ApolloClient<any>, id: string) {
await client.refetchQueries({ include: ["GetRepos", "GetCollabRepos"] });
// We need to do a deep copy here, because apollo client returned immutable objects.
let pods = res.data.repo.pods.map((pod) => ({ ...pod }));
let edges = res.data.repo.edges;
return {
pods,
edges,
name: res.data.repo.name,
error: null,
userId: res.data.repo.userId,
Expand All @@ -82,6 +88,7 @@ export async function doRemoteLoadRepo(client: ApolloClient<any>, id: string) {
console.log(e);
return {
pods: [],
edges: [],
name: "",
error: e,
userId: null,
Expand Down
Loading