Canvas: Add option to deploy latest draft

Change-Id: Ia74c64354e1f80ec794140ce406fb21c16feb0da
diff --git a/apps/canvas/back/src/index.ts b/apps/canvas/back/src/index.ts
index 9456d17..fb77431 100644
--- a/apps/canvas/back/src/index.ts
+++ b/apps/canvas/back/src/index.ts
@@ -16,6 +16,7 @@
 	Env,
 	generateDodoConfig,
 	ConfigSchema,
+	Config,
 	ConfigWithInput,
 	configToGraph,
 	Network,
@@ -24,7 +25,7 @@
 } from "config";
 import { Instant, DateTimeFormatter, ZoneId } from "@js-joda/core";
 import LogStore from "./log.js";
-import { GraphOrConfigSchema, GraphSchema } from "config/dist/graph.js";
+import { GraphOrConfigSchema, GraphSchema, GraphConfigOrDraft } from "config/dist/graph.js";
 
 async function generateKey(root: string): Promise<[string, string]> {
 	const privKeyPath = path.join(root, "key");
@@ -167,10 +168,6 @@
 	};
 }
 
-const projectDeleteReqSchema = z.object({
-	state: z.optional(z.nullable(z.string())),
-});
-
 const handleProjectDelete: express.Handler = async (req, resp) => {
 	try {
 		const projectId = Number(req.params["projectId"]);
@@ -191,20 +188,8 @@
 			resp.status(404);
 			return;
 		}
-		const parseResult = projectDeleteReqSchema.safeParse(req.body);
-		if (!parseResult.success) {
-			resp.status(400);
-			resp.write(JSON.stringify({ error: "Invalid request body", issues: parseResult.error.format() }));
-			return;
-		}
 		if (p.githubToken && p.deployKeyPublic) {
-			const allRepos = [
-				...new Set([
-					...extractGithubRepos(p.state),
-					...extractGithubRepos(p.draft),
-					...extractGithubRepos(parseResult.data.state),
-				]),
-			];
+			const allRepos = [...new Set([...extractGithubRepos(p.state), ...extractGithubRepos(p.draft)])];
 			if (allRepos.length > 0) {
 				const diff: RepoDiff = { toDelete: allRepos, toAdd: [] };
 				const github = new GithubClient(p.githubToken);
@@ -300,6 +285,12 @@
 
 const handleDeploy: express.Handler = async (req, resp) => {
 	try {
+		const reqParsed = GraphConfigOrDraft.safeParse(req.body);
+		if (!reqParsed.success) {
+			resp.status(400);
+			resp.write(JSON.stringify({ error: "Invalid request body", issues: reqParsed.error.format() }));
+			return;
+		}
 		const projectId = Number(req.params["projectId"]);
 		const p = await db.project.findUnique({
 			where: {
@@ -312,6 +303,7 @@
 				deployKey: true,
 				deployKeyPublic: true,
 				state: true,
+				draft: true,
 				geminiApiKey: true,
 				anthropicApiKey: true,
 			},
@@ -320,33 +312,54 @@
 			resp.status(404);
 			return;
 		}
-		const config = ConfigSchema.safeParse(req.body.config);
-		if (!config.success) {
-			resp.status(400);
-			resp.write(JSON.stringify({ error: "Invalid configuration", issues: config.error.format() }));
+		let graph: Graph | null = null;
+		let config: Config | null = null;
+		if (reqParsed.data.type === "config") {
+			const parsed = ConfigSchema.safeParse(reqParsed.data.config);
+			if (parsed.success) {
+				config = parsed.data;
+			} else {
+				resp.status(400);
+				resp.write(JSON.stringify({ error: "Invalid configuration", issues: parsed.error.format() }));
+				return;
+			}
+			let oldGraph: Graph | undefined = undefined;
+			if (p.state != null) {
+				oldGraph = parseGraph(p.state)!.data;
+			} else if (p.draft != null) {
+				oldGraph = parseGraph(p.draft)!.data;
+			}
+			let repos: GithubRepository[] = [];
+			if (p.githubToken) {
+				const github = new GithubClient(p.githubToken);
+				repos = await github.getRepositories();
+			}
+			graph = configToGraph(config, getNetworks(resp.locals.username), repos, oldGraph);
+		} else if (reqParsed.data.type === "graph") {
+			graph = reqParsed.data.graph;
+			const env = await getEnv(projectId, resp.locals.userId, resp.locals.username);
+			config = generateDodoConfig(projectId.toString(), graph?.nodes || [], env);
+		} else if (reqParsed.data.type === "draft") {
+			if (p.draft == null) {
+				resp.status(400);
+				resp.write(JSON.stringify({ error: "Invalid request body" }));
+				return;
+			}
+			graph = parseGraph(p.draft)!.data!;
+			const env = await getEnv(projectId, resp.locals.userId, resp.locals.username);
+			config = generateDodoConfig(projectId.toString(), graph?.nodes || [], env);
+		}
+		if (config == null || graph == null) {
+			resp.status(500);
+			resp.write(JSON.stringify({ error: "Failed to generate configuration" }));
 			return;
 		}
-		let repos: GithubRepository[] = [];
-		if (p.githubToken) {
-			const github = new GithubClient(p.githubToken);
-			repos = await github.getRepositories();
-		}
-		const state = req.body.state
-			? JSON.stringify(req.body.state)
-			: JSON.stringify(
-					configToGraph(
-						config.data,
-						getNetworks(resp.locals.username),
-						repos,
-						p.state ? parseGraph(p.state)!.data! : undefined,
-					),
-				);
 		await db.project.update({
 			where: {
 				id: projectId,
 			},
 			data: {
-				draft: state,
+				draft: JSON.stringify(graph),
 			},
 		});
 		let deployKey: string | null = p.deployKey;
@@ -360,7 +373,7 @@
 		}
 		let diff: RepoDiff | null = null;
 		const cfg: ConfigWithInput = {
-			...config.data,
+			...config,
 			input: {
 				appId: projectId.toString(),
 				managerAddr: env.INTERNAL_API_ADDR!,
@@ -380,22 +393,22 @@
 						id: projectId,
 					},
 					data: {
-						state,
+						state: JSON.stringify(graph),
 						draft: null,
 						instanceId: deployResponse.id,
 						access: JSON.stringify(deployResponse.access),
 					},
 				});
-				diff = { toAdd: extractGithubRepos(state) };
+				diff = { toAdd: extractGithubRepos(JSON.stringify(graph)) };
 			} else {
 				const deployResponse = await appManager.update(p.instanceId, cfg);
-				diff = calculateRepoDiff(extractGithubRepos(p.state), extractGithubRepos(state));
+				diff = calculateRepoDiff(extractGithubRepos(p.state), extractGithubRepos(JSON.stringify(graph)));
 				await db.project.update({
 					where: {
 						id: projectId,
 					},
 					data: {
-						state,
+						state: JSON.stringify(graph),
 						draft: null,
 						access: JSON.stringify(deployResponse.access),
 					},