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

Extend Prisma Client #7161

Closed
4 tasks
Tracked by #19416
matthewmueller opened this issue May 19, 2021 · 35 comments
Closed
4 tasks
Tracked by #19416

Extend Prisma Client #7161

matthewmueller opened this issue May 19, 2021 · 35 comments
Assignees
Labels
kind/feature A request for a new feature. 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
Milestone

Comments

@matthewmueller
Copy link
Contributor

matthewmueller commented May 19, 2021

Problem

Right now we don't provide guidance on how use the Prisma Client within a higher-level model or domain.

Use cases

1. Adding a computed field: The canonical example is getting a full name from a first and last name. We also have some use cases in: #3394 and #5998

2. Multi-step actions: Right now people put all their Prisma Client calls inside their GraphQL resolvers or RESTful controllers. This leads to "fat controllers" that are hard to test and lead to code duplication across controllers. The reason is because the Prisma client is often lower-level than your business domain and expressing your business logic requires multiple steps.

Some Examples:

  • team.signup: signing up for Slack requires you to create a user and a team at the same time
  • document.update: when you update a document, you also update its search index and fire off an email to subscribers

From a top-level view of your application, you'd consider these to be a single action (e.g. team.signup and document.update), but they break down into multiple steps. We should have a way to turn multiple steps into single actions without limiting the rich capabilities of the Prisma Client API. #5258 #5258

3. Custom Validation: Validate fields before they hit the database. Using the example above, for a team.signup(team_name, owner_email) action, you can validate that the owner's email address is valid and the team name doesn't have any special characters before you go to the database. Lots more use cases in: #3528

Existing Solutions

  • Middleware: Is one way to solve the multi-step actions triggering other actions, but you wouldn't really want to trigger other Prisma Client actions within this middleware. It's also global configuration for the entire instance of the client. Models can be more flexible.
  • Doing it in your resolver or controller: This works fine for simple applications. When you start sharing this logic, you need a way to abstract shared business logic into a model.

Prior Art

In Rails you have a difference between Active Record and the Active Record Query Interface. Prisma Client is an awesome Active Record Query Interface but Active Record is currently DIY. We should also have a story for some of the Active Record use cases described above.

Suggested Next Steps

  • Create an example script with the Prisma Client we have today demonstrating the use cases above. E.g. A model with computed fields, multi-step actions and custom validation.
  • Do we lose the rich querying capabilities of the Client? What can we do about that? Can we have both? If so, how could we have both?
  • How easy was this script to write with our existing generated type definitions? Anything we can do to improve this common workflow?
  • Document how to do currently this so we can point users to this documentation whenever they ask about it. Create new issues for larger future improvements.

What I'd like to avoid: Going too deep into implementing a solution before understanding how one would do this with today's Prisma Client in a hacky way perhaps.

Related

@matthewmueller matthewmueller added topic: extend-client Extending the Prisma Client team/client Issue for team Client. process/candidate kind/improvement An improvement to existing feature and code. labels May 19, 2021
@matthewmueller matthewmueller changed the title Wrap the Prisma Client Extend the Prisma Client May 19, 2021
@matthewmueller matthewmueller changed the title Extend the Prisma Client Provide an example for how to extend the Prisma Client May 19, 2021
@matthewmueller matthewmueller changed the title Provide an example for how to extend the Prisma Client Explore how to extend the Prisma Client May 19, 2021
@tobiasdiez
Copy link

tobiasdiez commented May 19, 2021

Especially computed fields are also related to #5998, which would provide a very easy and flexible way to implement them.

@matthewmueller matthewmueller added this to the 2.24.0 milestone May 19, 2021
@matthewmueller
Copy link
Contributor Author

@tobiasdiez that's a good one. Added that to the Related list. Thanks!

@josh-hemphill
Copy link

This might also be related: #6759
Or more generally, orchestrating your app's initialization/setup as it relates to the database.

@matthewmueller
Copy link
Contributor Author

matthewmueller commented May 20, 2021

