Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Proposal] Prisma Client Extensions #15074

Closed
Tracked by #19416
millsp opened this issue Aug 29, 2022 · 70 comments
Closed
Tracked by #19416

[Proposal] Prisma Client Extensions #15074

millsp opened this issue Aug 29, 2022 · 70 comments
Assignees
Labels
kind/feedback Issue for gathering feedback. status/is-preview-feature This feature request is currently available as a Preview feature. team/client Issue for team Client. topic: clientExtensions topic: extend-client Extending the Prisma Client

Comments

@millsp
Copy link
Member

millsp commented Aug 29, 2022

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({ ... })
@millsp millsp added kind/feature A request for a new feature. topic: extend-client Extending the Prisma Client labels Aug 29, 2022
@matthewmueller matthewmueller added team/client Issue for team Client. kind/feedback Issue for gathering feedback. and removed kind/feature A request for a new feature. labels Aug 30, 2022
@mmahalwy
Copy link

mmahalwy commented Aug 30, 2022

This is great! Appreciate starting this. Few things:

  • $* feels a bit out of place. Why not simplify the names and remove $?
  • Part of extensions is being able to extend the types as well. For example, on the Deleted model, there's a kind column. It's a string. I'd like to extend this to be an enum (not a db enum), for example. This also applies for JSON columns, string arrays, etc.

@davidthomasparks
Copy link

This is really dope!

@mishase
Copy link

mishase commented Aug 30, 2022

Check my issue for alternative implementation #14793, I think it is more flexible and easier to use

@millsp
Copy link
Member Author

millsp commented Aug 31, 2022

Hey @mishase thanks for sharing. I was not aware of your proposal. I'm glad to see that we had the same ideals for the API. I definitely agree that the API you showed is leaner. We had a similar candidate when we began our research. Here's why we did not adopt it:

  • In our design, we wanted to be as close as possible to classes. That means one requirement was that you could re-use computed fields in other computed fields. To achieve that, you would simply use this in your object and call a sibling. That implies that field declarations are siblings, and not nested in different objects.
  • Inference is tricky, so it also means that we need to know the keys of the computed fields ahead. Since this is a client-only feature, it means that we need to get TypeScript to do this somehow, because it cannot infer parent contexts withing nest contexts. That said, after digging into it a bit more, I think we can be pretty close from your original idea:
const prisma = new PrismaClient().$extends({
    $result: {
        User: {
            $needs: {
                fullName: {
                    firstName: true,
                    lastName: true,
                },
                fullNameAge: {
                    age: true,
                    firstName: true,
                    lastName: true,
                },
            },
            $fields: {
                fullName(user) {
                    return `${user.firstName} ${user.lastName}`
                },
                fullNameAge(user) {
                    return `${this.fullName(user)} ${user.age}`
                }
            }
        }
    },
})

I guess it is also nice to represent the $needs and the $fields separately and it looks more declarative (like prisma usually is) while being close to your proposal. The main difference is that this API would not let you use get/set. But that is maybe a fair tradeoff?

@mishase
Copy link

mishase commented Aug 31, 2022

Okay, @millsp I got your idea, but I think there's something we should modify in it.

  • Don't use $ in this config. In client we were need to prefix $transaction and other method not to conflict with some user declared models, but here we don't need it as our config won't conflict with model names
  • We should not be as close as possible to classes.
  • Nesting computed fields is easier by referencing another field in $needs. It also type-safe and avoids fetching more data from db after first query. Your implementation makes impossible to know which fields we need to fetch before the callback was executed, so we can't build the proper db query before the callbacks was invoked
  • Merge $needs and $fields to same object. This makes our callback and dependencies as close as possible, so this code is easier to read/modify
const prisma = new PrismaClient().$extends({
  fields: {
    user: {
      fullName: {
        select: {
          firstName: true,
          lastName: true,
        },
        compute(user) {
          return `${user.firstName} ${user.lastName}`;
        },
      },
      fullNameAge: {
        select: {
          age: true,
          fullName: true,
        },
        compute(user) {
          return `${user.fullName} ${user.age}`;
        },
      },
    },
  },
});
  • With this implementation you can build the proper query that needs all data before requesting it from the database and recursively resolve all query dependencies so even async callbacks should work properly

