Skip to content

Commit 0303946

Browse files
authored
feat: persist arrows to db (#222)
* persist arrows to db * support yjs sync for add/delete arrows
1 parent b656a69 commit 0303946

File tree

10 files changed

+336
-46
lines changed

10 files changed

+336
-46
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
Warnings:
3+
4+
- The primary key for the `Edge` table will be changed. If it partially fails, the table could be left without primary key constraint.
5+
- You are about to drop the column `fromId` on the `Edge` table. All the data in the column will be lost.
6+
- You are about to drop the column `toId` on the `Edge` table. All the data in the column will be lost.
7+
- You are about to drop the column `podsId` on the `Repo` table. All the data in the column will be lost.
8+
- Added the required column `sourceId` to the `Edge` table without a default value. This is not possible if the table is not empty.
9+
- Added the required column `targetId` to the `Edge` table without a default value. This is not possible if the table is not empty.
10+
11+
*/
12+
-- DropForeignKey
13+
ALTER TABLE "Edge" DROP CONSTRAINT "Edge_fromId_fkey";
14+
15+
-- DropForeignKey
16+
ALTER TABLE "Edge" DROP CONSTRAINT "Edge_toId_fkey";
17+
18+
-- AlterTable
19+
ALTER TABLE "Edge" DROP CONSTRAINT "Edge_pkey",
20+
DROP COLUMN "fromId",
21+
DROP COLUMN "toId",
22+
ADD COLUMN "repoId" TEXT,
23+
ADD COLUMN "sourceId" TEXT NOT NULL,
24+
ADD COLUMN "targetId" TEXT NOT NULL,
25+
ADD CONSTRAINT "Edge_pkey" PRIMARY KEY ("sourceId", "targetId");
26+
27+
-- AlterTable
28+
ALTER TABLE "Repo" DROP COLUMN "podsId";
29+
30+
-- AddForeignKey
31+
ALTER TABLE "Edge" ADD CONSTRAINT "Edge_sourceId_fkey" FOREIGN KEY ("sourceId") REFERENCES "Pod"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
32+
33+
-- AddForeignKey
34+
ALTER TABLE "Edge" ADD CONSTRAINT "Edge_targetId_fkey" FOREIGN KEY ("targetId") REFERENCES "Pod"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
35+
36+
-- AddForeignKey
37+
ALTER TABLE "Edge" ADD CONSTRAINT "Edge_repoId_fkey" FOREIGN KEY ("repoId") REFERENCES "Repo"("id") ON DELETE SET NULL ON UPDATE CASCADE;

api/prisma/schema.prisma

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ model Repo {
6969
owner User @relation("OWNER", fields: [userId], references: [id])
7070
userId String
7171
pods Pod[] @relation("BELONG")
72-
podsId String[]
72+
edges Edge[]
7373
public Boolean @default(false)
7474
collaborators User[] @relation("COLLABORATOR")
7575
createdAt DateTime @default(now())
@@ -88,12 +88,14 @@ enum PodType {
8888
}
8989

9090
model Edge {
91-
from Pod @relation("FROM", fields: [fromId], references: [id])
92-
fromId String
93-
to Pod @relation("TO", fields: [toId], references: [id])
94-
toId String
91+
source Pod @relation("SOURCE", fields: [sourceId], references: [id])
92+
sourceId String
93+
target Pod @relation("TARGET", fields: [targetId], references: [id])
94+
targetId String
95+
repo Repo? @relation(fields: [repoId], references: [id])
96+
repoId String?
9597
96-
@@id([fromId, toId])
98+
@@id([sourceId, targetId])
9799
}
98100

99101
model Pod {
@@ -146,6 +148,6 @@ model Pod {
146148
147149
repoId String
148150
// this is just a place holder. Not useful
149-
to Edge[] @relation("TO")
150-
from Edge[] @relation("FROM")
151+
source Edge[] @relation("SOURCE")
152+
target Edge[] @relation("TARGET")
151153
}

api/src/resolver_repo.ts

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,11 +151,69 @@ async function repo(_, { id }, { userId }) {
151151
index: "asc",
152152
},
153153
},
154+
edges: true,
154155
},
155156
});
156157
if (!repo) throw Error("Repo not found");
157158
await updateUserRepoData({ userId, repoId: id });
158-
return repo;
159+
return {
160+
...repo,
161+
edges: repo.edges.map((edge) => ({
162+
source: edge.sourceId,
163+
target: edge.targetId,
164+
})),
165+
};
166+
}
167+
168+
async function addEdge(_, { source, target }, { userId }) {
169+
if (!userId) throw new Error("Not authenticated.");
170+
const sourcePod = await prisma.pod.findFirst({ where: { id: source } });
171+
const targetPod = await prisma.pod.findFirst({ where: { id: target } });
172+
if (!sourcePod || !targetPod) throw new Error("Pods not found.");
173+
if (sourcePod.repoId !== targetPod.repoId)
174+
throw new Error("Pods are not in the same repo.");
175+
await ensureRepoEditAccess({ repoId: sourcePod.repoId, userId });
176+
await prisma.edge.create({
177+
data: {
178+
source: {
179+
connect: {
180+
id: source,
181+
},
182+
},
183+
target: {
184+
connect: {
185+
id: target,
186+
},
187+
},
188+
repo: {
189+
connect: {
190+
id: sourcePod.repoId,
191+
},
192+
},
193+
},
194+
});
195+
return true;
196+
}
197+
198+
async function deleteEdge(_, { source, target }, { userId }) {
199+
if (!userId) throw new Error("Not authenticated.");
200+
const sourcePod = await prisma.pod.findFirst({ where: { id: source } });
201+
const targetPod = await prisma.pod.findFirst({ where: { id: target } });
202+
if (!sourcePod || !targetPod) throw new Error("Pods not found.");
203+
if (sourcePod.repoId !== targetPod.repoId)
204+
throw new Error("Pods are not in the same repo.");
205+
await ensureRepoEditAccess({ repoId: sourcePod.repoId, userId });
206+
await prisma.edge.deleteMany({
207+
where: {
208+
source: {
209+
id: source,
210+
},
211+
target: {
212+
id: target,
213+
},
214+
},
215+
});
216+
return true;
159217
}
160218