Might be! I could see a use-case for two Prisma Clients in one higher-level model. One with read access, one with write access.

@yasaichi
Copy link

It seems to me that prisma/ent could deal with the usecase 1 and 2. Why don't you re-start from it?

@Newbie012
Copy link

Related - #6349

@ysfaran
Copy link

ysfaran commented Oct 24, 2021

Are there already plans to implement this soon?

@brianreeve
Copy link

brianreeve commented Oct 25, 2021

Seconded.

I've been evaluating Node ORMs supporting MySQL for a couple weeks. I was turned on to Prisma because it seems to provide fantastic tooling orbiting around a central schema definition such as generating and managing migrations.

What seems to be lacking is generating extendable model classes which encapsulate the data representation. Typically (my personal experience across various languages), you'd see ORM tooling generate base classes with accessors for each column. Instances of which represent rows in the database. Then, developers can extend those model classes with additional functionality.

The use cases are many as you've already pointed out, but here is a list of ones I commonly face:

  • Validation
  • Manipulate data on read or write: sanitization, normalization, encryption/decryption
  • Non-DB functionality - eg. read/write a user's avatar in cloud storage
  • Do multiple things - eg. archive user - copy data to static storage, then delete from DB, then notify someone

Fat controllers don't cut it because objects are often manipulated in multiple contexts: CLI, MVC controllers, REST API actions, etc. and you likely want to encapsulate coupled functionality right at the model level for DRY reasons.

I briefly looked into documentation and extending the client to achieve equivalent results, but there isn't even any tangible code representing the "Active Record". It seems that dynamically creates objects based on schema rather than providing a base class per table.

Can we at least get an estimate on some documentation which explains how to achieve this, and the pros/cons? Perhaps this would accomplish your first bullet:

Create an example script with the Prisma Client we have today demonstrating the use cases above. E.g. A model with computed fields, multi-step actions and custom validation.

I have limited knowledge of the inner workings, but can we hook into the client to tell it how to serialize/deserialize data per table? That way we could write our own classes and pass around instance of them as long as the Prisma client can understand how to handle them. We'd probably have to write some boilerplate (accessors mostly) for each, but that's a reasonable expectation and can be solved with tooling.


Update:

prisma/ent#1

This is exactly it but prisma/ent looks like a rough POC and is 3.5 years stale. However, it also seeks to solve another problem... extending what that repo terms "repositories".

Is this type of thing on the roadmap?

@matthewmueller
Copy link
Contributor Author

matthewmueller commented Oct 26, 2021

Hey @brianreeve thanks for sharing your use cases. This issue is definitely still a priority for our team, but we're still a small team of engineers with lots of other highly-requested features too.

I still think a guide or example would be a nice first step. Thanks for bumping this one up again.

@nahtnam
Copy link

nahtnam commented Oct 28, 2021

I'm not sure exactly what you want to see in an example, but for testing I would love to have a "seeding" system. Something like this:

import { BaseEntity } from 'prisma'

export function createFactory<Model, Entity extends BaseEntity<Model>>(
  model: Entity,
  defaultArgs: (args?: Partial<Entity["CreateInput"]>) => Promise<Entity["CreateInput"]>,
) {
  return {
    async create(args?: Partial<CreateInput>) {
      const defArgs = await defaultArgs(args);
      return model.create({
        data: {
          ...(defArgs as unknown as Model),
          ...args,
        },
      }) as Model;
    },
    async createMany(args?: Array<Partial<CreateInput> | null>) {
      // ...
    },
  };
}

Then I'd be able to use it like this:

import {Some} from 'prisma';

export const SomeFactory = createFactory(
  prisma.Some,
  () => ({
    name: faker.lorem.word(),
  }),
);

And finally use it in tests like this:

const newSome = SomeFactory.create()

@smolattack
Copy link

smolattack commented Oct 29, 2021

Just to expand on what @Newbie012 wrote, it would be useful to be able to add your own opaque types/smart constructors.

An example of how that might work has been suggested in #6349 (comment)

@tobiasdiez
Copy link