@capaj
Copy link

capaj commented Aug 31, 2022

const extendsAB = prisma
.$extends(extensionA)
.$extends(extensionB)

I like this. I just hope that if I will need to use a method/computed field from extensionA in the code of extensionB, will I be able to do it in typesafe manner? Will that be so @millsp ?

@Akxe
Copy link
Contributor

Akxe commented Aug 31, 2022

const prisma = new PrismaClient().$extends({
    $result: {
        User: {
            $needs: {
                fullName: {
                    firstName: true,
                    lastName: true,
                },
                fullNameAge: {
                    age: true,
                    firstName: true,
                    lastName: true,
                },
            },
            $fields: {
                fullName(user) {
                    return `${user.firstName} ${user.lastName}`
                },
                fullNameAge(user) {
                    return `${this.fullName(user)} ${user.age}`
                }
            }
        }
    },
})

I guess it is also nice to represent the $needs and the $fields separately and it looks more declarative (like prisma usually is) while being close to your proposal. The main difference is that this API would not let you use get/set. But that is maybe a fair tradeoff?

To solve multiple fields on the same model, I would use an array for the model. Every defined property would then have its own needs and results, and one could depend on the other (as long as no circular reference is made...).

const prisma = new PrismaClient().$extends({
    $result: {
        User: [
          {
              $title: 'fullName',
              $needs: {
                    firstName: true,
                    lastName: true,
              },
              $field(user) {
                  return `${user.firstName} ${user.lastName}`;
              },
          },
          {
              $title: 'fullNameAge',
              $needs: {
                    age: true,
                    fullName: true,
              },
              $field(user) {
                  return `${user.fullName} ${user.age}`;
              },
          },
        ],
    },
});

@tobiasdiez
Copy link

I really like the proposal. It looks flexible enough to cover many use cases that are currently not possible to implement in a convenient manner. Nice job!

Only the syntax for defining new fields seems a bit artificial:

const prisma = new PrismaClient().$extends({
  $result: {
    User: (compute) => ({
      fullName: compute({ firstName: true, lastName: true }, (user) => {
        return `${user.firstName} ${user.lastName}`
      }),
    }),
  },
})

I'm right in the assumption that the compute construction is to have type safety? If yes, I would propose to introduce a helper method computedFields that gives you the type information. The philosophy to provide type information through simple helpers served vue very well. The syntax could then be something like:

const client = new PrismaClient().extend({
    result: {
      User: computedFields({
        fullName: {
            needs: ['firstName', 'lastName'],
            compute: (user) => `${user.firstName} ${user.lastName}`
        }
      })
    }
})

A very rude first approximation of an implementation can be found at https://www.typescriptlang.org/play?ssl=27&ssc=3&pln=20&pc=1#code/MYGwhgzhAECqEFMBO0DeAoaXoAcCuARiAJbDQB2YAtggFzQQAuSx5A5tALzQDkP6AX3QAzPOWCNiAe3LRgUqvkYIAJgDFiCECogAeAAoA+ABSsl9API5JMvUYCU9fWiHpGATxwJoVm+TuGXGiY0ADaANL0TCzsALr0AMIKSqoaWioGADTQYOTuhoLo6KzKSMJgwN5JinjK6praugAq2QBqgRhY5AiqEAD89KEA1gjuUsLQTbEh8jXKA9Cm5OaT9lyBrYXooJAw+iwQVGAJJAjkjMHYcrbMeBJSSMb2qEJX+ESk0AgAHsrkKksVqg8IgkPR4MgBI5oPtiIdjqdzmgkAhGHgkLJGAALOFCVzyfwXUCaJHcboAdxhByOJxJjCeADofn8AZ1sCDkPRZil6ukIMY2VcsKIQCAAHLUOiXIUy7q9QY8Sg0HixTIhGVXbm1KXGDlINacQJ6hlKhDqoWvbBQwT2IA (which gives type safety for the implementation of the extension, but needs more ts magic to pass the type information back to the prisma client so that one can get the type of say client.findUser(...).fullName)

