question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

[Proposal] Prisma Client Extensions

See original GitHub issue

Client 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:open
  • Created a year ago
  • Reactions:241
  • Comments:54 (16 by maintainers)

github_iconTop GitHub Comments

107reactions
floelhoeffelcommented, Oct 28, 2022

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 objects
  • model - custom methods on models
  • query - custom client queries
  • client - custom Prisma client

Context

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
  firstName String
  lastName  String
  age       Number
  tags      String[]
  orders    Order[]
}

And the following PrismaClient instance:

const prisma = new PrismaClient()

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.

const xprisma = prisma.$extends({
  result: {
    user: {
      needs: {
        fullName: { firstName: true, lastName: true }
      },
      fields: {
        fullName(user) {
          return `${user.firstName} ${user.lastName}`
        }
      }
    }
  }
})

this extends the User model with a new, virtual field fullName. We declared the field’s dependencies and the computation logic.

const user = await xprisma.user.findFirst()
console.log(user.fullName) // "John Doe"

Result extensions don’t over-fetch. Fields are only computed if its dependencies are present:

const user = await xprisma.user.findFirst({ select: { email: true }))
console.log(user.fullName) // undefined

For performance reasons, results are only computed when accessed at runtime.

Result extensions can be used to add methods to result objects:

const xprisma = prisma.$extends({
  result: {
    user: {
      needs: {
        getAccountBalance: {
          account: true,
        },
      },
      fields: {
        getAccountBalance: (user) => (n: number) => {
          return user.account.balance + n
        }
      }
    }
  }
})
const result = await xprisma.user.findFirst()
console.log(result?.getAccountBalance(-500)) // 7245

Model extensions

model is designed to augment models. An augmented model has new methods to encapsulate business logic.

const xprisma = prisma.$extends({
  model: {
    user: {
      async signUp(email: string) {
        await prisma.user.create(...)
      },
    },
  }
})

The model User was augmented with a signUp method to wrap user creation and account logic. signUp can now be called from anywhere via your model and via the extension:

const user = await xprisma.user.signUp('john@prisma.io')

Model extensions support a wildcard to augment all models:

const xprisma = new prisma.$extends({
  model: {
    $allModels: {
      getClass<T extends object>(this: T): new () => T {
        return class { /** Generic Implementation */ } as any
      }
    }
  }
})

The getClass method is now accessible from all model:

class UserService extends xprisma.user.getClass() {
    method() {
        const user = this.findFirst({}) // fully typesafe
    }
}

Query extensions

query is designed to extend queries. To get a specific subset of User from the database, let’s only get users older than 18 years:

const xprisma = prisma.$extends({
  query: {
    user: {
      async findMany({ model, action, args, data }) {
        args.where.age = { gt: 18 }
        const myData = await data
        return data
      },
    },
  },
})

In contrast to middlewares, extended queries return type safe data.

await xprisma.user.findMany() // only above 18 here

Query extensions also support a way to handle nested operations. With $nestedOperations, it will be possible to recursively traverse a given operation’s arguments:

const xprisma = prisma.$extends({
  query: {
    user: {
      $nestedOperations: {
        where({ model, operation, args, data, path }) {
          // path here could be `user.findFirst`

          args.where.age = { gt: 18 }
        }
      }
    }
  }
})

The $allModels wildcard will also be available for query extensions.

💡 You asked us for a way to group extensions by models and in other files. While it is not reflected in this document, we have kept it in mind and will provide this in future iterations. We have some ideas for achieving this, one of them looks like the following. const xprisma = prisma.$extends.user({result: {}})

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:

const xprisma = prisma.$extends({
  client: {
    begin() { ... }, // sparing you the details
  }
})

Now we can start an interactive transaction omitting the traditional callback:

const tx = await xprisma.$begin()

await tx.user.create(...)
await tx.user.update(...)

await tx.$commit()

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.

import { Prisma } from '@prisma/client'

export default Prisma.createExtension({
  model: {
    $allModels: {
      // new method
      findOrCreate(...) { }
    }
  }
})

Usage:

import findOrCreate from "prisma-extension-find-or-create"

const xprisma = prisma.$extends(findOrCreate)
const user = await xprisma.user.findOrCreate({ ... })

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 😊

27reactions
floelhoeffelcommented, Nov 10, 2022

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 🙇‍♂️!

Read more comments on GitHub >

github_iconTop Results From Across the Web

Prisma Client extensions (Preview)
From version 4.7.0, you can use Prisma Client extensions to add functionality to your models, result objects, and queries, or to add client-level...
Read more >
Prisma on Twitter: "In 4.3.0, we shared a proposal for Prisma ...
0, we shared a proposal for Prisma Client Extensions on Github. We received a lot of great feedback, which we have incorporated into...
Read more >
Prisma 4.7 Introduces Client Extensions
With the release of Prisma 4.7, client extensions are now ... Please note that if you plan to perhaps send the response to...
Read more >
Several extensions of the PRISMA Statement
Several extensions of the PRISMA Statement have been developed to facilitate the reporting of different types or aspects of systematic reviews.
Read more >
PRISMA Extension for Scoping Reviews (PRISMA-ScR)
It was decided that a PRISMA extension for scoping reviews was needed ... their agreement with each of the proposed reporting items using...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found