@matthewmueller can we as a community help with the examples explaining the status quo. If yes, in which form do you prefer these, e.g comments here or as a external repository?

@matthewmueller
Copy link
Contributor Author

matthewmueller commented Nov 2, 2021

Yes, that would be great! The way I'm thinking about this is a realistic, yet minimal example. Perhaps a signin form where you need to take multiple actions or responding to a github comment that sends out emails. Ideally framework agnostic, but a well-understood framework like Express would be fine too.

Just looking to put together some patterns of higher-level models to see where we fall short and provide ideas on how to improve the Client to support these use cases.

If you or anyone else wants to create an example repo of how it currently would work today and highlight the pains, we can collect these examples and work towards addressing pain points.

@matthewmueller matthewmueller added this to the 3.5.0 milestone Nov 2, 2021
@matthewmueller
Copy link
Contributor Author

We added some new documentation on how to build some of these use cases yourself:

We do plan to implement these features into the Prisma Client eventually, but this is meant to help you along in the meantime. If you'd like to chat about any of these topics or have any questions, feel free to book some time with me.

@puchm
Copy link

puchm commented Feb 1, 2022

I have been following this discussion for some time and I would like to contribute my thoughts.

I've been trying to build a Prisma Middleware (using prisma.$use) that basically tracks changes to the database. While that is a rather complex topic by itself I found that possibly the hardest thing to do was to handle all of the cases how Prisma could possibly modify something. The possibility of nesting queries introduces a lot of cases that (considering you'd want to do it properly) you all need to cover. While it's probably very much debatable if the project I tried to pull off is in the correct spot in the Prisma Middleware (would probably be better doing this at a lower level). I still think it showed me what Prisma really needs (in my opinion).

What I would love to see would be hooks based on models and operations that would also get called if the operation is nested.

E.g. it would be cool if you could do

prisma.$foo.user.onCreate((data, select, include, next) => {
    // Do something, perhaps modify the data

    const result = next(data, select, include);

    // Possibly modify the result, e.g. adding computed fields
    // select or include could also be used to check if a computed field needs to be computed

    return result
})

As with the Prisma Middleware you could perform changes to the data that gets inserted, modify the result or you could even make additional calls to Prisma in the Middleware. The only difference is that you don't need to worry about covering every possible case of the query structure. Additionally, the typings for these hooks could be part of the Prisma client so it would be a lot more type safe than todays prisma.$use.

Some other things that would generally help would be:

  • custom directives for both models and fields
  • finding a good way for hooking into the client generation and migration. There should be a possibility of doing both separately so that you could
    • add custom columns into the database (hidden from the client) that help you compute computed fields
    • add computed fields that don't have an actual column in the database
    • possibly add models

Regarding the second point there should probably also be some predefined directives that make a field either client-only or database-only but I do think it would be best if you could also do this programmatically. This would also allow you to add fields to either all models programmatically or only some of them, e.g. based on the model directives I mentioned before.

I have no idea if any of this is feasible at all. To my understanding, much of the Prisma client is not actually written in Typescript so this could potentially come at a performance cost. Also this is obviously something that would require major work so for now it will remain a fantasy for me.

@deodad
Copy link

deodad commented Feb 1, 2022

My team has been using Prisma for a year now. Our use case is complex and involves many of the use cases mentioned here and then some.

In the JS/TS ecosystem there are multiple strong opinionated ORMs with plugins for just about everything. We specifically use Prisma for its simple API and strong types which lends it well to a functional paradigm.

Documentation on how to compose the Prisma client with other specialized libraries for things such as validation is a great direction. There are some things that could improve the composability of the client. Outside that I'd prefer the core team to keep delivering features specific to managing and interacting with the database than make an ActiveModel style ORM that tries to do it all.

@tobiasdiez
Copy link

@tobiasdiez, would love to learn more about why some of these solutions (especially computed fields) are suboptimal. Do you mind sharing a bit more?