@janpio
Copy link
Member

janpio commented Aug 31, 2022

I like the inherent meaning of $result, $model and $client (they extend exactly what they have in their name!), but $use then threw me off as it is now about $use() (which you use to add middlewares to Prisma Client) but queries - $query seems like a good alternative.

@hrueger
Copy link

hrueger commented Aug 31, 2022

Hi all,
I really like the proposal and I think that this is a super-useful feature. Really looking forward to using it.

I'm just wondering: If I add methods to models like here (this is the example from the very first post in this issue):

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

where does the client varaiable come from? Shouldn't this example be updated to

const prisma = new PrismaClient().$extends({
	$model: {
		User: (client) => ({
			async signUp(email: string) {
				await client.user.create(...)
			},
		}),
	}
})

?

@aeddie-zapidhire
Copy link

Reminds me a lot of "the Sequelize way". I think this is a good chunk of effort, but for me I still would still prefer my own model class that uses dependency injection to separates concerns.

const model = new UserModel(prisma.user)

await model.activate()

My experience has been that is an easy pattern to unit test.

@millsp
Copy link
Member Author

millsp commented Aug 31, 2022

Hey @aeddie-zapidhire, we aim to support that too via extensions. If you want you could get a class out of prisma.user as I described here. If it's about extending your results with classes, that will not be possible but perhaps achievable with an extension.

@aeddie-zapidhire
Copy link

@millsp nice. Yeah, I'm not trying to extends Prisma. Prisma just lacks the typings to easily inject a model into a service pattern.

@jgb-solutions
Copy link

I've been waiting for this.

@jacobclarke92
Copy link

jacobclarke92 commented Sep 1, 2022

This is great, phenomenal work. Curious if the api could (or should) allow passing additional args, i.e. "context" to those functions. e.g.

const prisma = new PrismaClient().$extends({
	$model: {
		User: (client) => ({
			async updateEmail(data: Prisma.UpdateUserArgs, context: {actor: User}) {
			        if (data.id !== context.actor.id) throw new Error("You can't do that.")
				await client.user.update(...)
			},
		}),
	}
})

not sure if it makes sense, but just an idea

@chief-austinc the functions themselves (your updateEmail) could have any arguments you want, no?

@hrueger I definitely agree having reference to the client would be essential..
Curious how this would work when using $all.
If it were a function, as suggested, it might actually make sense to have the model's delegate accessible too (if possible).
For example:
(I know soft delete is better solved in other ways, but just as an example).

const prisma = new PrismaClient().$extends({
    $model: {
        $all: (client, modelDelegate) => ({
            async softDelete(ids: number[]) {
                return await modelDelegate.updateMany({ 
                    where: { 
                        id: { in: ids }
                    },
                    data: {
                        deletedAt: new Date(),
                    }
                })
            },
        }),
    }
})

Obviously it'd never be perfect when implementing global functions like above, but having the ability to do something like that would be pretty powerful.
Thoughts?

@hrueger
Copy link

hrueger commented Sep 1, 2022

Imo that sounds great 👍
It should be possible to then add methods conditionally, too, right? e.g.

const prisma = new PrismaClient().$extends({
    $model: {
        $all: (client, modelDelegate) => {
			if (!modelDelegate instanceof LogItem) {
				return {
                    async softDelete(ids: number[]) {
                        return await modelDelegate.updateMany({ 
                            where: { 
                                id: { in: ids }
                            },
                            data: {
                                deletedAt: new Date(),
                            }
                        })
                    },
                };
			}
		},
    }
})

This would add softDelete to all models except LogItem.
Should we require the callback provided to $all to return an object? In that case, I would have to return {} at the end of the function. Maybe it should work with void, null and undefined, too and not throw an error. What do you think?

@Akxe
Copy link
Contributor

Akxe commented Sep 1, 2022

