Skip to content

Zod Schemas

Les schémas Zod sont la source unique de vérité pour tous les types de l'application. Définis dans packages/shared, ils sont importés par le serveur, l'agent et le frontend via l'inférence de types tRPC.

Location

packages/shared/src/schemas/
├── service.schema.ts      # ServiceConfig, ServiceVariable
├── environment.schema.ts  # Environment, Variable
├── agent.schema.ts        # Agent
├── project.schema.ts      # Project
├── settings.schema.ts     # Settings, JSLogParser
└── log.schema.ts          # LogLine, HealthMessage

Definition Rules

Required Constraints

typescript
// ✅ Toujours .max() sur les strings
z.string().min(1).max(128)     // IDs, noms
z.string().max(2048)           // commandes, chemins, URLs
z.string().url().max(2048)     // URLs (url() ET max())
z.string().email().max(320)    // emails

// ✅ Toujours .max() sur les arrays
z.array(ItemSchema).max(100)

// ✅ .refine() pour limiter les records
z.record(z.string(), VariableSchema).refine(
  r => Object.keys(r).length <= 1000,
  'Too many variables'
)

Type Inference

typescript
export const ServiceConfigSchema = z.object({ ... })

// Type inféré — pas de duplication
export type ServiceConfig = z.infer<typeof ServiceConfigSchema>

Partial Schemas for Updates

typescript
// Pour les mutations update, utiliser .partial() ou .pick()
export const ServiceUpdateSchema = ServiceConfigSchema
  .omit({ id: true, createdAt: true })
  .partial()

Mandatory Rebuild

Après modification de packages/shared :

sh
yarn workspace @runeya/packages-shared build

Sans cette étape, tsup bundle l'ancienne version du dist et Zod strip silencieusement les nouveaux champs.

Inline Schemas in Routers

Les routers tRPC doivent référencer les schémas partagés — jamais dupliquer inline :

typescript
// ✅ Référence au schéma partagé
service.create: protectedProcedure
  .input(ServiceCreateSchema)
  .mutation(...)

// ❌ Schéma inline qui peut dériver
service.create: protectedProcedure
  .input(z.object({ name: z.string(), ... }))  // copie qui diverge
  .mutation(...)

LogLine schema

typescript
export const LogLineSchema = z.object({
  raw: z.string().max(16384),
  stream: z.enum(['stdout', 'stderr']),
  level: z.enum(['error', 'warn', 'info', 'debug']),
  message: z.string().max(16384),
  hidden: z.boolean(),
  metadata: z.record(z.string(), z.unknown()).refine(
    r => Object.keys(r).length <= 100
  ),
  json: z.record(z.string(), z.unknown()).optional(),
  timestamp: z.string().datetime(),
})

HealthMessage schema

typescript
export const HealthMessageSchema = z.discriminatedUnion('type', [
  z.object({ type: z.literal('ping') }),
  z.object({ type: z.literal('pong') }),
  z.object({ type: z.literal('status'), data: AgentStatusSchema }),
])

Les messages WebSocket de health sont validés avec ce schéma — messages invalides ignorés (le timer d'alive n'est reset qu'après validation réussie).

Released under the MIT License.