Adding helper methods that compute additional fields works well to add one or two fields. But in a more complex project, you end up with a lot of helper methods, that devs have to remember, import separately, etc. It would be nice if one could create a custom model class and then say "Prisma please use this class as the model for users". Something like the following rewrite of the example from https://www.prisma.io/docs/concepts/components/prisma-client/computed-fields:

import User as PrismaUser from prisma

// Somewhere add a config that prisma automatically wraps all methods normally returning `User` with this class 
class User extends PrismaUser {
    constructor(private user: PrismaUser) {
         // So that methods/properties defined on `PrismaUser` work
         super(user)
   }

    function computeFullName(): string {
         return this.user.firstName + ' ' + this.user.lastName,
    }
}

async function main() {
  const user = await prisma.user.findUnique({ where: 1 })
  user.computeFullName() // This now works, because `findUnique` returns a `User` and not a `PrismaUser`
  user.firstName // Still works
}

Currently one can achieve something like this using Objects.assign and manually wrapping return values of prisma functions. But this becomes tiresome and complicated.

Alternatively, if prisma would return a class User (instead of an object with type User), then one could use module augmentation to get

import { User } from "prisma"
declare module "prisma" {
  interface User {
    computeFullName(): string
  }
}
User.prototype.computeFullName = function () {
  return this.user.firstName + ' ' + this.user.lastName,
};

async function main() {
  const user = await prisma.user.findUnique({ where: 1 })
  user.computeFullName() // This now works, because we extended `User` 
  user.firstName // Still works
}

@yasaichi
Copy link

yasaichi commented Feb 14, 2022

@tobiasdiez
Your idea is just what I needed before, however, I found out that class-based ORM introduced another problem: type safety.
In the 2nd example of your comment, if you fetch users that don't have firstName or lastName using the select option, you'll get an unexpected result from User.prototype.computeFullName. Unfortunately, you can't notice that before running the code.
That's why now I don't think introducing class into Prisma is best to deal with the use cases above.
By the way, if you just want to use the syntax object.method, Bind-this operator (currently stage-1 proposal) could help you.

import { User } from "@prisma/client"

function computeFullName(this: Pick<User, "firstName" | "lastName">) {
  return this.firstName + ' ' + this.lastName,
};

async function main() {
  const user = await prisma.user.findUnique({ where: { id: 1 } });
  user!::computeFullName()
}

@tarazena
Copy link

I'm thinking we should use something like Firebase does when fetching data from their store. In Firsbase we use withConverter to cast the result to different instances e.g.

class Post {
  constructor(readonly title: string, readonly author: string) {}

  toString(): string {
    return this.title + ', by ' + this.author;
  }
}

const postConverter = {
  toFirestore(post: Post): firebase.firestore.DocumentData {
    return {title: post.title, author: post.author};
  },
  fromFirestore(
    snapshot: firebase.firestore.QueryDocumentSnapshot,
    options: firebase.firestore.SnapshotOptions
  ): Post {
    const data = snapshot.data(options)!;
    return new Post(data.title, data.author);
  }
};

const postSnap = await firebase.firestore()
  .collection('posts')
  .withConverter(postConverter)
  .doc().get();
const post = postSnap.data();
if (post !== undefined) {
  post.title; // string
  post.toString(); // Should be defined
  post.someNonExistentProperty; // TS error
}

One thing i found out difficult to build is dynamic typing, if we started including different entities in queries, we need a way to use the types more easily rather than building them out manually e.g.

 const uses = await prisma.user.findUnique({where : { id }, include: { posts: true, comments: true }});
/* 
Prisma here knows that the user type is going to be User & { posts: Post[]; comments: Comment[] } but there is not an exported type by default so we have to build those/
*/

type UserWithCommentsAndPosts = User & { posts: Post[]; comments: Comment[] };

const findPostWithComment (user: UserWithCommentsAndPosts) => ....

doc for FirebaseDataConverter

@mattrabe
Copy link

I'm using the custom model approach to add "computed fields" to my prisma models.