This would add softDelete to all models except LogItem. Should we require the callback provided to $all to return an object? In that case, I would have to return {} at the end of the function. Maybe it should work with void, null and undefined, too and not throw an error. What do you think?

This does require you to have every table the same columns id and deletedAt, the typeseafty for this might be even impossible. Maybe a function that can wrap every function would do the same in your case, but I do not see how many people would use this...

@millsp
Copy link
Member Author

millsp commented Nov 29, 2022

Hey everyone! I wanted to let you know that we have officially shipped Prisma Client Extensions in our latest 4.7.0 release. Head to the release notes to learn how to get started and follow the relevant documentation links. Please let us know what you think at the preview feature feedback issue. And also, a very special thanks to everyone who commented in this issue and contributed to making this feature a better one ❤️ Thanks!

@adrian-goe
Copy link

adrian-goe commented Nov 29, 2022

Hi @millsp how dose this extends work with a class? This is in context of prisma in nestJS.

import { OnApplicationShutdown } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';

export class PrismaService extends PrismaClient implements OnApplicationShutdown {
  constructor() {
    super();
  }

  async onApplicationShutdown(): Promise<void> {
    await this.$disconnect();
  }
}

Do I even use $extend for client extensions?
How would it look like for other types of extensions?

EDIT:

Also, there is always mentioned // sparing you the details:

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

But in the end, my question is how do i start a callback free Transaction? surly not just with $queryRawUnsafe('BEGIN;')

@millsp
Copy link
Member Author

millsp commented Nov 29, 2022

Hey @adrian-goe! I think the best for this one would be to publish an extension for everyone to enjoy it. What I did is a just ported code that I wrote a while ago and put it into an extension. What does it look like:

  1. Create a new repository for this package and initialize the project
  2. Install the @prisma/client as a dependency (no schema needed)
  3. Create the extension with the following code I wrote before
  import { Prisma } from "@prisma/client"

  type TxClient = {} // we don't have better types
  const ROLLBACK = { [Symbol.for('prisma.client.extension.rollback')]: true }

  async function $begin<T extends object>(this: T) {
      let setTxClient: (txClient: TxClient) => void
      let commit: () => void
      let rollback: () => void

      // a promise for getting the tx inner client
      const txClient = new Promise<TxClient>((res) => {
          setTxClient = (txClient) => res(txClient)
      })

      // a promise for controlling the transaction
      const txPromise = new Promise((_res, _rej) => {
          commit = () => _res(undefined)
          rollback = () => _rej(ROLLBACK)
      })

      // opening a transaction to control externally
      if ('$transaction' in this && typeof this.$transaction === 'function') {
        const tx = this.$transaction((txClient: any) => {
            setTxClient(txClient as TxClient)

            return txPromise.catch((e) => {
                if (e === ROLLBACK) return
                throw e
            })
        })

      return Object.assign(await txClient, {
          $commit: async () => { commit(); await tx },
          $rollback: async () => { rollback(); await tx }
      } as T & { $commit: () => Promise<void>, $rollback: () => Promise<void> })
    }

    throw new Error('Transactions are not supported by this client')
  }

  export default Prisma.defineExtension({ client: { $begin } })
  1. Publish your extension as prisma-extension-callback-free-itx
  2. In your code, import the extension then load it into your client via $extends

(Publishing is completely optional of course, you could also put it in a file in your local project)

@Jackman3005
Copy link

Hey all, thanks for the work you've been putting in on the Prisma client extensions and Prisma as a whole 🙏. I think it's awesome the direction you're heading in and the thought you're putting into these things.

Extending a result with a computed field that needs data from a related model

I noticed that the when defining a result extension we are limited to scalar fields of the owning entity. We were really hoping to move an n+1 problem for computed fields into the prisma extension, but some of the needs data for the field comes from a relationship off the main entity. Here's an idea of what we're trying to do:

(Note: this code does not compile b/c quest is not a scalar field whereas questId is)

