VibORM

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 → canonical

How It Works

All Scalar Normalizations

Field TypeUser WritesQuery Engine Receives
String"Alice"{ equals: "Alice" }
Number42{ equals: 42 }
Booleantrue{ equals: true }
DateTimenew Date(){ equals: Date }
BigIntBigInt(1){ equals: BigInt(1) }
Enum"A"{ equals: "A" }
BlobUint8Array{ equals: Uint8Array }
Nullablenull{ 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 TypeUser WritesQuery Engine Receives
String"value"{ set: "value" }
Number42{ set: 42 }
Booleanfalse{ set: false }
BlobUint8Array{ 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

OperationUser Can WriteQuery 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

  1. Scalar filters are always objects - Never raw values
  2. To-one relations have is/isNot - Never shorthand
  3. Null is wrapped - { equals: null } or { is: null }
  4. not can contain objects - Must handle nested filters

Update Guarantees

  1. Scalar updates have set - Direct values wrapped
  2. Numeric ops are explicit - increment, decrement, etc.
  3. Array ops are explicit - set, push, unshift

Relation Guarantees

  1. To-many operations use arrays - Even single items
  2. 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" } } }

On this page