In a nutshell I'm "extending" the find & create methods that prisma provides, in order to inject my computed fields. This works, but my biggest complaint is that I was unable to reuse the method names - so I'm not actually extending findUnique() - I'm providing a new method named find() that uses findUnique() under the hood. Using prisma's findUnique() inside of my findUnique() appears to create an infinite loop... There's also more boilerplate than I'd love.

I think it would be great if custom classes/methods could be passed to new PrismaClient(). That would provide maximum flexibility, while still defaulting to all of the existing prisma functionality if nothing is passed. I do not think prescribing this stuff in the schema file would be a good approach. This could be something like:

export default new PrismaClient({
  user: {
    findUnique: (user: User): User => doWhateverToUser(user)
  }
})

or:

export default new PrismaClient({
  user: {
    beforeFindUnique: (args: Prisma.UserFindUniqueArgs): Prisma.UserFindUniqueArgs => doWhateverToArgs(args)
    afterFindUnique: (user: User): User => doWhateverToUser(user)
  }
})

or if just for computed fields:

export default new PrismaClient({
  user: {
    computedFields: (user: User): User => applyComputedFieldsToUser(user)
  }
})

Current Use Example:

Currently using this setup in a monorepo.

packages/orm:

schema.prisma:

generator client {
  provider = "prisma-client-js"
  binaryTargets = [ "native", "rhel-openssl-1.0.x" ]
}

datasource db {
  provider = "mysql"
  url = env("DATABASE_URL")
}

model User {
  id                String          @id @default(uuid())
  email             String          @unique
  password          String?
  firstName         String
  lastName          String
  createdAt         DateTime        @default(now())
  updatedAt         DateTime?       @updatedAt
  deletedAt         DateTime?
}

models/user/types.ts:

import { User as PrismaUser } from '@prisma/client'

export interface User extends PrismaUser {
  name: string,
}

models/user/index.ts:

import {
  Prisma,
  PrismaClient,
  User as PrismaUser,
} from '@prisma/client'

import { User } from './types'

const userModel = (model: PrismaClient['user']) => ({
  add: async (args: Prisma.UserCreateArgs): Promise<User> => {
    const user = await model.create(args)

    return applyComputedFields(model)(user)
  },

  find: async (args: Prisma.UserFindUniqueArgs): Promise<User|undefined> => {
    const user = await model.findUnique({
      ...args,
      include: {
        teams: { include: { team: true } },
        ...args.include,
      },
    })

    if (!user) {
      return undefined
    }

    return applyComputedFields(model)(user)
  },

  get: async (args: Prisma.UserFindManyArgs = {}): Promise<User[]> => {
    const users = await model.findMany({
      ...args,
      include: {
        teams: { include: { team: true } },
        ...args?.include,
      },
    })

    return Promise.all(users.map(applyComputedFields(model)))
  },

  name: (user: PrismaUser): string => `${user.firstName}${(user.firstName && user.lastName && ' ') || ''}${user.lastName}`,
})

const applyComputedFields = (model: PrismaClient['user']) => async (user: PrismaUser): Promise<User> => {
  const self = userModel(model)

  return {
    ...user,
    name: self.name(user),
  }
}

export default (model: PrismaClient['user']) => Object.assign(model, userModel(model))

export * from './types'

index.ts:

import { PrismaClient } from '@prisma/client'

import userModel, { User } from './models/user'

const prisma = new PrismaClient()

const models = {
  user: userModel(prisma.user),
  // ...more models...
}

export default Object.assign(prisma, models)

export const {
  user,
  // ...more models...
} = models

export type {
  User,
  // ...more model types...
}

packages/app:

import orm, { User } from '@monorepo/orm'

const user: User|undefined = await orm.user.find({ where: { email: 'test@example.com' } })

@millsp millsp self-assigned this Apr 28, 2022
@millsp millsp removed this from the 3.9.0 milestone Apr 28, 2022
@ShanonJackson
Copy link

ShanonJackson commented May 21, 2022

This functionality already exists:
ES6 "Proxy" contains two traps for get/apply, these traps can be combined and wrapped around PrismaClient; Now whenever .findMany etc are called you can compose your own pre and post logic in an easy way.