const xprisma = new PrismaClient().$extends({
  result: {
    questRun: {
      displayName: {
        // the dependencies
        needs: {
          status: true,
          name: true,
          startedAt: true,
          quest: {
            select: {
              showDateInRunName: true,
              timezone: true,
            },
          },
        },
        // the computation logic
        compute(questRun) {
          if (questRun.quest.showDateInRunName && questRun.startedAt) {
            const formattedDate = new Date(
              questRun.startedAt!
            ).toLocaleDateString(undefined, {
              weekday: "long",
              year: "numeric",
              month: "short",
              day: "numeric",
              timeZone: questRun.quest.timezone,
            });
            return `${questRun.name} - ${formattedDate}`;
          } else {
            return questRun.name;
          }
        },
      },
    },
  },
});

Why use the extension for this?

Some of the reasons we were looking at Prisma to do this is that we were hoping to have a way of querying the additional data in needs only when we are asking for displayName and also that that additional data would not be present on the resulting model object (unless it was also selected specifically when querying).

Why not roll our own?

We have many areas in our app where we want to retrieve multiple models with this computed displayName but are also sending the exact response given from Prisma back to the front-end. We were hoping to not have to add the needed fields in the initial query, add the computed field afterwards (and ensure typings), then somehow determine if we are supposed to remove or leave the needs fields depending on if they aren't intended to be there in the response or not.

TL;DR our alternatives require a relatively substantial increase in boilerplate that we otherwise haven't needed to date while introducing more opportunities for mistakes

Is our problem common?

Something could probably be said about simplifying our data structures to keep all the necessary data for our computed field within one table, but I think there will always be exceptions or good reasons for it to not be that way. Of course querying a relationship would have a performance impact, but we already have to suffer that performance impact when not using the prisma extension.

Is this by design?

Was it by design and strong intention to disallow using fields from related models in the needs of the computed field? Or was this chosen for technical/complexity-based reasons? Or some other reason I have not yet thought of?

Thanks for getting this far in my monologue and apologies if this has been answered before. I did skim over the comments in this thread, but didn't see this particular topic under discussion.

Cheers!
Jack (& The Questmate Team 🚀)

@mcrowe
Copy link

mcrowe commented Jan 6, 2023

I'd like to argue that this proposal is a big mistake. You are taking on a lot of complexity in Prisma that doesn't need to be there and will slow down future development. All of the use-cases above can be solved at a higher level, and arguably they should. We went through the "fat models" phase with Rails years ago and collectively decided that it quickly got out of hand. This type of logic fits well into higher-level functional services (not necessarily "controllers").

Please don't take pressure from the community to solve all use-cases within Prisma, and don't take on this complexity. I'd encourage Prisma to focus on being an amazing abstraction to the data layer and not trying to be more.

@Mahi
Copy link

Mahi commented Jan 6, 2023

I have to agree with @mcrowe, it's much better for Prisma to do one thing and do it well, than to try and do everything anyone might ever need. Go down this path and you'll be competing with Nest.js before you notice.

@millsp
Copy link
Member Author

millsp commented Jan 9, 2023

Hey @Jackman3005, thanks for your thorough proposal

Was it by design and strong intention to disallow using fields from related models in the needs of the computed field? Or was this chosen for technical/complexity-based reasons? Or some other reason I have not yet thought of?

No strong intention. We have discussed it but decided not to implement this for this first version. The consensus was that if you start selecting relations, we should treat the computed field as a relation too, otherwise one could generate too many queries too easily. In other words, if your computed field depends on a relation, it should be included or selected, and it won't be selected by default. Whether that is/can be efficient on the type-system is not known yet, and will drive this as we also want to maintain a good DX. We will get to it before GA.

In the mean time, I think you could make compute async and do an extra query in there. You'll need to await your result field though. Hope that helps!

@millsp
Copy link
Member Author

millsp commented Jan 9, 2023

Hey @mcrowe @Mahi Thanks for your feedback.

Please don't take pressure from the community to solve all use-cases within Prisma, and don't take on this complexity

