Normalization Pipeline
How VibORM transforms user input for the query engine
Normalization Pipeline
VibORM uses ArkType's .pipe() to normalize user-friendly shorthand syntax into canonical forms. This simplifies the query engine by ensuring it always receives predictable data structures.
Pipeline Overview
Scalar Filter Normalization
The Pattern
// Schema definition with pipe
export const stringFilter = type({
equals: stringBase,
contains: stringBase,
// ... other operators
})
.partial()
.or(stringBase.pipe((v) => ({ equals: v }))); // Shorthand → canonicalHow It Works
All Scalar Normalizations
| Field Type | User Writes | Query Engine Receives |
|---|---|---|
| String | "Alice" | { equals: "Alice" } |
| Number | 42 | { equals: 42 } |
| Boolean | true | { equals: true } |
| DateTime | new Date() | { equals: Date } |
| BigInt | BigInt(1) | { equals: BigInt(1) } |
| Enum | "A" | { equals: "A" } |
| Blob | Uint8Array | { equals: Uint8Array } |
| Nullable | null | { equals: null } |
Scalar Update Normalization
The Pattern
export const stringUpdate = type({ set: stringBase })
.partial()
.or(stringBase.pipe((v) => ({ set: v })));Normalization Flow
All Update Normalizations
| Field Type | User Writes | Query Engine Receives |
|---|---|---|
| String | "value" | { set: "value" } |
| Number | 42 | { set: 42 } |
| Boolean | false | { set: false } |
| Blob | Uint8Array | { set: Uint8Array } |
| JSON | { a: 1 } | { set: { a: 1 } } |
Numeric Operations (No Normalization)
These are already in canonical form:
// Already canonical - no normalization needed
{ increment: 5 }
{ decrement: 3 }
{ multiply: 2 }
{ divide: 4 }To-One Relation Filter Normalization
The Most Important Normalization
shape[name + "?"] = type("object | null").pipe((t) => {
if (t === null) return { is: null };
if (isToOneShorthand(t)) return t; // Already has is/isNot
return { is: t }; // Wrap shorthand
});Detection Logic
const isToOneShorthand = (t?: unknown): t is { is: unknown } | { isNot: unknown } => {
return (
t !== undefined &&
typeof t === "object" &&
t !== null &&
("is" in t || "isNot" in t)
);
};Normalization Cases
Complete Examples
// User writes (shorthand)
where: { author: { name: "Alice" } }
// Query engine receives (canonical)
where: { author: { is: { name: "Alice" } } }// User writes (null shorthand for optional relation)
where: { profile: null }
// Query engine receives
where: { profile: { is: null } }// User writes (explicit - no change)
where: { author: { is: { name: "Alice" }, isNot: { role: "admin" } } }
// Query engine receives (same)
where: { author: { is: { name: "Alice" }, isNot: { role: "admin" } } }To-Many Relation Operations
Array Normalization
const ensureArray = <T>(v: T | T[]): T[] => Array.isArray(v) ? v : [v];
// Single-or-array operations
shape["create?"] = createSchema.or(createSchema.array()).pipe(ensureArray);
shape["connect?"] = connectSchema.or(connectSchema.array()).pipe(ensureArray);Array-Only Operations
Some operations don't support single-value shorthand (ArkType limitation):
// These always require arrays
shape["deleteMany?"] = whereSchema.array();
shape["updateMany?"] = updateManySchema.array();
shape["upsert?"] = upsertSchema.array();Normalization Summary
| Operation | User Can Write | Query Engine Receives |
|---|---|---|
| create | { title: "Post" } | [{ title: "Post" }] |
| create | [{ title: "A" }, { title: "B" }] | [{ title: "A" }, { title: "B" }] |
| connect | { id: "123" } | [{ id: "123" }] |
| deleteMany | [{ published: false }] | [{ published: false }] |
| updateMany | [{ where: {...}, data: {...} }] | [{ where: {...}, data: {...} }] |
Nested not Filter
The not operator accepts both direct values and nested filter objects:
const stringFilterBase = type({
equals: stringBase,
contains: stringBase,
// ...
}).partial();
export const stringFilter = stringFilterBase
.merge(type({ "not?": stringFilterBase.or(stringNullable) }))
.or(stringBase.pipe((v) => ({ equals: v })));Examples
// Direct value
{ name: { not: "Alice" } }
// SQL: name != 'Alice'
// Nested filter
{ name: { not: { contains: "test", startsWith: "A" } } }
// SQL: NOT (name LIKE '%test%' AND name LIKE 'A%')Query Engine Guarantees
After normalization, the query engine can rely on:
Filter Guarantees
- Scalar filters are always objects - Never raw values
- To-one relations have
is/isNot- Never shorthand - Null is wrapped -
{ equals: null }or{ is: null } notcan contain objects - Must handle nested filters
Update Guarantees
- Scalar updates have
set- Direct values wrapped - Numeric ops are explicit -
increment,decrement, etc. - Array ops are explicit -
set,push,unshift
Relation Guarantees
- To-many operations use arrays - Even single items
- Some operations are array-only -
deleteMany,updateMany,upsert
Debugging Normalization
To see normalized output:
const whereSchema = model["~"].schemas.where;
// Validate and get normalized result
const result = whereSchema({ name: "Alice" });
console.log(result); // { name: { equals: "Alice" } }
// Or with relations
const result2 = whereSchema({ author: { name: "Alice" } });
console.log(result2); // { author: { is: { name: "Alice" } } }