L9 - Client
The orm.model.operation() interface providing type-safe queries with zero code generation
Location: src/client/
Why This Layer Exists
VibORM’s type safety comes from inferring types from schema definitions. The client layer makes this work:
import { createClient } from "viborm";
const orm = createClient({ schema, driver });
// Fully typed - no codegen!
const users = await orm.user.findMany({
where: { email: { contains: "@example.com" } },
select: { id: true, email: true, posts: { select: { title: true } } }
});
// Type: Array<{ id: string; email: string; posts: Array<{ title: string }> }>
How It Works
Recursive Proxy Pattern
The client uses JavaScript Proxy to handle dynamic model access:
orm.user // Proxy intercepts "user" property access
.findMany // Proxy intercepts "findMany" method access
(args) // Calls actual findMany implementation
This enables orm.<anyModel>.<anyOperation> without defining every combination.
Type Inference Chain
Types flow through a chain:
Schema Definition
↓
Model extracts fields (scalars and relations)
↓
Client infers model names from schema
↓
Operations infer args/return from model
↓
Select/Include modifies return type
No step uses code generation - TypeScript infers at each level.
Select/Include Aware Results
Return types change based on what you select:
// Full model (all fields)
orm.user.findFirst({ where: { id: "1" } });
// Type: { id, email, name, createdAt, ... } | null
// Selected fields only
orm.user.findFirst({
where: { id: "1" },
select: { id: true, email: true }
});
// Type: { id, email } | null
// With relations
orm.user.findFirst({
where: { id: "1" },
include: { posts: true }
});
// Type: { id, email, ..., posts: Post[] } | null
Available Operations
The full Operations union (see src/client/types.ts):
| Operation | Description |
|---|---|
findMany |
Get multiple records |
findFirst |
Get first matching record |
findFirstOrThrow |
Get first matching record or throw |
findUnique |
Get record by unique constraint |
findUniqueOrThrow |
Get record by unique constraint or throw |
exist |
Check whether any row matches |
create |
Create single record |
createMany |
Create multiple records; returns { count }, or the created rows when select is present |
update |
Update a record by unique constraint |
updateMany |
Update multiple records; returns { count }, or the updated rows when select is present |
upsert |
Create or update on conflict |
delete |
Delete a record by unique constraint |
deleteMany |
Delete multiple records; returns { count }, or the deleted rows when select is present |
count |
Count matching records |
aggregate |
Aggregate functions (SUM, AVG, etc.) |
groupBy |
Group rows and aggregate per group |
Why Zero Codegen?
Code generation ORMs (like Prisma) require a build step:
prisma generate # Must run after schema changes
VibORM infers types directly from your schema definition:
const schema = {
user: s.model({ ... }),
post: s.model({ ... }),
};
const orm = createClient({ schema, driver }); // Types inferred immediately
Benefits:
- Instant feedback: Types update on save
- No build step: Simpler CI/CD
- No generated files: Cleaner repository
Connection to Other Layers
- L2-L4 (Schema): Client infers types from schema definitions
- L3 (Query Schemas): Client validates args at runtime
- L6 (Query Engine): Client routes operations to query engine
- L8 (Drivers): Query results flow back through client