Our community has many needs that come from different angles and for many different reasons. There is a real pressure there and we want to help. So for us extensions is a way to make that possible. We don't want to fix every single use-case, but by providing an API that is generic enough, we can enable our community to solve these by themselves.

All of the use-cases above can be solved at a higher level, and arguably they should

I agree with you that some of these use cases can be solved in Prisma itself, and do deserve a first-class feature. Extensions don't remove the need for first-class features, and we will continue working on shipping features and improvements. On the other hand, some valuable community use-cases also don't belong in the ORM and are better in a local extension and maybe even as an npm package (not within Prisma).

We went through the "fat models" phase with Rails years ago and collectively decided that it quickly got out of hand. This type of logic fits well into higher-level functional services (not necessarily "controllers").

I agree, however this feature isn't designed to be opinionated, and it is for you to take it where you need to and integrate patterns as you wish.

I'd like to argue that this proposal is a big mistake. You are taking on a lot of complexity in Prisma that doesn't need to be there and will slow down future development

Retrospectively, after implementing the feature, I don't think that it will slow down future development. Internally, extensions are a layer on top of JS and TS and are well separated. I think that by far, the most complex part was result (aka computed fields) but also a very requested feature, so definitely worth doing. All other components (model, client, query) were quite straightforward and introduced very little complexity.

While our views on the topics obviously differ, I do agree with on some of your points and wanted thank you for your feedback.

@KATT
Copy link

KATT commented Jan 13, 2023

Questions

1. Is is possible to use Prisma.validator together with client extensions?

I always define a select: when doing queries (because otherwise we get the equivalent of select *), and want to do the equivalent to the below but with my default prisma client:

const postSelect = Prisma.validator<Prisma.PostSelect>()({
  // ..
})

2. Will it be possible to override fields?

I strongly dislike that Prisma return Date-instances, especially for plain dates and we're using mapper functions to create Temporal.PlainDate instead.

Will it be possible to do the following?:

client.$extends({
      result: {
        user: {
          needs: { createdAt: true },
          compute(it) {
            return toPlainDate(it.createdAt);
          },
        },
      },
});

2. Issue

Another thing, I tried enabling Prisma client extensions and did a simple extension:

client.$extends({
    result: {
      user: {
        fullName: {
          needs: { firstName: true, lastName: true },
          compute(it) {
            return (
              [it.firstName, it.lastName].filter(Boolean).join(' ') || null
            );
          },
        },
      },
    },
  })

But I immediately hit "Maximum call stack size exceeded" as a side-effect.

Got quite a big app that I'm testing it on. Can't share source.

@jove4015
Copy link

But I immediately hit "Maximum call stack size exceeded" as a side-effect.

Got quite a big app that I'm testing it on. Can't share source.

There's a known issue: #16600

@millsp
Copy link
Member Author

millsp commented Jan 16, 2023

Hey @KATT 👋, nice to see you here!

  1. I think we have everything we need to do this, but it's not possible right now. Could you please open an improvement request?
  2. Yes, field overrides and augmentation like you showed is something we support but is currently broken (call stack issue). In the upcoming 4.9.0, we have fixed call stack issues with tsc as well as runtime issues.

@mellson
Copy link

mellson commented Jan 16, 2023

Hey @KATT 👋, nice to see you here!

  1. I think we have everything we need to do this, but it's not possible right now. Could you please open an improvement request?
  2. Yes, field overrides and augmentation like you showed is something we support but is currently broken (call stack issue). In the upcoming 4.9.0, we have fixed call stack issues with tsc as well as runtime issues.

Thank you for the info 👍🏻 I was able to fix our call stack issues using 4.9.0-dev.69.

But now I'm getting some errors because it looks like all the GetPayload type helpers have been removed.
Do you have any clues as to what we should use instead?

@millsp
Copy link
Member Author

millsp commented Jan 16, 2023

