blob: 66c3c075bb0c2584f6124fb722cff525f1c460d1 [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;
153 groups: string[];
154 noAuthPathPatterns: string[];
155 };
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(),
451 })
452 .extend(NodeBaseDataSchema.shape),
453 })
454 .extend(NodeBaseSchema.shape);
455
456export const GatewayTCPNodeSchema = z
457 .object({
458 type: z.literal("gateway-tcp"),
459 data: z
460 .object({
461 readonly: z.boolean().optional(),
462 network: z.string().optional(),
463 subdomain: z.string().optional(),
464 exposed: z.array(
465 z.object({
466 serviceId: z.string(),
467 portId: z.string(),
468 }),
469 ),
470 selected: z
471 .object({
472 serviceId: z.string().optional(),
473 portId: z.string().optional(),
474 })
475 .optional(),
476 })
477 .extend(NodeBaseDataSchema.shape),
478 })
479 .extend(NodeBaseSchema.shape);
480
481export const ServiceNodeSchema = z
482 .object({
483 type: z.literal("app"),
484 data: z
485 .object({
486 type: ServiceTypeSchema,
487 repository: z
488 .union([
489 z.object({
490 id: z.number(),
491 repoNodeId: z.string(),
492 branch: z.string(),
493 rootDir: z.string(),
494 }),
495 z.object({
496 id: z.number(),
497 repoNodeId: z.string(),
498 branch: z.string(),
499 }),
500 z.object({
501 id: z.number(),
502 repoNodeId: z.string(),
503 }),
504 ])
505 .optional(),
506 volume: z.array(z.string()).optional(),
507 preBuildCommands: z.string().optional(),
508 isChoosingPortToConnect: z.boolean().optional(),
509 dev: z
gio43e0aad2025-08-01 16:17:27 +0400510 .union([
gio10ff1342025-07-05 10:22:15 +0000511 z.object({
512 enabled: z.literal(false),
513 expose: DomainSchema.optional(),
514 }),
515 z.object({
516 enabled: z.literal(true),
gio43e0aad2025-08-01 16:17:27 +0400517 mode: z.literal("VM"),
gio10ff1342025-07-05 10:22:15 +0000518 expose: DomainSchema.optional(),
519 codeServerNodeId: z.string(),
520 sshNodeId: z.string(),
521 }),
gio43e0aad2025-08-01 16:17:27 +0400522 z.object({
523 enabled: z.literal(true),
524 mode: z.literal("PROXY"),
525 address: z.string(),
526 }),
gio10ff1342025-07-05 10:22:15 +0000527 ])
528 .optional(),
529 model: z
530 .object({
531 name: z.enum(["gemini", "claude"]),
532 apiKey: z.string().optional(),
533 })
534 .optional(),
535 info: serviceAnalyzisSchema.optional(),
536
537 ports: z.array(
538 z.object({
539 id: z.string(),
540 name: z.string(),
541 value: z.number(),
542 }),
543 ),
544 activeField: z.string().optional(),
545 state: z.string().nullable().optional(),
546 })
547 .extend(NodeBaseDataSchema.shape),
548 })
549 .extend(NodeBaseSchema.shape);
550
551export const VolumeNodeSchema = z
552 .object({
553 type: z.literal("volume"),
554 data: z
555 .object({
556 size: z.string(),
557 type: VolumeTypeSchema,
558 attachedTo: z.array(z.string()),
559 })
560 .extend(NodeBaseDataSchema.shape),
561 })
562 .extend(NodeBaseSchema.shape);
563
564export const PostgreSQLNodeSchema = z
565 .object({
566 type: z.literal("postgresql"),
567 data: z.object({}).extend(NodeBaseDataSchema.shape),
568 })
569 .extend(NodeBaseSchema.shape);
570
571export const MongoDBNodeSchema = z
572 .object({
573 type: z.literal("mongodb"),
574 data: z.object({}).extend(NodeBaseDataSchema.shape),
575 })
576 .extend(NodeBaseSchema.shape);
577
578export const NodeSchema = z.discriminatedUnion("type", [
579 NetworkNodeSchema,
580 GithubNodeSchema,
581 GatewayHttpsNodeSchema,
582 GatewayTCPNodeSchema,
583 ServiceNodeSchema,
584 VolumeNodeSchema,
585 PostgreSQLNodeSchema,
586 MongoDBNodeSchema,
587]);
588
589export const EdgeSchema = z.object({
590 id: z.string(),
591 source: z.string(),
592 sourceHandle: z.string().optional(),
593 target: z.string(),
594 targetHandle: z.string().optional(),
595});
596
gio52441602025-07-06 18:22:56 +0000597export const ViewportTransformSchema = z.object({
598 transformX: z.number(),
599 transformY: z.number(),
600 transformZoom: z.number(),
601 width: z.number(),
602 height: z.number(),
603});
604
gio10ff1342025-07-05 10:22:15 +0000605export const GraphSchema = z.object({
606 nodes: z.array(NodeSchema),
607 edges: z.array(EdgeSchema),
608 viewport: z
609 .object({
610 x: z.number(),
611 y: z.number(),
612 zoom: z.number(),
613 })
614 .optional(),
gio52441602025-07-06 18:22:56 +0000615 viewportTransform: ViewportTransformSchema.optional(),
gio10ff1342025-07-05 10:22:15 +0000616});
617
618export const GraphOrConfigSchema = z.discriminatedUnion("type", [
619 z.object({
620 type: z.literal("graph"),
621 graph: GraphSchema,
622 }),
623 z.object({
624 type: z.literal("config"),
625 config: ConfigSchema,
626 }),
627]);
628
gio56e9f472025-07-07 03:33:38 +0000629export const GraphConfigOrDraft = z.discriminatedUnion("type", [
630 z.object({
631 type: z.literal("graph"),
632 graph: GraphSchema,
633 }),
634 z.object({
635 type: z.literal("config"),
636 config: ConfigSchema,
637 }),
638 z.object({
639 type: z.literal("draft"),
640 }),
641]);
642
gio10ff1342025-07-05 10:22:15 +0000643export type Edge = z.infer<typeof EdgeSchema>;
gio52441602025-07-06 18:22:56 +0000644export type ViewportTransform = z.infer<typeof ViewportTransformSchema>;
gio10ff1342025-07-05 10:22:15 +0000645export type Graph = z.infer<typeof GraphSchema>;