Agree that maybe it might be better to have this as part of PrismaClient but if it is it should just use ES6 Proxy because then it can be fully implemented without touching the internals and creating additional bloat

const proxyify = <T extends object>(value: T, table?: string, operation?: string): T => {
	return new Proxy(value, {
		apply: function trap(obj, props, args) {
			if (typeof obj !== 'function') return;
			if (operation) {
				console.log(args, table, operation);
				// pre logic.
			}
			const returned = obj(...args);
			if (returned instanceof Promise) {
				/* post logic feel free to use finally */
				return returned;
			}
			/* Can actually re-chain proxyify here if you need to. */
		},
		get: function trap<K extends keyof T & string>(obj: T, key: K) {
			return proxyify(obj[key] as any /* 'any' forgive me jesus */, table || key, table ? key : undefined);
		},
	});
};
const client: PrismaClient = proxyify(new PrismaClient());

This is a simple example in our internal use we have an API we're written to easily allow pre/post logic by adding another parameter to proxify I.E

proxify(new PrismaClient(), {
    user: {pre: () => {/* do something*}, post: () => {/* do something */}},
}

In my experience these pre/post validations start with good intentions, but eventually a use case comes along that needs to disable logic in the pre/post and that's when the architecture starts to cave in on itself.

@matthewmueller
Copy link
Contributor Author

matthewmueller commented Jun 15, 2022

@ShanonJackson I haven't used ES6 Proxies yet, but it's an interesting idea that's worth looking into. Do you also get type-safety with proxies?

@ShanonJackson
Copy link

@matthewmueller

Yeah type-safety around ES6 Proxies in Typescript cover almost all use cases, but there are some patterns left to be typed but I don't think this will run into those.

@omar-dulaimi
Copy link

Hey all,

I've built a new Prisma Generator to emit custom models based on Prisma recommendations(as mentioned in the docs).
Currently, you could either WRAP or EXTEND. More grouping behaviors will be added soon.

Feel free to try it here: https://github.com/omar-dulaimi/prisma-custom-models-generator

@floelhoeffel
Copy link

Everyone passionate about extending Prisma, please check our new proposal in #15074 by @millsp

@random42
Copy link

random42 commented Sep 14, 2022

Hi,

My opinion is that extending specific models is not that important, since business specific tasks should be handled one layer above Prisma. But you added it so hurray.

However, extending the base functionality of the client with generic functions is of primary importance to me, for example to have a findByPrimaryKey or findPaginated functionality. The real problem is that generated types are not extending one generic Delegate interface, but every model has its own. Plus, for runtime sake, I think it would be better for it to be an abstract class instead of interface.

See my question on StackOverflow that explains the problem.

@janpio janpio changed the title Explore how to extend the Prisma Client Extend Prisma Client Nov 5, 2022
@janpio janpio added kind/feature A request for a new feature. topic: clientExtensions and removed kind/improvement An improvement to existing feature and code. labels Nov 30, 2022
@janpio janpio added the status/is-preview-feature This feature request is currently available as a Preview feature. label Mar 27, 2023
@JuanGalilea
Copy link

has someone had any issues with $-methods while using ES6 proxies or even tried them?

I'm getting errors like:
proxyOfPrismaClient.$disconnect() // $disconnect is not a function
Even when using very simple or even transparent proxies.

I can't find any info on why this might happen.

@millsp
Copy link
Member

millsp commented May 31, 2023

@random42 we will soon publish and expansion of our guide to show how to do that with client extensions.

@JuanGalilea It is possible that we have a bug there, please create a new issue and provide us with a repro.

Thanks.

@millsp
Copy link
Member

millsp commented May 31, 2023

We're closing this issue as the points from the OP have been implemented in Client Extensions. If something is still missing for you, please open a feature request, a bug report, or a discussion. Thanks.

@millsp millsp closed this as completed May 31, 2023
@janpio janpio added this to the 4.16.0 milestone Jun 19, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
kind/feature A request for a new feature. 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