[Proposal] Prisma Client Extensions
See original GitHub issueClient Extensions Proposal
Hey folks, we started designing this feature and we’re ready to share our proposal. Please let us know what you think!
Design
We aim to provide you with a type-safe way to extend your Prisma Client to support many new use-cases and also give a way to express your creativity. We plan to work on four extension layers which are $result
, $model
, $client
, $use
.
Let’s consider the following model:
model Order {
id String @id
paid Int
due Int
user User @relation(fields: [userId], references: [id])
userId String
}
model User {
id String @id
email String @unique
name String
age Number
tags String[]
orders Order[]
}
model Deleted {
id String
kind String
}
Computed fields
We have this database model but we want to “augment” it at runtime. We want to add fields to our query results, have them computed at runtime, and let our own types flow through. To do this, we use $result
to extend the results:
const prisma = new PrismaClient().$extends({
$result: {
User: (compute) => ({
fullName: compute({ firstName: true, lastName: true }, (user) => {
return `${user.firstName} ${user.lastName}`
}),
}),
},
})
We just extended our User
model results with a new field called fullName
. To do that, we defined our field with compute
, where we expressed our field dependencies and computation logic.
const user = await prisma.user.findFirst()
console.log(user.fullName) // "John Doe"
Result extensions will never over-fetch. It means that we only compute if we are able to:
const user = await prisma.user.findFirst({ select: { email: true }))
console.log(user.fullName) // undefined
Finally, you will be able to add fields to all your model results at once via a generic call using $all
instead of a model name:
const prisma = new PrismaClient().$extends({
$result: {
$all: (client) => ({
date() {
return new Date()
},
}),
},
})
Results are never computed ahead of time, but only on access for performance reasons.
Model methods
Extending the results is useful, but we would also love to store some custom logic on our models too… so that we can encapsulate repetitive logic, or business logic. To do this, we want to use the new $model
extension capability:
const prisma = new PrismaClient().$extends({
$model: {
User: {
async signUp(email: string) {
await client.user.create(...)
},
},
}
})
We extended our model User
with a signUp
method and put the user creation and account logic away into a signUp
method. signUp
can now be called from anywhere via your model and via the extension:
const user = await prisma.user.signUp('john@prisma.io')
If you want to build more advanced model extensions, we will also provide an $all
wildcard like before:
const prisma = new PrismaClient().$extends({
$model: {
$all: (client) => ({
softDelete<T>(this: T, id: string) { // T is the model
await client.deleted.create(...)
}
}),
}
})
We just implemented a brand new softDelete
operation, we can now easily soft delete any of the models:
await prisma.user.softDelete('42')
Extending your queries
We want to perform queries on a specific subset of User
in our database. In this case, we just want to work on the users that are above 18
years old. For this, we have a $use
extension:
const prisma = new PrismaClient().$extends({
$use: {
User: {
async findMany({ model, action, args, data }) {
args.where.age = { gt: 18 }
console.log(await data)
return data
},
},
},
})
$use
extensions allow you to modify the queries that come through in a type-safe manner. This is a type-safe alternative to middlewares. If you’re using TypeScript, you will benefit from end-to-end type safety here.
await prisma.user.findMany() // only above 18 here
Note: The
$all
wildcard will also be available for$use
extensions
Client methods
Models aren’t enough, maybe there’s a missing feature? Or maybe you need to solve something specific to your application? Whatever it is, we want to give you the possibility to experiment and build top-level client features.
For this example, we want to be able to start an interactive transaction without callbacks. To do this, we will use the $client
extension layer:
const prisma = new PrismaClient().$extends({
$client: {
begin() { ... }, // sparing you the details
}
})
Now we can start an interactive transaction without needing the traditional callback:
const tx = await prisma.$begin()
await tx.user.create(...)
await tx.user.update(...)
await tx.$commit()
Extension isolation
When you call $extends
, you actually get a forked state of your client. This is powerful because you can customize your client with many extensions and independently. Let’s see what this means:
// First of all, store your original prisma client into a variable (as usual)
const prisma = new PrismaClient()
const extendsA = prisma.$extends(extensionA)
const extendsB = prisma.$extends(extensionB)
const extendsAB = prisma
.$extends(extensionA)
.$extends(extensionB)
Thanks to this forking mechanism, you can mix and match them as needed. That means that you can write as many flavors of extensions as you would like and for all your different use-cases, without any conflicts.
More extensibility
We are building Client Extensions with shareability in mind so that they can be shared as packages or code snippets. We hope that this feature will attract your curiosity and spark creativity 🚀.
export default {
$model: {
$all: {
// new method
findOrCreate(...) { }
}
}
}
Usage
import findOrCreate from "prisma-find-or-create"
const prisma = new PrismaClient().$extends(findOrCreate)
const user = await prisma.user.findOrCreate({ ... })
Issue Analytics
- State:
- Created a year ago
- Reactions:241
- Comments:54 (16 by maintainers)
Top GitHub Comments
New proposal 🎉
Thanks everyone for the great feedback 😊 - we have applied lots of improvements to a new version of the proposal and are eager to hear what you think:
tl;dr
Client extensions are designed to enable 4 ways to extend Prisma client:
result
- custom result objectsmodel
- custom methods on modelsquery
- custom client queriesclient
- custom Prisma clientContext
Let’s consider the following model:
And the following PrismaClient instance:
Result extensions
result
is designed to “augment” results. This can be used to add fields to query results, computed at runtime and in a type safe way.this extends the
User
model with a new, virtual fieldfullName
. We declared the field’s dependencies and the computation logic.Result extensions don’t over-fetch. Fields are only computed if its dependencies are present:
For performance reasons, results are only computed when accessed at runtime.
Result extensions can be used to add methods to result objects:
Model extensions
model
is designed to augment models. An augmented model has new methods to encapsulate business logic.The model
User
was augmented with asignUp
method to wrap user creation and account logic.signUp
can now be called from anywhere via your model and via the extension:Model extensions support a wildcard to augment all models:
The
getClass
method is now accessible from all model:Query extensions
query
is designed to extend queries. To get a specific subset ofUser
from the database, let’s only get users older than 18 years:In contrast to middlewares, extended queries return type safe data.
Query extensions also support a way to handle nested operations. With $nestedOperations, it will be possible to recursively traverse a given operation’s arguments:
The
$allModels
wildcard will also be available for query extensions.Client extensions
$client is designed to allow top level customization of Prisma client. An example use case is to start an interactive transaction without callbacks:
Now we can start an interactive transaction omitting the traditional callback:
Isolating extensions
$extends
returns a forked state of Prisma client. This allows the creation of instances of Prisma client extended by different extensions.Sharing extensions
We are designing Client Extensions with shareability in mind so that they can be shared as packages or code snippets.
Usage:
Thanks for reading this far 🙏 and any comments / feedback are highly appreciated!
You could also message @millsp or myself in our slack community.
Thanks again 😊
Hello everyone 👋 - we have an early prototype in an integration branch 🔥. It contains
client
(type & runtime support)model
(type & runtime support)result
(type support, no runtime support yet)query
(type support, no runtime support yet)To try it out, checkout this repository that shows a demo. We also have some early docs.
We are working hard to get it properly into preview - any feedback as always very welcome 🙇♂️!