Hey @mellson, thanks for the feedback. Which errors are you seeing? (DM'd you on our public slack). Will work on a fix.

@mellson
Copy link

mellson commented Jan 16, 2023

Hey @mellson, thanks for the feedback. Which errors are you seeing? (DM'd you on our public slack). Will work on a fix.

Thanks, I'm scrambling to remember which email I used to create my Slack account (it has been a while since I used Slack 😅)

@mellson
Copy link

mellson commented Jan 17, 2023

Hey @mellson, thanks for the feedback. Which errors are you seeing? (DM'd you on our public slack). Will work on a fix.

Thank you for the quick fix; 4.9.0-dev.70 fixes the TypeGetPayload problems.

@kdawgwilk
Copy link

This approach seems really cool at first glance but how does it fit in with other OOP style frameworks such as NestJS? Given the example above

import { OnApplicationShutdown } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';

export class PrismaService extends PrismaClient implements OnApplicationShutdown {
  constructor() {
    super();
  }

  async onApplicationShutdown(): Promise<void> {
    await this.$disconnect();
  }
}

How do I $extend this client/service? I keep coming back to the delegate pattern mentioned in this thread, if prisma just provided us with better type mappings to allow us to create our own generic type-safe abstractions it would allow the community to provide Repository pattern libraries that interop with the prisma clients and the prisma team would not have to maintain those libraries. I have been using been using the wonderful generator library from @johannesschobel https://github.com/prisma-utils/prisma-utils/tree/main/libs/prisma-crud-generator but its painful to manage the generated code. We keep going back and forth on checking the code into source control vs not. Before I used this codegen I had a generic mixin base class

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type NoInfer<T> = [T][T extends any ? 0 : never]

export interface IPrismaCrudDataService<Model> {
  prisma: PrismaService

  create<CreateInput = 'please provide generic arg'>(args: NoInfer<CreateInput>): Promise<Model>

  one<WhereUniqueInput>(where: NoInfer<WhereUniqueInput>): Promise<Model>

  all<WhereInput = 'please provide generic arg'>(where: NoInfer<WhereInput>): Promise<Model[]>

  some<Where, OrderBy, Cursor>(
    where: NoInfer<Where>,
    orderBy?: NoInfer<OrderBy>,
    cursor?: NoInfer<Cursor>,
    take?: number,
    skip?: number
  ): Promise<Model[]>

  update<UpdateInput = 'please provide generic arg'>(id: string, input: NoInfer<UpdateInput>): Promise<Model>

  delete(id: string): Promise<Model>
}

export const PrismaCrudDataService = <Model>(model: keyof PrismaClient): Type<IPrismaCrudDataService<Model>> => {
  @Injectable()
  class CrudDataServiceHost {
    @Inject(PrismaService) prisma: PrismaService

    async create<CreateInput>(args: CreateInput): Promise<Model> {
      // @ts-expect-error This is a bit too dynamic to figure out
      return this.prisma[model].create({ data: args })
    }

    async one<WhereInput>(where: WhereInput): Promise<Model> {
      // @ts-expect-error This is a bit too dynamic to figure out
      return this.prisma[model].findFirst({ where })
    }

    async all<WhereInput>(where: WhereInput): Promise<Model[]> {
      // @ts-expect-error This is a bit too dynamic to figure out
      return this.prisma[model].findMany({ where })
    }

    async some<Where, OrderBy, Cursor>(
      where: Where,
      orderBy?: OrderBy,
      cursor?: Cursor,
      take?: number,
      skip?: number
    ): Promise<Model[]> {
      // @ts-expect-error This is a bit too dynamic to figure out
      return this.prisma[model].findMany({
        where,
        orderBy,
        cursor,
        take,
        skip,
      })
    }

    async update<UpdateInput>(id: string, input: UpdateInput): Promise<Model> {
      // @ts-expect-error This is a bit too dynamic to figure out
      return this.prisma[model].update({ where: { id }, data: input })
    }

    async delete(id: string): Promise<Model> {
      // @ts-expect-error This is a bit too dynamic to figure out
      return this.prisma[model].delete({ id })
    }
  }

  return mixin(CrudDataServiceHost)
}

And it was used like this:

@Injectable()
export class UserDataService extends PrismaCrudDataService<User>('user') {}

One of the things you will notice is the hacky NoInfer type that was basically added to the interface to require devs to put a generic type on all method calls because there was no way for me to extrapolate those types generically from the prisma client. It would be great if there was a flag to pass to the prisma client generator to include type mappings we could use to create our own generic abstractions. I imagine a world where I can write something like (heavily inspired by @krsbx #5273 (comment)):

import { ModelTypes } from '@prisma/client'

export const PrismaCrudDataService = <ModelName extends keyof PrismaClient>(model: ModelName):
  @Injectable()
  class CrudDataServiceHost {
    @Inject(PrismaService) prisma: PrismaService

    async create(args: ModelTypes[ModelName]['Create']): Promise<ModelTypes[ModelName]['Model']> {
      return this.prisma[model].create({ data: args })
    }
  }
}

And I could use it like so

@Injectable()
export class UserDataService extends PrismaCrudDataService('user') {}

Also issue seems to be very similar and summed up for just the WhereInput case in this issue #6980

@krsbx
Copy link

krsbx commented Jan 22, 2023

Base on the @kdawgwilk input that shows we need a type definition so we can just re-use it if some of us want to use a repository pattern or more OOP like approaches. Based on my implementation in #5273 (comment), I need to create some sort of a types that will contains the types that we need for anyone who want to use Prisma in a Repository Pattern or OOP like pattern. This kind of implementation is some sort of a band aid but could break if Prisma implementation changes on how the types are being produces.

To fix this issue, I try to create a Prisma generator that will generate all the necessary types while the model is being created by generator. This package can be found in this github repository. But still this led to some kind of problem since we are not really sure about the return type that we have based on the create, update or any functions that could return not only one but two or model with an include options.

So, to fix this kind of Issue I believe if we have a new type definition that allow a use case of Generic this kind of problem will allow the developer to extend the use case of Prisma much better and of course type safety. The implementation that I use in my generator could be improve if there's a model types that allow Generic in the first place.

@peace-duro
Copy link

Client extensions look powerful and could apply to advanced use cases. However it's adding a lot of complexity to an already complex code base. Moreover, this is the type of work that an API should do, not the ORM. What will inevitably happen is that business logic will get munged and spread between the API and ORM, making it harder to understand and maintain. The only way that could be avoided is if the entirety of the API could be implemented in the ORM. That isn't feasible even with client extensions, and attempting to do so would be in vain.

@janpio janpio added the status/is-preview-feature This feature request is currently available as a Preview feature. label Mar 27, 2023
@anton-g
Copy link

anton-g commented May 26, 2023

Questions

1. Is is possible to use Prisma.validator together with client extensions?

I always define a select: when doing queries (because otherwise we get the equivalent of select *), and want to do the equivalent to the below but with my default prisma client:

const postSelect = Prisma.validator<Prisma.PostSelect>()({
  // ..
})

We got a similar (identical?) issue where we use Prisma.validator to reuse includes:

const animalWithOwner = Prisma.validator<Prisma.AnimalArgs>()({
  include: {
    owner: true,
  },
})

And maybe a sidenote, but it would also be good to have a recommended solution for how to get the types with computed fields. For example from this extended client:

new PrismaClient().$extends({
    result: {
      animal: {
        computedState: {
          needs: {},
          compute(animal) {
            return "computed"
          },
        },
      },
    },
  })

I would like to avoid doing this manually:

import { Animal as PrismaAnimal } from '@prisma/client'

export type Animal = PrismaAnimal & {
  computedState: string
}

@millsp
Copy link
Member Author

millsp commented May 31, 2023

Hey, thanks for all your feedback. Receiving your comments has been really valuable. We will be closing this issue, as we created it mostly to design the feature. That said, we understood that many of you are interested in further improvements, so we are now tracking these in the following issues:

If we missed anything, make sure to create an issue. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
kind/feedback Issue for gathering feedback. status/is-preview-feature This feature request is currently available as a Preview feature. team/client Issue for team Client. topic: clientExtensions topic: extend-client Extending the Prisma Client
Projects
None yet
Development

No branches or pull requests