blob: fc3258e0d6b424006df87acf7fdaf11d2ec95f2d [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 }),
336]);
337
338export const serviceInfoSchema = z.object({
339 name: z.string(),
340 workers: z.array(
341 z.object({
342 id: z.string(),
gioa70535a2025-07-02 15:50:25 +0000343 commit: z
344 .object({
gioc31bf142025-06-16 07:48:20 +0000345 hash: z.string(),
346 message: z.string(),
gioa70535a2025-07-02 15:50:25 +0000347 })
348 .nullable()
349 .optional(),
350 commands: z
351 .array(
gioc31bf142025-06-16 07:48:20 +0000352 z.object({
353 command: z.string(),
354 state: z.string(),
355 }),
gioa70535a2025-07-02 15:50:25 +0000356 )
357 .optional(),
gioc31bf142025-06-16 07:48:20 +0000358 }),
359 ),
360});
361
362export const envSchema = z.object({
gio69ff7592025-07-03 06:27:21 +0000363 deployKeyPublic: z.string().optional(),
364 instanceId: z.string().optional(),
365 networks: z.array(
366 z.object({
367 name: z.string(),
368 domain: z.string(),
369 hasAuth: z.boolean(),
370 }),
371 ),
gioc31bf142025-06-16 07:48:20 +0000372 integrations: z.object({
373 github: z.boolean(),
gio69148322025-06-19 23:16:12 +0400374 gemini: z.boolean(),
gio69ff7592025-07-03 06:27:21 +0000375 anthropic: z.boolean(),
gioc31bf142025-06-16 07:48:20 +0000376 }),
377 services: z.array(serviceInfoSchema),
378 user: z.object({
379 id: z.string(),
380 username: z.string(),
381 }),
382 access: z.array(accessSchema),
383});
384
385export type ServiceInfo = z.infer<typeof serviceInfoSchema>;
386export type Env = z.infer<typeof envSchema>;
giocc5ce582025-06-25 07:45:21 +0400387export type Access = z.infer<typeof accessSchema>;
gio74c6f752025-07-05 04:10:58 +0000388export type AgentAccess = Required<Extract<Access, { type: "https" }>>;
gio10ff1342025-07-05 10:22:15 +0000389
390const NodeBaseSchema = z.object({
391 id: z.string(),
392 position: z.object({
393 x: z.number(),
394 y: z.number(),
395 }),
396});
397
398export const NodeBaseDataSchema = z.object({
399 label: z.string(),
400 envVars: z.array(BoundEnvVarSchema),
401 ports: z.array(PortSchema),
402});
403
404export const NetworkNodeSchema = z
405 .object({
406 type: z.literal("network"),
407 data: z
408 .object({
409 domain: z.string(),
410 })
411 .extend(NodeBaseDataSchema.shape),
412 })
413 .extend(NodeBaseSchema.shape);
414
415export const GithubNodeSchema = z
416 .object({
417 type: z.literal("github"),
418 data: z
419 .object({
420 repository: z
421 .object({
422 id: z.number(),
423 sshURL: z.string(),
424 fullName: z.string(),
425 })
426 .optional(),
427 })
428 .extend(NodeBaseDataSchema.shape),
429 })
430 .extend(NodeBaseSchema.shape);
431
432export const GatewayHttpsNodeSchema = z
433 .object({
434 type: z.literal("gateway-https"),
435 data: z
436 .object({
437 readonly: z.boolean().optional(), // TODO: remove this
438 network: z.string().optional(),
439 subdomain: z.string().optional(),
440 https: z
441 .object({
442 serviceId: z.string(),
443 portId: z.string(),
444 })
445 .optional(),
446 })
447 .extend(NodeBaseDataSchema.shape),
448 })
449 .extend(NodeBaseSchema.shape);
450
451export const GatewayTCPNodeSchema = z
452 .object({
453 type: z.literal("gateway-tcp"),
454 data: z
455 .object({
456 readonly: z.boolean().optional(),
457 network: z.string().optional(),
458 subdomain: z.string().optional(),
459 exposed: z.array(
460 z.object({
461 serviceId: z.string(),
462 portId: z.string(),
463 }),
464 ),
465 selected: z
466 .object({
467 serviceId: z.string().optional(),
468 portId: z.string().optional(),
469 })
470 .optional(),
471 })
472 .extend(NodeBaseDataSchema.shape),
473 })
474 .extend(NodeBaseSchema.shape);
475
476export const ServiceNodeSchema = z
477 .object({
478 type: z.literal("app"),
479 data: z
480 .object({
481 type: ServiceTypeSchema,
482 repository: z
483 .union([
484 z.object({
485 id: z.number(),
486 repoNodeId: z.string(),
487 branch: z.string(),
488 rootDir: z.string(),
489 }),
490 z.object({
491 id: z.number(),
492 repoNodeId: z.string(),
493 branch: z.string(),
494 }),
495 z.object({
496 id: z.number(),
497 repoNodeId: z.string(),
498 }),
499 ])
500 .optional(),
501 volume: z.array(z.string()).optional(),
502 preBuildCommands: z.string().optional(),
503 isChoosingPortToConnect: z.boolean().optional(),
504 dev: z
gio43e0aad2025-08-01 16:17:27 +0400505 .union([
gio10ff1342025-07-05 10:22:15 +0000506 z.object({
507 enabled: z.literal(false),
508 expose: DomainSchema.optional(),
509 }),
510 z.object({
511 enabled: z.literal(true),
gio43e0aad2025-08-01 16:17:27 +0400512 mode: z.literal("VM"),
gio10ff1342025-07-05 10:22:15 +0000513 expose: DomainSchema.optional(),
514 codeServerNodeId: z.string(),
515 sshNodeId: z.string(),
516 }),
gio43e0aad2025-08-01 16:17:27 +0400517 z.object({
518 enabled: z.literal(true),
519 mode: z.literal("PROXY"),
520 address: z.string(),
521 }),
gio10ff1342025-07-05 10:22:15 +0000522 ])
523 .optional(),
524 model: z
525 .object({
526 name: z.enum(["gemini", "claude"]),
527 apiKey: z.string().optional(),
528 })
529 .optional(),
530 info: serviceAnalyzisSchema.optional(),
531
532 ports: z.array(
533 z.object({
534 id: z.string(),
535 name: z.string(),
536 value: z.number(),
537 }),
538 ),
539 activeField: z.string().optional(),
540 state: z.string().nullable().optional(),
541 })
542 .extend(NodeBaseDataSchema.shape),
543 })
544 .extend(NodeBaseSchema.shape);
545
546export const VolumeNodeSchema = z
547 .object({
548 type: z.literal("volume"),
549 data: z
550 .object({
551 size: z.string(),
552 type: VolumeTypeSchema,
553 attachedTo: z.array(z.string()),
554 })
555 .extend(NodeBaseDataSchema.shape),
556 })
557 .extend(NodeBaseSchema.shape);
558
559export const PostgreSQLNodeSchema = z
560 .object({
561 type: z.literal("postgresql"),
562 data: z.object({}).extend(NodeBaseDataSchema.shape),
563 })
564 .extend(NodeBaseSchema.shape);
565
566export const MongoDBNodeSchema = z
567 .object({
568 type: z.literal("mongodb"),
569 data: z.object({}).extend(NodeBaseDataSchema.shape),
570 })
571 .extend(NodeBaseSchema.shape);
572
573export const NodeSchema = z.discriminatedUnion("type", [
574 NetworkNodeSchema,
575 GithubNodeSchema,
576 GatewayHttpsNodeSchema,
577 GatewayTCPNodeSchema,
578 ServiceNodeSchema,
579 VolumeNodeSchema,
580 PostgreSQLNodeSchema,
581 MongoDBNodeSchema,
582]);
583
584export const EdgeSchema = z.object({
585 id: z.string(),
586 source: z.string(),
587 sourceHandle: z.string().optional(),
588 target: z.string(),
589 targetHandle: z.string().optional(),
590});
591
gio52441602025-07-06 18:22:56 +0000592export const ViewportTransformSchema = z.object({
593 transformX: z.number(),
594 transformY: z.number(),
595 transformZoom: z.number(),
596 width: z.number(),
597 height: z.number(),
598});
599
gio10ff1342025-07-05 10:22:15 +0000600export const GraphSchema = z.object({
601 nodes: z.array(NodeSchema),
602 edges: z.array(EdgeSchema),
603 viewport: z
604 .object({
605 x: z.number(),
606 y: z.number(),
607 zoom: z.number(),
608 })
609 .optional(),
gio52441602025-07-06 18:22:56 +0000610 viewportTransform: ViewportTransformSchema.optional(),
gio10ff1342025-07-05 10:22:15 +0000611});
612
613export const GraphOrConfigSchema = z.discriminatedUnion("type", [
614 z.object({
615 type: z.literal("graph"),
616 graph: GraphSchema,
617 }),
618 z.object({
619 type: z.literal("config"),
620 config: ConfigSchema,
621 }),
622]);
623
gio56e9f472025-07-07 03:33:38 +0000624export const GraphConfigOrDraft = z.discriminatedUnion("type", [
625 z.object({
626 type: z.literal("graph"),
627 graph: GraphSchema,
628 }),
629 z.object({
630 type: z.literal("config"),
631 config: ConfigSchema,
632 }),
633 z.object({
634 type: z.literal("draft"),
635 }),
636]);
637
gio10ff1342025-07-05 10:22:15 +0000638export type Edge = z.infer<typeof EdgeSchema>;
gio52441602025-07-06 18:22:56 +0000639export type ViewportTransform = z.infer<typeof ViewportTransformSchema>;
gio10ff1342025-07-05 10:22:15 +0000640export type Graph = z.infer<typeof GraphSchema>;