| name | zodipus-query-engine |
| description | Build type-safe, composable Prisma queries with automatic Zod validation. Use when working with createRegistry, query builders, select/include patterns, validated database queries, findMany validation, or type-safe Prisma results. |
| license | MIT |
| metadata | {"author":"zodipus","version":"1.0.0"} |
Query Engine
Build validated, type-safe Prisma queries with automatic result validation.
When to Apply
- User mentions "query engine" or "createRegistry"
- User wants type-safe Prisma queries
- User asks about select/include with validation
- User needs to validate Prisma query results
- User mentions "composable queries"
- User asks about relation queries with Zod
Core Concept
The Query Engine creates composable query builders that:
- Generate Prisma query objects (
select, include)
- Provide matching Zod schemas for validation
- Infer TypeScript types automatically
User Request → Query Builder → Prisma Query + Zod Schema → Validated Result
Setup
import { createRegistry } from 'zodipus/queryEngine';
import { models, modelRelations } from './generated/generated-index';
const registry = createRegistry({
models,
relations: modelRelations,
});
const userQuery = registry.createQuery('user');
const postQuery = registry.createQuery('post');
const commentQuery = registry.createQuery('comment');
Basic Patterns
Pattern 1: Select Specific Fields
Select only the fields you need. Validation matches your selection.
const query = userQuery({
select: { id: true, email: true, name: true }
});
const user = await prisma.user.findFirst(query.query);
const validated = query.parse(user);
Pattern 2: Include Relations
Include related records with their own field selection.
const query = userQuery({
select: { id: true, email: true },
posts: {
select: { id: true, title: true, published: true }
}
});
const users = await prisma.user.findMany(query.query);
const validated = query.array().parse(users);
Pattern 3: Nested Relations
Chain relations as deeply as your relationDepth allows.
const query = userQuery({
select: { id: true, name: true },
posts: {
select: { title: true },
comments: {
select: { content: true, createdAt: true },
author: {
select: { name: true, email: true }
}
}
}
});
Pattern 4: Array Results (findMany)
Use .array() for validating arrays of results.
const query = userQuery({
select: { id: true, email: true }
});
const users = await prisma.user.findMany(query.query);
const validated = query.array().parse(users);
Pattern 5: Safe Parsing
Handle validation errors without throwing.
const result = query.safeParse(data);
if (result.success) {
console.log(result.data.email);
} else {
console.error(result.error.issues);
console.error(result.error.format());
}
Pattern 6: Partial Validation
For PATCH endpoints or partial updates.
const query = userQuery({
select: { id: true, email: true, name: true }
});
const partialQuery = query.partial();
const updates = partialQuery.parse({ name: 'New Name' });
Advanced Patterns
Combining with Prisma Where/OrderBy
The Query Engine generates select/include objects. Combine with your own conditions:
const query = userQuery({
select: { id: true, email: true, name: true },
posts: { select: { title: true } }
});
const users = await prisma.user.findMany({
...query.query,
where: { role: 'ADMIN' },
orderBy: { createdAt: 'desc' },
take: 10,
skip: 0,
});
const validated = query.array().parse(users);
Reusable Query Fragments
Create reusable query configurations:
const minimalUser = { select: { id: true, email: true } } as const;
const fullUser = {
select: { id: true, email: true, name: true, role: true },
posts: { select: { id: true, title: true } }
} as const;
const minimalQuery = userQuery(minimalUser);
const fullQuery = userQuery(fullUser);
Conditional Relations
Include relations conditionally:
function getUserQuery(includePosts: boolean) {
const base = { select: { id: true, email: true, name: true } };
if (includePosts) {
return userQuery({
...base,
posts: { select: { id: true, title: true } }
});
}
return userQuery(base);
}
Type Extraction
Extract types from queries:
import { z } from 'zod';
const query = userQuery({
select: { id: true, email: true },
posts: { select: { title: true } }
});
type UserWithPosts = z.infer<ReturnType<typeof query.parse>>;
type UserWithPosts = z.output<typeof query>;
API Reference
createRegistry(config)
Creates a registry for building queries.
const registry = createRegistry({
models,
relations: modelRelations,
});
registry.createQuery(modelName)
Returns a query builder function for the specified model.
const userQuery = registry.createQuery('user');
const postQuery = registry.createQuery('post');
Query Builder Return Object
const query = userQuery({ select: { id: true } });
query.query
query.parse()
query.safeParse()
query.array()
query.partial()
Priority Patterns
| Priority | Pattern | When to Use |
|---|
| Critical | query.parse(result) | Validate findFirst/findUnique results |
| Critical | query.array().parse(results) | Validate findMany results |
| High | query.safeParse(result) | When you need error handling |
| High | Nested relations | Querying with related data |
| Medium | query.partial() | PATCH endpoints, partial updates |
| Medium | Combining with where/orderBy | Filtered queries |
| Low | Type extraction | Advanced TypeScript usage |
Common Mistakes
Mistake 1: Forgetting .array() for findMany
const users = await prisma.user.findMany(query.query);
const validated = query.parse(users);
const validated = query.array().parse(users);
Mistake 2: Using query.query as the whole argument
const user = await prisma.user.findFirst(query.query);
const user = await prisma.user.findFirst({
query.query,
where: { id: '123' }
});
const user = await prisma.user.findFirst({
...query.query,
where: { id: '123' }
});
Mistake 3: Accessing unselected fields
const query = userQuery({ select: { id: true } });
const user = query.parse(result);
console.log(user.email);
const query = userQuery({ select: { id: true, email: true } });
For more patterns, see references/QUERY-PATTERNS.md.