Canvas: Use GraphSchema to validate state

Change-Id: I342c8959c97f3486c4a7cb2aff92fb930a2b3146
diff --git a/apps/canvas/back/src/index.ts b/apps/canvas/back/src/index.ts
index 38122f8..9456d17 100644
--- a/apps/canvas/back/src/index.ts
+++ b/apps/canvas/back/src/index.ts
@@ -24,6 +24,7 @@
 } from "config";
 import { Instant, DateTimeFormatter, ZoneId } from "@js-joda/core";
 import LogStore from "./log.js";
+import { GraphOrConfigSchema, GraphSchema } from "config/dist/graph.js";
 
 async function generateKey(root: string): Promise<[string, string]> {
 	const privKeyPath = path.join(root, "key");
@@ -42,6 +43,13 @@
 
 const projectMonitors = new Map<number, ProjectMonitor>();
 
+function parseGraph(data: string | null | undefined) {
+	if (data == null) {
+		return null;
+	}
+	return GraphSchema.safeParse(JSON.parse(data));
+}
+
 const handleProjectCreate: express.Handler = async (req, resp) => {
 	try {
 		const tmpDir = tmp.dirSync().name;
@@ -94,26 +102,6 @@
 	}
 };
 
-const handleSave: express.Handler = async (req, resp) => {
-	try {
-		await db.project.update({
-			where: {
-				id: Number(req.params["projectId"]),
-				userId: resp.locals.userId,
-			},
-			data: {
-				draft: JSON.stringify(req.body),
-			},
-		});
-		resp.status(200);
-	} catch (e) {
-		console.log(e);
-		resp.status(500);
-	} finally {
-		resp.end();
-	}
-};
-
 async function getState(projectId: number, userId: string, state: "deploy" | "draft"): Promise<Graph | null> {
 	const r = await db.project.findUnique({
 		where: {
@@ -131,7 +119,7 @@
 	let currentState: Graph | null = null;
 	if (state === "deploy") {
 		if (r.state != null) {
-			currentState = JSON.parse(Buffer.from(r.state).toString("utf8"));
+			currentState = parseGraph(r.state)!.data!;
 		}
 	} else {
 		if (r.draft == null) {
@@ -142,10 +130,10 @@
 					viewport: { x: 0, y: 0, zoom: 1 },
 				};
 			} else {
-				currentState = JSON.parse(Buffer.from(r.state).toString("utf8"));
+				currentState = parseGraph(r.state)!.data!;
 			}
 		} else {
-			currentState = JSON.parse(Buffer.from(r.draft).toString("utf8"));
+			currentState = parseGraph(r.draft)!.data!;
 		}
 	}
 	return currentState;
@@ -350,7 +338,7 @@
 						config.data,
 						getNetworks(resp.locals.username),
 						repos,
-						p.state ? JSON.parse(Buffer.from(p.state).toString("utf8")) : null,
+						p.state ? parseGraph(p.state)!.data! : undefined,
 					),
 				);
 		await db.project.update({
@@ -431,13 +419,13 @@
 	}
 };
 
-const handleSaveFromConfig: express.Handler = async (req, resp) => {
+const handleSave: express.Handler = async (req, resp) => {
 	try {
 		const projectId = Number(req.params["projectId"]);
 		const p = await db.project.findUnique({
 			where: {
 				id: projectId,
-				// userId: resp.locals.userId, TODO(gio): validate
+				userId: resp.locals.userId,
 			},
 			select: {
 				instanceId: true,
@@ -453,10 +441,18 @@
 			resp.status(404);
 			return;
 		}
-		const config = ConfigSchema.safeParse(req.body.config);
-		if (!config.success) {
+		const gc = GraphOrConfigSchema.safeParse(req.body);
+		if (!gc.success) {
 			resp.status(400);
-			resp.write(JSON.stringify({ error: "Invalid configuration", issues: config.error.format() }));
+			resp.write(JSON.stringify({ error: "Invalid configuration", issues: gc.error.format() }));
+			return;
+		}
+		if (gc.data.type === "graph") {
+			await db.project.update({
+				where: { id: projectId },
+				data: { draft: JSON.stringify(gc.data.graph) },
+			});
+			resp.status(200);
 			return;
 		}
 		let repos: GithubRepository[] = [];
@@ -466,10 +462,10 @@
 		}
 		const state = JSON.stringify(
 			configToGraph(
-				config.data,
+				gc.data.config,
 				getNetworks(resp.locals.username),
 				repos,
-				p.state ? JSON.parse(Buffer.from(p.state).toString("utf8")) : null,
+				p.state ? parseGraph(p.state)!.data! : undefined,
 			),
 		);
 		await db.project.update({
@@ -538,7 +534,7 @@
 			return;
 		}
 
-		const state = JSON.parse(Buffer.from(project.state).toString("utf8"));
+		const state = parseGraph(project.state)!.data!;
 		const env = await getEnv(projectId, resp.locals.userId, resp.locals.username);
 		const config = generateDodoConfig(projectId.toString(), state.nodes, env);
 
@@ -1204,7 +1200,6 @@
 	const projectRouter = express.Router();
 	projectRouter.use(auth);
 	projectRouter.post("/:projectId/analyze", handleAnalyzeRepo);
-	projectRouter.post("/:projectId/saved/config", handleSaveFromConfig);
 	projectRouter.post("/:projectId/saved", handleSave);
 	projectRouter.get("/:projectId/state/stream/deploy", handleStateGetStream("deploy"));
 	projectRouter.get("/:projectId/state/stream/draft", handleStateGetStream("draft"));
@@ -1235,6 +1230,7 @@
 	internalApi.use(express.json());
 	internalApi.post("/api/project/:projectId/workers", handleRegisterWorker);
 	internalApi.get("/api/project/:projectId/config", handleConfigGet);
+	internalApi.post("/api/project/:projectId/saved", handleSave);
 	internalApi.post("/api/project/:projectId/deploy", handleDeploy);
 	internalApi.post("/api/validate-config", handleValidateConfig);