161219
async function createRepo(_, { id, name, isPublic }, { userId }) {
@@ -435,6 +493,8 @@ export default {
435493
deleteRepo,
436494
updatePod,
437495
deletePod,
496+
addEdge,
497+
deleteEdge,
438498
addCollaborator,
439499
updateVisibility,
440500
deleteCollaborator,

api/src/typedefs.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,19 @@ export const typeDefs = gql`
2323
id: ID!
2424
name: String
2525
pods: [Pod]
26+
edges: [Edge]
2627
userId: ID!
2728
collaborators: [User]
2829
public: Boolean
2930
createdAt: String
3031
updatedAt: String
3132
}
3233
34+
type Edge {
35+
source: String!
36+
target: String!
37+
}
38+
3339
type Pod {
3440
id: ID!
3541
type: String
@@ -121,6 +127,8 @@ export const typeDefs = gql`
121127
deletePod(id: String!, toDelete: [String]): Boolean
122128
addPods(repoId: String!, pods: [PodInput]): Boolean
123129
updatePod(id: String!, repoId: String!, input: PodInput): Boolean
130+
addEdge(source: ID!, target: ID!): Boolean
131+
deleteEdge(source: ID!, target: ID!): Boolean
124132
clearUser: Boolean
125133
clearRepo: Boolean
126134
clearPod: Boolean

ui/src/components/Canvas.tsx

Lines changed: 82 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import ReactFlow, {
2121
MarkerType,
2222
Node,
2323
ReactFlowProvider,
24+
Edge,
2425
} from "reactflow";
2526
import "reactflow/dist/style.css";
2627

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

3435
import { RepoContext } from "../lib/store";
3536
import { dbtype2nodetype, nodetype2dbtype } from "../lib/utils";
36-
import { useYjsObserver } from "../lib/nodes";
37+
import { useEdgesYjsObserver, useYjsObserver } from "../lib/nodes";
3738

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

153+
function verifyEdgeConsistency(edges: Edge[], edgesMap: YMap<Edge>) {
154+
let keys = new Set(edgesMap.keys());
155+
let edgesMap2 = new Map<string, Edge>();
156+
edges.forEach((edge) => edgesMap2.set(edge.id, edge));
157+
let keys2 = new Set(edgesMap2.keys());
158+
if (keys.size !== keys2.size) {
159+
console.error("key sizes are not the same", keys, keys2);
160+
return false;
161+
}
162+
for (let i = 0; i < keys.size; i++) {
163+
if (keys[i] !== keys2[i]) {
164+
console.error("keys are not the same", keys, keys2);
165+
return false;
166+
}
167+
}
168+
// verify the values
169+
for (let key of Array.from(keys)) {
170+
let edge1 = edgesMap.get(key);
171+
let edge2 = edgesMap2.get(key);
172+
if (!edge1) {
173+
console.error("edge1 is undefined");
174+
return false;
175+
}
176+
if (!edge2) {
177+
console.error("edge2 is undefined");
178+
return false;
179+
}
180+
if (edge1.id !== edge2.id) {
181+
console.error("edge id are not the same", edge1.id, edge2.id, "key", key);
182+
return false;
183+
}
184+
if (edge1.source !== edge2.source) {
185+
console.error("edge source are not the same", edge1.source, edge2.source);
186+
return false;
187+
}
188+
if (edge1.target !== edge2.target) {
189+
console.error("edge target are not the same", edge1.target, edge2.target);
190+
return false;
191+
}
192+
}
193+
return true;
194+
}
195+
152196
function useInitNodes() {
153197
const store = useContext(RepoContext)!;
154198
const getPod = useStore(store, (state) => state.getPod);
155199
const nodesMap = useStore(store, (state) => state.ydoc.getMap<Node>("pods"));
200+
const edgesMap = useStore(store, (state) => state.ydoc.getMap<Edge>("edges"));
201+
const arrows = useStore(store, (state) => state.arrows);
156202
const getId2children = useStore(store, (state) => state.getId2children);
157203
const provider = useStore(store, (state) => state.provider);
158204
const [loading, setLoading] = useState(true);
159205
const updateView = useStore(store, (state) => state.updateView);
206+
const updateEdgeView = useStore(store, (state) => state.updateEdgeView);
160207
const adjustLevel = useStore(store, (state) => state.adjustLevel);
161208
useEffect(() => {
162209
const init = () => {
@@ -173,15 +220,7 @@ function useInitNodes() {
173220
let nodesMap2 = new Map<string, Node>();
174221
nodes.forEach((node) => nodesMap2.set(node.id, node));
175222
// Not only should we set nodes, but also delete.
176-
nodesMap.forEach((node, key) => {
177-
if (!nodesMap2.has(key)) {
178-
console.error(`Yjs has key ${key} that is not in database.`);
179-
// FIXME CAUTION This will delete the node in the database! Be
180-
// careful! For now, just log errors and do not delete.
181-
//
182-
nodesMap.delete(key);
183-
}
184-
});
223+
nodesMap.clear();
185224
// add the nodes, so that the nodesMap is consistent with the database.
186225
nodes.forEach((node) => {
187226
nodesMap.set(node.id, node);
@@ -190,8 +229,36 @@ function useInitNodes() {
190229
// NOTE we have to trigger an update here, otherwise the nodes are not
191230
// rendered.
192231
// triggerUpdate();
232+
// adjust level and update view
193233
adjustLevel();
194234
updateView();
235+
// handling the arrows
236+
isConsistent = verifyEdgeConsistency(
237+
arrows.map(({ source, target }) => ({
238+
source,
239+
target,
240+
id: `${source}_${target}`,
241+
})),
242+
edgesMap
243+
);
244+
if (!isConsistent) {
245+
console.warn("The yjs server is not consistent with the database.");
246+
// delete the old keys
247+
edgesMap.clear();
248+
arrows.forEach(({ target, source }) => {
249+
const edge: Edge = {
250+
id: `${source}_${target}`,
251+
source,
252+
sourceHandle: "top",
253+
target,
254+
targetHandle: "top",
255+
};
256+
edgesMap.set(edge.id, edge);
257+
// This isn't working. I need to set {edges} manually (from edgesMap)
258+
// reactFlowInstance.addEdges(edge);
259+
});
260+
}
261+
updateEdgeView();
195262
setLoading(false);
196263
};
197264

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

393460
useYjsObserver();
461+
useEdgesYjsObserver();
394462
usePaste(reactFlowWrapper);
395463
useCut(reactFlowWrapper);
396464

@@ -419,8 +487,10 @@ function CanvasImpl() {
419487
const onNodesChange = useStore(store, (state) =>
420488
state.onNodesChange(apolloClient)
421489
);
422-
const onEdgesChange = useStore(store, (state) => state.onEdgesChange);
423-
const onConnect = useStore(store, (state) => state.onConnect);
490+
const onEdgesChange = useStore(store, (state) =>
491+
state.onEdgesChange(apolloClient)
492+
);
493+
const onConnect = useStore(store, (state) => state.onConnect(apolloClient));
424494
const moveIntoScope = useStore(store, (state) => state.moveIntoScope);
425495
const setDragHighlight = useStore(store, (state) => state.setDragHighlight);
426496
const removeDragHighlight = useStore(

ui/src/components/nodes/FloatingEdge.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,14 @@ import { useStore, getStraightPath, EdgeProps } from "reactflow";
33

44
import { getEdgeParams } from "./utils";
55

6-
function FloatingEdge({ id, source, target, markerEnd, style }: EdgeProps) {
6+
function FloatingEdge({
7+
id,
8+
source,
9+
target,
10+
markerEnd,
11+
style,
12+
selected,
13+
}: EdgeProps) {
714
const sourceNode = useStore(
815
useCallback((store) => store.nodeInternals.get(source), [source])
916
);
@@ -30,7 +37,7 @@ function FloatingEdge({ id, source, target, markerEnd, style }: EdgeProps) {
3037
className="react-flow__edge-path"
3138
d={edgePath}
3239
markerEnd={markerEnd}
33-
style={style}
40+
style={selected ? { ...style, stroke: "red" } : style}
3441
/>
3542
);
3643
}

ui/src/lib/fetch.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ export async function doRemoteLoadRepo(client: ApolloClient<any>, id: string) {
2323
lastname
2424
}
2525
public
26+
edges {
27+
source
28+
target
29+
}
2630
pods {
2731
id
2832
type
@@ -70,8 +74,10 @@ export async function doRemoteLoadRepo(client: ApolloClient<any>, id: string) {
7074
await client.refetchQueries({ include: ["GetRepos", "GetCollabRepos"] });
7175
// We need to do a deep copy here, because apollo client returned immutable objects.
7276
let pods = res.data.repo.pods.map((pod) => ({ ...pod }));
77+
let edges = res.data.repo.edges;
7378
return {
7479
pods,
80+
edges,
7581
name: res.data.repo.name,
7682
error: null,
7783
userId: res.data.repo.userId,
@@ -82,6 +88,7 @@ export async function doRemoteLoadRepo(client: ApolloClient<any>, id: string) {
8288
console.log(e);
8389
return {
8490
pods: [],
91+
edges: [],
8592
name: "",
8693
error: e,
8794
userId: null,

0 commit comments

Comments
 (0)