Skip to content

Schémas Zod

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.

Localisation

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
└── image.schema.ts        # ImagePart (ajouté — image support)

Règles de définition

Contraintes obligatoires

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'
)

Inférence de types

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

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

Schémas partiels pour update

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

Rebuild obligatoire

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.

Schémas inline dans les 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(...)

ImagePart schema

Introduit dans packages/shared/src/schemas/image.schema.ts pour le support des images dans le chat.

typescript
export const ImagePartSchema = z.object({
  type: z.literal('image'),
  imageId: z.string().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i),
  url: z.string().min(1),
  mimeType: z.enum(['image/png', 'image/jpeg', 'image/webp', 'image/gif']),
  altText: z.string().max(500).optional(),
})
export type ImagePart = z.infer<typeof ImagePartSchema>

Ce schéma est intégré dans l'union discriminée MessagePartSchema dans apps/server/src/trpc/routers/chat.ts. Les messages sans images (parts ne contenant pas d'image) continuent de valider sans modification.


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).

Publié sous licence MIT.