L3 - Operation Schemas
Validation schemas generated for where, create, update, and args operations
Location: src/validation/model/, src/validation/relations/
Why This Layer Exists
VibORM validates query inputs at runtime:
orm.user.findMany({
where: { email: { contains: "@example.com" } }, // Validated!
include: { posts: true }, // Validated!
});
Operation schemas define what’s valid for each operation. They’re built dynamically by SchemaRegistry from model definitions.
Schema Categories
Core Schemas (Per Scalar)
Each scalar type generates its own schemas:
| Schema | Purpose | Example |
|---|---|---|
| Base | Raw input validation | string or null |
| Filter | Where clause operators | { contains: string } |
| Create | Create operation input | Required unless defaulted |
| Update | Update operation input | Optional, can be null |
Args Schemas (Per Model)
The validation registry composes scalar, relation, and model state into operation schemas:
| Schema | Purpose |
|---|---|
| WhereSchema | Full where clause with all fields |
| CreateDataSchema | Full data for create operations |
| UpdateDataSchema | Full data for update operations |
| FindArgs | where + select + include + orderBy |
| CreateArgs | data with nested relation creates |
| UpdateArgs | where + data with nested writes |
Dynamic Schema Generation
Schemas are generated based on scalar state:
// If scalar is nullable + has default:
// - Filter: allows null comparisons
// - Create: optional (has default)
// - Update: accepts null or value
The logic considers:
- Is the scalar nullable?
- Does it have a default value?
- Is it an auto-generated scalar (UUID, timestamps)?
- Is it optional vs required?
Handling Relations
Relations add complexity because queries can be nested:
orm.user.findMany({
where: {
posts: { some: { published: true } } // Nested where
},
include: {
posts: {
where: { published: true }, // Nested filter
select: { title: true } // Nested select
}
}
});
Operation schemas handle this with recursive definitions using thunks to avoid infinite loops.
Why Registry Building?
Schemas are expensive to construct and nested relation inputs need full model graph context. VibORM builds them through SchemaRegistry:
const registry = createSchemaRegistry({ user });
registry.proxy.user.core.where; // Built through registry access, then cached
This keeps schema definitions focused on structure while operation validation stays centralized.
Boundary With the Query Engine
An operation schema is the trust boundary for public query input. Its output can contain defaults, normalized shorthands, and transformed values, so the query engine treats that output as a domain value rather than reparsing the caller’s raw object.
For nested writes, L6 separates scalar fields from relation payloads. As soon as
a relation payload is schema-transformed, it becomes a
RelationMutationProgram. The program preserves mutation-kind order, source
array order, duplicates, and explicit values such as set: []. OwnWrite
analysis and relation emitters consume the same program.
This gives the validation transform one owner and one execution. Downstream compilers do not assume that a transform is idempotent.
Connection to Other Layers
- L1 (Validation): Operation schemas use
v.*primitives andSchemaRegistry - L2 (Scalars): Scalar base schemas and state feed registry scalar schemas
- L4 (Relations): Relation state feeds nested operation schemas
- L6 (Query Engine): Validates at the operation boundary, then compiles the schema-transformed values without reparsing them