blob: ef868da3e58cb94ed97db8a09c479b91b7a77c6e [file] [log] [blame]
gioc31bf142025-06-16 07:48:20 +00001import { z } from "zod";
2import { Node } from "@xyflow/react";
gio10ff1342025-07-05 10:22:15 +00003import {
4 ConfigSchema,
5 Domain,
6 DomainSchema,
7 ServiceType,
8 ServiceTypeSchema,
9 VolumeType,
10 VolumeTypeSchema,
11} from "./types.js";
gioc31bf142025-06-16 07:48:20 +000012
13export const serviceAnalyzisSchema = z.object({
14 name: z.string(),
15 location: z.string(),
16 configVars: z.array(
17 z.object({
18 name: z.string(),
19 category: z.enum(["CommandLineFlag", "EnvironmentVariable"]),
20 type: z.optional(z.enum(["String", "Number", "Boolean"])),
21 semanticType: z.optional(
22 z.enum([
23 "EXPANDED_ENV_VAR",
24 "PORT",
25 "FILESYSTEM_PATH",
26 "DATABASE_URL",
27 "SQLITE_PATH",
28 "POSTGRES_URL",
29 "POSTGRES_PASSWORD",
30 "POSTGRES_USER",
31 "POSTGRES_DB",
32 "POSTGRES_PORT",
33 "POSTGRES_HOST",
34 "POSTGRES_SSL",
35 "MONGO_URL",
36 "MONGO_PASSWORD",
37 "MONGO_USER",
38 "MONGO_DB",
39 "MONGO_PORT",
40 "MONGO_HOST",
41 "MONGO_SSL",
42 ]),
43 ),
44 }),
45 ),
46});
47
gio10ff1342025-07-05 10:22:15 +000048export const BoundEnvVarSchema = z.union([
49 z.object({
50 id: z.string(),
51 source: z.null(),
52 name: z.string(),
53 value: z.string(),
54 isEditting: z.boolean().optional(),
55 }),
56 z.object({
57 id: z.string(),
58 source: z.string().nullable(),
59 portId: z.string(),
60 name: z.string(),
61 alias: z.string(),
62 isEditting: z.boolean(),
63 }),
64 z.object({
65 id: z.string(),
66 source: z.string().nullable(),
67 name: z.string(),
68 alias: z.string(),
69 isEditting: z.boolean(),
70 }),
71 z.object({
72 id: z.string(),
73 source: z.string().nullable(),
74 name: z.string(),
75 isEditting: z.boolean(),
76 }),
77 z.object({
78 id: z.string(),
79 source: z.string().nullable(),
80 }),
81]);
82
gioc31bf142025-06-16 07:48:20 +000083export type BoundEnvVar =
84 | {
85 id: string;
86 source: string | null;
87 }
88 | {
89 id: string;
90 source: string | null;
91 name: string;
92 isEditting: boolean;
93 }
94 | {
95 id: string;
96 source: string | null;
97 name: string;
98 alias: string;
99 isEditting: boolean;
100 }
101 | {
102 id: string;
103 source: string | null;
104 portId: string;
105 name: string;
106 alias: string;
107 isEditting: boolean;
gio1dacf1c2025-07-03 16:39:04 +0000108 }
109 | {
110 id: string;
111 source: null;
112 name: string;
113 value: string;
114 isEditting?: boolean;
gioc31bf142025-06-16 07:48:20 +0000115 };
116
117export type EnvVar = {
118 name: string;
119 value: string;
120};
121
122export type InitData = {
123 label: string;
124 envVars: BoundEnvVar[];
125 ports: Port[];
126};
127
128export type NodeData = InitData & {
129 activeField?: string | undefined;
130 state?: string | null;
131};
132
133export type PortConnectedTo = {
134 serviceId: string;
135 portId: string;
136};
137
138export type NetworkData = NodeData & {
139 domain: string;
140};
141
142export type NetworkNode = Node<NetworkData> & {
143 type: "network";
144};
145
146export type GatewayHttpsData = NodeData & {
147 readonly?: boolean;
148 network?: string;
149 subdomain?: string;
150 https?: PortConnectedTo;
151 auth?: {
152 enabled: boolean;
giodeaf3262025-08-04 11:12:41 +0000153 groups?: string[];
154 noAuthPathPatterns?: string[];
gioc31bf142025-06-16 07:48:20 +0000155 };
156};
157
158export type GatewayHttpsNode = Node<GatewayHttpsData> & {
159 type: "gateway-https";
160};
161
162export type GatewayTCPData = NodeData & {
163 readonly?: boolean;
164 network?: string;
165 subdomain?: string;
166 exposed: PortConnectedTo[];
167 selected?: {
168 serviceId?: string;
169 portId?: string;
170 };
171};
172
173export type GatewayTCPNode = Node<GatewayTCPData> & {
174 type: "gateway-tcp";
175};
176
gio10ff1342025-07-05 10:22:15 +0000177export const PortSchema = z.object({
178 id: z.string(),
179 name: z.string(),
180 value: z.number(),
181});
182
183export type Port = z.infer<typeof PortSchema>;
gioc31bf142025-06-16 07:48:20 +0000184
185export type ServiceData = NodeData & {
186 type: ServiceType;
187 repository?:
188 | {
189 id: number;
190 repoNodeId: string;
191 }
192 | {
193 id: number;
194 repoNodeId: string;
195 branch: string;
196 }
197 | {
198 id: number;
199 repoNodeId: string;
200 branch: string;
201 rootDir: string;
202 };
gio10ff1342025-07-05 10:22:15 +0000203 volume?: string[];
204 preBuildCommands?: string;
205 isChoosingPortToConnect?: boolean;
gioc31bf142025-06-16 07:48:20 +0000206 dev?:
207 | {
208 enabled: false;
209 expose?: Domain;
210 }
211 | {
212 enabled: true;
gio43e0aad2025-08-01 16:17:27 +0400213 mode: "VM";
gioc31bf142025-06-16 07:48:20 +0000214 expose?: Domain;
215 codeServerNodeId: string;
216 sshNodeId: string;
gio43e0aad2025-08-01 16:17:27 +0400217 }
218 | {
219 enabled: true;
220 mode: "PROXY";
221 address: string;
gioc31bf142025-06-16 07:48:20 +0000222 };
gio69ff7592025-07-03 06:27:21 +0000223 model?: {
224 name: "gemini" | "claude";
225 apiKey?: string;
gio69148322025-06-19 23:16:12 +0400226 };
gioc31bf142025-06-16 07:48:20 +0000227 info?: z.infer<typeof serviceAnalyzisSchema>;
228};
229
230export type ServiceNode = Node<ServiceData> & {
231 type: "app";
232};
233
234export type VolumeData = NodeData & {
235 type: VolumeType;
236 size: string;
237 attachedTo: string[];
238};
239
240export type VolumeNode = Node<VolumeData> & {
241 type: "volume";
242};
243
gio10ff1342025-07-05 10:22:15 +0000244export type PostgreSQLData = NodeData & {};
gioc31bf142025-06-16 07:48:20 +0000245
246export type PostgreSQLNode = Node<PostgreSQLData> & {
247 type: "postgresql";
248};
249
gio10ff1342025-07-05 10:22:15 +0000250export type MongoDBData = NodeData & {};
gioc31bf142025-06-16 07:48:20 +0000251
252export type MongoDBNode = Node<MongoDBData> & {
253 type: "mongodb";
254};
255
256export type GithubData = NodeData & {
257 repository?: {
258 id: number;
259 sshURL: string;
260 fullName: string;
261 };
262};
263
264export type GithubNode = Node<GithubData> & {
265 type: "github";
266};
267
268export type NANode = Node<NodeData> & {
269 type: undefined;
270};
271
272export type AppNode =
273 | NetworkNode
274 | GatewayHttpsNode
275 | GatewayTCPNode
276 | ServiceNode
277 | VolumeNode
278 | PostgreSQLNode
279 | MongoDBNode
280 | GithubNode
281 | NANode;
282
283export type NodeType = Exclude<Pick<AppNode, "type">["type"], undefined>;
284
gio10ff1342025-07-05 10:22:15 +0000285export const NetworkSchema = z.object({
gioc31bf142025-06-16 07:48:20 +0000286 name: z.string().min(1),
287 domain: z.string().min(1),
288 hasAuth: z.boolean(),
289});
290
gio10ff1342025-07-05 10:22:15 +0000291export type Network = z.infer<typeof NetworkSchema>;
gioc31bf142025-06-16 07:48:20 +0000292
293export const accessSchema = z.discriminatedUnion("type", [
294 z.object({
295 type: z.literal("https"),
296 name: z.string(),
297 address: z.string(),
giocc5ce582025-06-25 07:45:21 +0400298 agentName: z.string().optional(),
gioc31bf142025-06-16 07:48:20 +0000299 }),
300 z.object({
301 type: z.literal("ssh"),
302 name: z.string(),
303 host: z.string(),
304 port: z.number(),
305 }),
306 z.object({
307 type: z.literal("tcp"),
308 name: z.string(),
309 host: z.string(),
310 port: z.number(),
311 }),
312 z.object({
313 type: z.literal("udp"),
314 name: z.string(),
315 host: z.string(),
316 port: z.number(),
317 }),
318 z.object({
319 type: z.literal("postgresql"),
320 name: z.string(),
321 host: z.string(),
322 port: z.number(),
323 database: z.string(),
324 username: z.string(),
325 password: z.string(),
326 }),
327 z.object({
328 type: z.literal("mongodb"),
329 name: z.string(),
330 host: z.string(),
331 port: z.number(),
332 database: z.string(),
333 username: z.string(),
334 password: z.string(),
335 }),
gio8323a052025-08-04 06:12:26 +0000336 z.object({
337 type: z.literal("env_var"),
338 name: z.string(),
339 var: z.string(),
340 }),
gioc31bf142025-06-16 07:48:20 +0000341]);
342
343export const serviceInfoSchema = z.object({
344 name: z.string(),
345 workers: z.array(
346 z.object({
347 id: z.string(),
gioa70535a2025-07-02 15:50:25 +0000348 commit: z
349 .object({
gioc31bf142025-06-16 07:48:20 +0000350 hash: z.string(),
351 message: z.string(),
gioa70535a2025-07-02 15:50:25 +0000352 })
353 .nullable()
354 .optional(),
355 commands: z
356 .array(
gioc31bf142025-06-16 07:48:20 +0000357 z.object({
358 command: z.string(),
359 state: z.string(),
360 }),
gioa70535a2025-07-02 15:50:25 +0000361 )
362 .optional(),
gioc31bf142025-06-16 07:48:20 +0000363 }),
364 ),
365});
366
367export const envSchema = z.object({
gio69ff7592025-07-03 06:27:21 +0000368 deployKeyPublic: z.string().optional(),
369 instanceId: z.string().optional(),
370 networks: z.array(
371 z.object({
372 name: z.string(),
373 domain: z.string(),
374 hasAuth: z.boolean(),
375 }),
376 ),
gioc31bf142025-06-16 07:48:20 +0000377 integrations: z.object({
378 github: z.boolean(),
gio69148322025-06-19 23:16:12 +0400379 gemini: z.boolean(),
gio69ff7592025-07-03 06:27:21 +0000380 anthropic: z.boolean(),
gioc31bf142025-06-16 07:48:20 +0000381 }),
382 services: z.array(serviceInfoSchema),
383 user: z.object({
384 id: z.string(),
385 username: z.string(),
386 }),
387 access: z.array(accessSchema),
388});
389
390export type ServiceInfo = z.infer<typeof serviceInfoSchema>;
391export type Env = z.infer<typeof envSchema>;
giocc5ce582025-06-25 07:45:21 +0400392export type Access = z.infer<typeof accessSchema>;
gio74c6f752025-07-05 04:10:58 +0000393export type AgentAccess = Required<Extract<Access, { type: "https" }>>;
gio10ff1342025-07-05 10:22:15 +0000394
395const NodeBaseSchema = z.object({
396 id: z.string(),
397 position: z.object({
398 x: z.number(),
399 y: z.number(),
400 }),
401});
402
403export const NodeBaseDataSchema = z.object({
404 label: z.string(),
405 envVars: z.array(BoundEnvVarSchema),
406 ports: z.array(PortSchema),
407});
408
409export const NetworkNodeSchema = z
410 .object({
411 type: z.literal("network"),
412 data: z
413 .object({
414 domain: z.string(),
415 })
416 .extend(NodeBaseDataSchema.shape),
417 })
418 .extend(NodeBaseSchema.shape);
419
420export const GithubNodeSchema = z
421 .object({
422 type: z.literal("github"),
423 data: z
424 .object({
425 repository: z
426 .object({
427 id: z.number(),
428 sshURL: z.string(),
429 fullName: z.string(),
430 })
431 .optional(),
432 })
433 .extend(NodeBaseDataSchema.shape),
434 })
435 .extend(NodeBaseSchema.shape);
436
437export const GatewayHttpsNodeSchema = z
438 .object({
439 type: z.literal("gateway-https"),
440 data: z
441 .object({
442 readonly: z.boolean().optional(), // TODO: remove this
443 network: z.string().optional(),
444 subdomain: z.string().optional(),
445 https: z
446 .object({
447 serviceId: z.string(),
448 portId: z.string(),
449 })
450 .optional(),
giodeaf3262025-08-04 11:12:41 +0000451 auth: z
452 .object({
453 enabled: z.boolean(),
454 groups: z.array(z.string()).optional(),
455 noAuthPathPatterns: z.array(z.string()).optional(),
456 })
457 .optional(),
gio10ff1342025-07-05 10:22:15 +0000458 })
459 .extend(NodeBaseDataSchema.shape),
460 })
461 .extend(NodeBaseSchema.shape);
462
463export const GatewayTCPNodeSchema = z
464 .object({
465 type: z.literal("gateway-tcp"),
466 data: z
467 .object({
468 readonly: z.boolean().optional(),
469 network: z.string().optional(),
470 subdomain: z.string().optional(),
471 exposed: z.array(
472 z.object({
473 serviceId: z.string(),
474 portId: z.string(),
475 }),
476 ),
477 selected: z
478 .object({
479 serviceId: z.string().optional(),
480 portId: z.string().optional(),
481 })
482 .optional(),
483 })
484 .extend(NodeBaseDataSchema.shape),
485 })
486 .extend(NodeBaseSchema.shape);
487
488export const ServiceNodeSchema = z
489 .object({
490 type: z.literal("app"),
491 data: z
492 .object({
493 type: ServiceTypeSchema,
494 repository: z
495 .union([
496 z.object({
497 id: z.number(),
498 repoNodeId: z.string(),
499 branch: z.string(),
500 rootDir: z.string(),
501 }),
502 z.object({
503 id: z.number(),
504 repoNodeId: z.string(),
505 branch: z.string(),
506 }),
507 z.object({
508 id: z.number(),
509 repoNodeId: z.string(),
510 }),
511 ])
512 .optional(),
513 volume: z.array(z.string()).optional(),
514 preBuildCommands: z.string().optional(),
515 isChoosingPortToConnect: z.boolean().optional(),
516 dev: z
gio43e0aad2025-08-01 16:17:27 +0400517 .union([
gio10ff1342025-07-05 10:22:15 +0000518 z.object({
519 enabled: z.literal(false),
520 expose: DomainSchema.optional(),
521 }),
522 z.object({
523 enabled: z.literal(true),
gio43e0aad2025-08-01 16:17:27 +0400524 mode: z.literal("VM"),
gio10ff1342025-07-05 10:22:15 +0000525 expose: DomainSchema.optional(),
526 codeServerNodeId: z.string(),
527 sshNodeId: z.string(),
528 }),
gio43e0aad2025-08-01 16:17:27 +0400529 z.object({
530 enabled: z.literal(true),
531 mode: z.literal("PROXY"),
532 address: z.string(),
533 }),
gio10ff1342025-07-05 10:22:15 +0000534 ])
535 .optional(),
536 model: z
537 .object({
538 name: z.enum(["gemini", "claude"]),
539 apiKey: z.string().optional(),
540 })
541 .optional(),
542 info: serviceAnalyzisSchema.optional(),
543
544 ports: z.array(
545 z.object({
546 id: z.string(),
547 name: z.string(),
548 value: z.number(),
549 }),
550 ),
551 activeField: z.string().optional(),
552 state: z.string().nullable().optional(),
553 })
554 .extend(NodeBaseDataSchema.shape),
555 })
556 .extend(NodeBaseSchema.shape);
557
558export const VolumeNodeSchema = z
559 .object({
560 type: z.literal("volume"),
561 data: z
562 .object({
563 size: z.string(),
564 type: VolumeTypeSchema,
565 attachedTo: z.array(z.string()),
566 })
567 .extend(NodeBaseDataSchema.shape),
568 })
569 .extend(NodeBaseSchema.shape);
570
571export const PostgreSQLNodeSchema = z
572 .object({
573 type: z.literal("postgresql"),
574 data: z.object({}).extend(NodeBaseDataSchema.shape),
575 })
576 .extend(NodeBaseSchema.shape);
577
578export const MongoDBNodeSchema = z
579 .object({
580 type: z.literal("mongodb"),
581 data: z.object({}).extend(NodeBaseDataSchema.shape),
582 })
583 .extend(NodeBaseSchema.shape);
584
585export const NodeSchema = z.discriminatedUnion("type", [
586 NetworkNodeSchema,
587 GithubNodeSchema,
588 GatewayHttpsNodeSchema,
589 GatewayTCPNodeSchema,
590 ServiceNodeSchema,
591 VolumeNodeSchema,
592 PostgreSQLNodeSchema,
593 MongoDBNodeSchema,
594]);
595
596export const EdgeSchema = z.object({
597 id: z.string(),
598 source: z.string(),
599 sourceHandle: z.string().optional(),
600 target: z.string(),
601 targetHandle: z.string().optional(),
602});
603
gio52441602025-07-06 18:22:56 +0000604export const ViewportTransformSchema = z.object({
605 transformX: z.number(),
606 transformY: z.number(),
607 transformZoom: z.number(),
608 width: z.number(),
609 height: z.number(),
610});
611
gio10ff1342025-07-05 10:22:15 +0000612export const GraphSchema = z.object({
613 nodes: z.array(NodeSchema),
614 edges: z.array(EdgeSchema),
615 viewport: z
616 .object({
617 x: z.number(),
618 y: z.number(),
619 zoom: z.number(),
620 })
621 .optional(),
gio52441602025-07-06 18:22:56 +0000622 viewportTransform: ViewportTransformSchema.optional(),
gio10ff1342025-07-05 10:22:15 +0000623});
624
625export const GraphOrConfigSchema = z.discriminatedUnion("type", [
626 z.object({
627 type: z.literal("graph"),
628 graph: GraphSchema,
629 }),
630 z.object({
631 type: z.literal("config"),
632 config: ConfigSchema,
633 }),
634]);
635
gio56e9f472025-07-07 03:33:38 +0000636export const GraphConfigOrDraft = z.discriminatedUnion("type", [
637 z.object({
638 type: z.literal("graph"),
639 graph: GraphSchema,
640 }),
641 z.object({
642 type: z.literal("config"),
643 config: ConfigSchema,
644 }),
645 z.object({
646 type: z.literal("draft"),
647 }),
648]);
649
gio10ff1342025-07-05 10:22:15 +0000650export type Edge = z.infer<typeof EdgeSchema>;
gio52441602025-07-06 18:22:56 +0000651export type ViewportTransform = z.infer<typeof ViewportTransformSchema>;
gio10ff1342025-07-05 10:22:15 +0000652export type Graph = z.infer<typeof GraphSchema>;