Worried about Prisma Next #29631
QuestionConcern about loss of composable query objects in Prisma NextFirst of all, I really like the direction Prisma Next is taking. The new query API looks significantly more readable and avoids deeply nested structures. However, I have a concern regarding a pattern that is heavily used in real-world applications: composable query objects for business logic. Current pattern (Prisma 7)In many production systems, we build queries dynamically using plain objects: const filters: Prisma.ProductoWhereInput = {
...baseFilters,
...(search ? { nombre: { contains: search } } : {}),
};We also encapsulate domain logic in reusable functions: export function buildListaPrecioDetalleFilter(...) {
return {
listaPrecio: {
...base,
canalComercial: { isActive: true, codigo: canalCodigo },
},
};
}This allows us to:
Concern with Prisma Next APIWith the new chain-based API: db.orders
.where(...)
.include(...)It seems harder (or impossible?) to:
This pattern is critical in systems where business logic determines query structure dynamically. Questions
SuggestionIf this pattern is not directly supported, it would be great to have:
Why this mattersThis is not just a stylistic preference — it directly impacts:
Thanks again for building Prisma Next in the open. This kind of feedback loop is really valuable. How to reproduce (optional)No response Expected behavior (optional)No response Information about Prisma Schema, Client Queries and Environment (optional)No response |
Replies: 5 comments
|
The piped API doesn't stop you from composing conditions dynamically — you just compose the arguments instead of the whole object. With Prisma Next's pipe syntax, you can still build reusable filter factories: // Build filter conditions the same way, just pass them to the pipe
const filters = {
...baseFilters,
...(search ? { nombre: { contains: search } } : {}),
};
const result = await db.producto
.where(filters)
.orderBy({ precio: 'asc' })
.select({ id: true, nombre: true })
.exec();Your export function buildListaPrecioDetalleFilter(opts: FilterOpts) {
return {
listaPrecio: {
...base,
canalComercial: { isActive: true, codigo: opts.canalCodigo },
},
};
}
// Usage hasn't changed meaningfully
const result = await db.producto
.where(buildListaPrecioDetalleFilter({ canalCodigo }))
.exec();The main difference is that you're piping methods instead of nesting keys. The composition patterns — spread, conditional spreads, helper functions returning filter objects — all carry over directly because the For cases where you need to dynamically add/remove entire clauses (sort, pagination, etc.), the pipe actually makes it cleaner: let query = db.producto.where(filters);
if (sortField) query = query.orderBy({ [sortField]: sortDir });
if (page) query = query.skip((page - 1) * limit).take(limit);
const result = await query.exec();That's harder to do with the nested object syntax. |
|
Your concern about composition in a method-chained API is highly valid. Object-based queries in Prisma 7 made dynamic filters, conditional spreads, and fragment sharing trivial. However, chain-based query APIs are declarative and lazy under the hood, meaning you can still compose queries dynamically using the following patterns: 1. Incremental Query BuildingInstead of immediately awaiting a query, you can build the query builder instance incrementally by conditionally chaining methods: async function getFilteredProducts(search?: string, baseFilters?: Prisma.ProductoWhereInput) {
// Start the base query builder
let query = db.products.where(baseFilters || {});
// Conditionally add filters
if (search) {
query = query.where({ nombre: { contains: search } });
}
// Chain includes and finally execute the query
const results = await query.include({ category: true });
return results;
}2. Functional Composition (Reusable Fragments)You can write helper functions that accept a query builder and return a modified builder. This encapsulates business logic and keeps it reusable across layers: // Encapsulated domain logic
export function withActivePriceList(query: any, canalCodigo: string) {
return query.where({
listaPrecio: {
canalComercial: { isActive: true, codigo: canalCodigo },
},
});
}
// Usage:
const query = db.products.all();
const filteredQuery = withActivePriceList(query, 'retail');
const results = await filteredQuery.include({ details: true });3. Utilizing Client ExtensionsPrisma Client Extensions allow you to attach custom methods directly to your models, acting as a native way to share query composition: const prisma = new PrismaClient().$extends({
model: {
product: {
async findActiveByChannel(canalCodigo: string) {
return prisma.product.findMany({
where: {
listaPrecio: {
canalComercial: { isActive: true, codigo: canalCodigo }
}
}
});
}
}
}
}); |
|
I think this is a valid concern, especially for larger applications where query construction is part of the business layer rather than something written inline at the point of execution. One thing I would separate is query composition from query execution. In Prisma 7, WhereInput objects naturally acted as a composable intermediate representation: const filters = { Because these are plain objects, they are easy to: Merge with spread operators The concern with a chain-based API is that the query becomes an executable construct much earlier in the process. For example: db.products is very readable, but it's less obvious how to build that across multiple modules without coupling business rules to the query implementation. A possible approach One pattern that might preserve separation of concerns is to keep domain logic returning plain filter definitions and only convert them into a query at the final execution layer. Something like: function activeProductsFilter() { function searchFilter(search: string) { Then: const filter = { db.products.where(filter); If Prisma Next still accepts object-based filters inside the chain API, this would preserve most of the existing architecture while benefiting from the new syntax. My biggest concern The challenge is not readability of individual queries. The challenge is maintaining: Reusable domain filters These are extremely common requirements in enterprise applications. What would be ideal A hybrid model would probably offer the best developer experience: const filter = buildProductFilter(...); db.products This would allow: Chain-based readability So my question would be: Is the intention that filter objects remain the primary composable unit and the chain API simply becomes a more ergonomic execution layer, or is Prisma Next moving toward query builders as the main composition mechanism? The answer to that will likely determine how easy it is for large codebases to migrate existing business-logic-heavy query patterns. |
|
This is a well-articulated, important concern, and the good news is that "collections" — which you specifically asked about — are exactly the answer to most of what you're describing, though they work differently from object-spread composition rather than replicating it directly. Let me walk through what's actually there based on what Prisma has published so far. Answering your numbered questions directly1. Is there an equivalent to composing
|
|
Thanks, everyone; you've really put my mind at ease. I'll leave the discussion open in case anyone wants to contribute or so GitHub can close it automatically. |
This is a well-articulated, important concern, and the good news is that "collections" — which you specifically asked about — are exactly the answer to most of what you're describing, though they work differently from object-spread composition rather than replicating it directly. Let me walk through what's actually there based on what Prisma has published so far.
Answering your numbered questions directly
1. Is there an equivalent to composing
WhereInputas plain data?Not as plain object spreading, based on what's been shown publicly so far. Prisma Next's SQL query builder uses a fluent chain syntax —
db.sql.selectFrom(...).leftJoin(...).select(...).orderBy(...)— and the ORM-level quer…