Canvas: Use GraphSchema to validate state
Change-Id: I342c8959c97f3486c4a7cb2aff92fb930a2b3146
diff --git a/apps/canvas/config/src/graph.ts b/apps/canvas/config/src/graph.ts
index c36dbcf..e309926 100644
--- a/apps/canvas/config/src/graph.ts
+++ b/apps/canvas/config/src/graph.ts
@@ -1,6 +1,14 @@
import { z } from "zod";
import { Node } from "@xyflow/react";
-import { Domain, ServiceType, VolumeType } from "./types.js";
+import {
+ ConfigSchema,
+ Domain,
+ DomainSchema,
+ ServiceType,
+ ServiceTypeSchema,
+ VolumeType,
+ VolumeTypeSchema,
+} from "./types.js";
export const serviceAnalyzisSchema = z.object({
name: z.string(),
@@ -37,6 +45,41 @@
),
});
+export const BoundEnvVarSchema = z.union([
+ z.object({
+ id: z.string(),
+ source: z.null(),
+ name: z.string(),
+ value: z.string(),
+ isEditting: z.boolean().optional(),
+ }),
+ z.object({
+ id: z.string(),
+ source: z.string().nullable(),
+ portId: z.string(),
+ name: z.string(),
+ alias: z.string(),
+ isEditting: z.boolean(),
+ }),
+ z.object({
+ id: z.string(),
+ source: z.string().nullable(),
+ name: z.string(),
+ alias: z.string(),
+ isEditting: z.boolean(),
+ }),
+ z.object({
+ id: z.string(),
+ source: z.string().nullable(),
+ name: z.string(),
+ isEditting: z.boolean(),
+ }),
+ z.object({
+ id: z.string(),
+ source: z.string().nullable(),
+ }),
+]);
+
export type BoundEnvVar =
| {
id: string;
@@ -131,11 +174,13 @@
type: "gateway-tcp";
};
-export type Port = {
- id: string;
- name: string;
- value: number;
-};
+export const PortSchema = z.object({
+ id: z.string(),
+ name: z.string(),
+ value: z.number(),
+});
+
+export type Port = z.infer<typeof PortSchema>;
export type ServiceData = NodeData & {
type: ServiceType;
@@ -155,10 +200,9 @@
branch: string;
rootDir: string;
};
- env: string[];
- volume: string[];
- preBuildCommands: string;
- isChoosingPortToConnect: boolean;
+ volume?: string[];
+ preBuildCommands?: string;
+ isChoosingPortToConnect?: boolean;
dev?:
| {
enabled: false;
@@ -191,17 +235,13 @@
type: "volume";
};
-export type PostgreSQLData = NodeData & {
- volumeId: string;
-};
+export type PostgreSQLData = NodeData & {};
export type PostgreSQLNode = Node<PostgreSQLData> & {
type: "postgresql";
};
-export type MongoDBData = NodeData & {
- volumeId: string;
-};
+export type MongoDBData = NodeData & {};
export type MongoDBNode = Node<MongoDBData> & {
type: "mongodb";
@@ -236,13 +276,13 @@
export type NodeType = Exclude<Pick<AppNode, "type">["type"], undefined>;
-export const networkSchema = z.object({
+export const NetworkSchema = z.object({
name: z.string().min(1),
domain: z.string().min(1),
hasAuth: z.boolean(),
});
-export type Network = z.infer<typeof networkSchema>;
+export type Network = z.infer<typeof NetworkSchema>;
export const accessSchema = z.discriminatedUnion("type", [
z.object({
@@ -340,3 +380,225 @@
export type Env = z.infer<typeof envSchema>;
export type Access = z.infer<typeof accessSchema>;
export type AgentAccess = Required<Extract<Access, { type: "https" }>>;
+
+const NodeBaseSchema = z.object({
+ id: z.string(),
+ position: z.object({
+ x: z.number(),
+ y: z.number(),
+ }),
+});
+
+export const NodeBaseDataSchema = z.object({
+ label: z.string(),
+ envVars: z.array(BoundEnvVarSchema),
+ ports: z.array(PortSchema),
+});
+
+export const NetworkNodeSchema = z
+ .object({
+ type: z.literal("network"),
+ data: z
+ .object({
+ domain: z.string(),
+ })
+ .extend(NodeBaseDataSchema.shape),
+ })
+ .extend(NodeBaseSchema.shape);
+
+export const GithubNodeSchema = z
+ .object({
+ type: z.literal("github"),
+ data: z
+ .object({
+ repository: z
+ .object({
+ id: z.number(),
+ sshURL: z.string(),
+ fullName: z.string(),
+ })
+ .optional(),
+ })
+ .extend(NodeBaseDataSchema.shape),
+ })
+ .extend(NodeBaseSchema.shape);
+
+export const GatewayHttpsNodeSchema = z
+ .object({
+ type: z.literal("gateway-https"),
+ data: z
+ .object({
+ readonly: z.boolean().optional(), // TODO: remove this
+ network: z.string().optional(),
+ subdomain: z.string().optional(),
+ https: z
+ .object({
+ serviceId: z.string(),
+ portId: z.string(),
+ })
+ .optional(),
+ })
+ .extend(NodeBaseDataSchema.shape),
+ })
+ .extend(NodeBaseSchema.shape);
+
+export const GatewayTCPNodeSchema = z
+ .object({
+ type: z.literal("gateway-tcp"),
+ data: z
+ .object({
+ readonly: z.boolean().optional(),
+ network: z.string().optional(),
+ subdomain: z.string().optional(),
+ exposed: z.array(
+ z.object({
+ serviceId: z.string(),
+ portId: z.string(),
+ }),
+ ),
+ selected: z
+ .object({
+ serviceId: z.string().optional(),
+ portId: z.string().optional(),
+ })
+ .optional(),
+ })
+ .extend(NodeBaseDataSchema.shape),
+ })
+ .extend(NodeBaseSchema.shape);
+
+export const ServiceNodeSchema = z
+ .object({
+ type: z.literal("app"),
+ data: z
+ .object({
+ type: ServiceTypeSchema,
+ repository: z
+ .union([
+ z.object({
+ id: z.number(),
+ repoNodeId: z.string(),
+ branch: z.string(),
+ rootDir: z.string(),
+ }),
+ z.object({
+ id: z.number(),
+ repoNodeId: z.string(),
+ branch: z.string(),
+ }),
+ z.object({
+ id: z.number(),
+ repoNodeId: z.string(),
+ }),
+ ])
+ .optional(),
+ volume: z.array(z.string()).optional(),
+ preBuildCommands: z.string().optional(),
+ isChoosingPortToConnect: z.boolean().optional(),
+ dev: z
+ .discriminatedUnion("enabled", [
+ z.object({
+ enabled: z.literal(false),
+ expose: DomainSchema.optional(),
+ }),
+ z.object({
+ enabled: z.literal(true),
+ expose: DomainSchema.optional(),
+ codeServerNodeId: z.string(),
+ sshNodeId: z.string(),
+ }),
+ ])
+ .optional(),
+ model: z
+ .object({
+ name: z.enum(["gemini", "claude"]),
+ apiKey: z.string().optional(),
+ })
+ .optional(),
+ info: serviceAnalyzisSchema.optional(),
+
+ ports: z.array(
+ z.object({
+ id: z.string(),
+ name: z.string(),
+ value: z.number(),
+ }),
+ ),
+ activeField: z.string().optional(),
+ state: z.string().nullable().optional(),
+ })
+ .extend(NodeBaseDataSchema.shape),
+ })
+ .extend(NodeBaseSchema.shape);
+
+export const VolumeNodeSchema = z
+ .object({
+ type: z.literal("volume"),
+ data: z
+ .object({
+ size: z.string(),
+ type: VolumeTypeSchema,
+ attachedTo: z.array(z.string()),
+ })
+ .extend(NodeBaseDataSchema.shape),
+ })
+ .extend(NodeBaseSchema.shape);
+
+export const PostgreSQLNodeSchema = z
+ .object({
+ type: z.literal("postgresql"),
+ data: z.object({}).extend(NodeBaseDataSchema.shape),
+ })
+ .extend(NodeBaseSchema.shape);
+
+export const MongoDBNodeSchema = z
+ .object({
+ type: z.literal("mongodb"),
+ data: z.object({}).extend(NodeBaseDataSchema.shape),
+ })
+ .extend(NodeBaseSchema.shape);
+
+export const NodeSchema = z.discriminatedUnion("type", [
+ NetworkNodeSchema,
+ GithubNodeSchema,
+ GatewayHttpsNodeSchema,
+ GatewayTCPNodeSchema,
+ ServiceNodeSchema,
+ VolumeNodeSchema,
+ PostgreSQLNodeSchema,
+ MongoDBNodeSchema,
+]);
+
+export const EdgeSchema = z.object({
+ id: z.string(),
+ source: z.string(),
+ sourceHandle: z.string().optional(),
+ target: z.string(),
+ targetHandle: z.string().optional(),
+});
+
+export const GraphSchema = z.object({
+ nodes: z.array(NodeSchema),
+ edges: z.array(EdgeSchema),
+ viewport: z
+ .object({
+ x: z.number(),
+ y: z.number(),
+ zoom: z.number(),
+ })
+ .optional(),
+});
+
+export const GraphOrConfigSchema = z.discriminatedUnion("type", [
+ z.object({
+ type: z.literal("graph"),
+ graph: GraphSchema,
+ }),
+ z.object({
+ type: z.literal("config"),
+ config: ConfigSchema,
+ }),
+]);
+
+export type Edge = z.infer<typeof EdgeSchema>;
+export type Graph = z.infer<typeof GraphSchema>;