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

Service Classes (e.g. for NestJS) #5273

Open
johannesschobel opened this issue Jan 25, 2021 · 54 comments
Open

Service Classes (e.g. for NestJS) #5273

johannesschobel opened this issue Jan 25, 2021 · 54 comments
Labels
kind/feature A request for a new feature. team/client Issue for team Client. tech/typescript Issue for tech TypeScript. topic: extend-client Extending the Prisma Client topic: NestJS

Comments

@johannesschobel
Copy link
Contributor

Dear Prisma Team,

for my upcoming project, i would like to use Prisma, since it is ready to be used in production. I have been around for a year or so, but now finally use it in a concrete project. I really like what you've been working on - Prisma looks great and i can't wait to try it out.

Problem

In the context of my project i will be building a RESTful API with NestJS. Unfortunately, because of various reasons I cannot rely on GraphQL, for example, existing 3rd party clients are not able to "speak" (i.e., work with) GraphQL.

In order to reduce boilerplate code in my Services, i thought it may be a good idea to create some kind of generic CrudService that offers basic functionality, like create, update, ... as some kind of wrapper around prisma. Having used typeorm in projects before, i thought that this may be an easy task. However, i quickly hit some roadblocks, because there are no Repositories in Prisma like in typeorm.

The next idea was to simply inject (i.e., pass) the corresponding Delegate to the CrudService.

The closest i could get, however, is like this:

import { Injectable } from '@nestjs/common';
import { Prisma } from '@prisma/client';

@Injectable()
export abstract class CrudService<E, M, C, R, U, D> {
  constructor(protected modelDelegate: m) {}

  public async create(data: C) {
    return await this.modelDelegate.create({ data: data });
    // !!! Property "create" does not exist on type "D"
  }

  // other methods to wrap prisma (i.e., findMany, findFirst, update, ...)
}

and then create a concrete UserService like this:

import { Injectable } from '@nestjs/common';
import { PrismaService } from './prisma.service';
import { CrudService } from './crud.service';
import { Prisma } from '@prisma/client';

@Injectable()
export class UserService extends CrudService<
  Prisma.UserDelegate,
  Prisma.UserCreateInput,
  // ... basically a LOT of types
> {
  constructor(private readonly prisma: PrismaService) {
    super(prisma.user);
  }
}

While this works, it has a few drawbacks:

  1. all modelDelegate methods (i.e, create(), update(), findMany(), ...) are unknown, because the delegate is not known
  2. the UserService with all its generics looks very ugly

Suggested solution

Regarding the drawbacks discussed earlier, i would suggest:

  1. All generated Delegates (i.e., UserDelegate) should extend a basic Delegate that holds all method descriptions. This way, we could use this basic Delegate within the CrudService like so:
@Injectable()
export abstract class CrudService<E, M extends Delegate, C, R, U, D> {
  constructor(protected modelDelegate: m) {}

  // ...
}
  1. Maybe, I could create a new Decorator, that automatically creates all this "boilerplate code" from the extends CrudService<...> block. This would basically just act as another wrapper.

Additional context

I am using NestJS and need to develop a RESTful application. In this context, i cannot use builders like Nexus or whatever to bootstrap CRUD features.

Question

Do you have any idea how to properly target this issue? Keep in mind that i cannot rely on existing GraphQL packages, like nexus. I don't think that generators would particularly help in this case, as everything required to create a CrudService is already there and in place - however, i cannot properly access / extend it, as there are some basic types / interfaces missing.

Would it be possible to make the Delegates extend a basic interface that i am able to use in a CrudService?

All the best

@johannesschobel
Copy link
Contributor Author

Ok, regarding Nr. 2 from my post above.
I was trying to prettify my code (i.e., to not pass the types for UserCreateArgs, UserUpdateOneArgs, UserUpdateManyArgs, UserFindManyArgs, ...). Basically, all those type names could be generated, if you know the name of the entity (i.e., User), like so:

const typeCreateArgs = `Prisma.${type}CreateArgs`;
const typeFindManyArgs = `Prisma.${type}FindManyArgs`;
// ...

Unfortunately you're not able to use these variable names as generics, like so:

export UserService extends CrudService<
  type,
  typeDelegate
  typeCreateArgs,
  typeFindManyArgs,
  // ...
> {
  // ...
}

again, they cannot be created in the CrudService either.
How can i "reference" (i.e., load) a type by string-name?

All the best

@timsuchanek
Copy link
Contributor

Thanks @johannesschobel! We'll have a look how to make this possible.
Would generating type-safe repositories for all models already solve your use-case?

@timsuchanek timsuchanek added team/client Issue for team Client. tech/typescript Issue for tech TypeScript. labels Jan 26, 2021
@johannesschobel
Copy link
Contributor Author

Dear @timsuchanek , thank you very much for getting back to me with this issue.
I have found a solution for my problem - although it is very dirty and hacky at the moment. But it works - and i guess that's all i can ask for, right now..

The solution would certainly be a bit prettier, if Prisma would auto-generate a few things for me. Creating Repositories would certainly help as well, however, not sure, what the best solution is.

However, for now, let me explain my current solution to the problem:

Solution to create CRUD Services:

1. Create a Delegate Interface

First, i created a Delegate Interface that simply contains all methods a respective Delegate (from Prisma) offers (i.e., the UserDelegate. Obviously, that interface is quite ugly and not type-safe at all, however, i only need it to make the methods of the delegate available and known.

export interface Delegate {
  aggregate(data: unknown): unknown;
  count(data: unknown): unknown;
  create(data: unknown): unknown;
  delete(data: unknown): unknown;
  deleteMany(data: unknown): unknown;
  findFirst(data: unknown): unknown;
  findMany(data: unknown): unknown;
  findUnique(data: unknown): unknown;
  update(data: unknown): unknown;
  updateMany(data: unknown): unknown;
  upsert(data: unknown): unknown;
}

Obviously, if you (i.e., Prisma) would generate this DelegateInterface once and add it, you may be able to add better typings here.. However, for the sake of this solution, the previously described interface works!

2. Simplify CrudService

I thought it would be a good idea to simplify the CrudService described in previous posts. Most importantly, i wanted to reduce the generic types that have to be passed, because it is very (!) ugly.

I ended up with this solution:

import { Injectable } from '@nestjs/common';
import { Delegate } from './models/delegate.interface';

@Injectable()
export abstract class CrudService<
  D extends Delegate,
  T
> {
  constructor(protected delegate: D) {}

  public getDelegate(): D {
    return this.delegate;
  }

  public async aggregate(data: unknown) {
    const result = await this.delegate.aggregate(data);
    return result;
  }

  public async count(data: unknown) {
    const result = await this.delegate.count(data);
    return result;
  }

  public async create(data: unknown) {
    const result = await this.delegate.create(data);
    return result;
  }
  
  // ... a lot of other functions
}

Note the D extends Delegate (which is described in 1)). With this extends I was able to make all delegate methods (i.e., create(), findMany(), ...) available.
T should be a type that holds all other generic types that i may need to properly implement the CrudService. Note that i still need to have data: unkown in my method params.

3. Add a new class that summarizes other types (UserMapType)

I defined a new class that holds all required types:

import { Prisma } from '@prisma/client';
import { CrudTypeMap } from './crud-type-map.model.ts

export class UserTypeMap implements CrudTypeMap {
  aggregate: Prisma.UserAggregateArgs;
  count: Prisma.UserCountArgs;
  create: Prisma.UserCreateArgs;
  delete: Prisma.UserDeleteArgs;
  deleteMany: Prisma.UserDeleteManyArgs;
  findFirst: Prisma.UserFindFirstArgs;
  findMany: Prisma.UserFindManyArgs;
  findUnique: Prisma.UserFindUniqueArgs;
  update: Prisma.UserUpdateArgs;
  updateMany: Prisma.UserUpdateManyArgs;
  upsert: Prisma.UserUpsertArgs;
}

This is basically just some kind of mapping; i.e., the create param would use Prisma.UserCreateArgs as input. Again, this would be something that could be autogenerated by the client.

You would think, that it would be a good idea to add an additional _delegate: Prisma.UserDelegate here as well, and you may certainly are right about this. This would remove another generic parameter from the CrudService. However, when doing this, i was not able to use the extends Delegate anymore, which made all methods unknown again. Maybe we can figure out another solution for this.

4. Add a CrudMapType Interface

In order to make it more accessible (i.e., autocomplete, ...) i created an additional interface for the UserTypeMap. Again, not very beautiful (i.e., everything is unknown) but i guess it works, haha 😆

export interface CrudTypeMap {
  aggregate: unknown;
  count: unknown;
  create: unknown;
  delete: unknown;
  deleteMany: unknown;
  findFirst: unknown;
  findMany: unknown;
  findUnique: unknown;
  update: unknown;
  updateMany: unknown;
  upsert: unknown;
}

5. Refactor CrudService

With this additional information it is now able to get rid of the unkown typings in the CrudService. Lets review the updated version of my code.

import { Injectable } from '@nestjs/common';
import { CrudTypeMap } from './models/crud-type-map.interface';
import { Delegate } from './models/delegate.interface';

@Injectable()
export abstract class CrudService<
  D extends Delegate,
+  T extends CrudTypeMap 
> {
  constructor(protected delegate: D) {}

  public getDelegate(): D {
    return this.delegate;
  }

+  public async aggregate(data: T['aggregate']) {
    const result = await this.delegate.aggregate(data);
    return result;
  }

+  public async count(data: T['count']) {
    const result = await this.delegate.count(data);
    return result;
  }

+  public async create(data: T['create']) {
    const result = await this.delegate.create(data);
    return result;
  }
  
  // .. again, a lot of methods
}

With the help of my new interface (see 4.) i was able to properly type the input for respective methods (i.e., T["create"]). Note that T.create is not possible here, but you can use the assoc-array notation to get the desired result. This will use the assigned type from the mapping class described in 3.

6. Wire everything together

Now its time to wire everything together. For this purpose, lets use the UserService:

@Injectable()
export class UserService extends CrudService<
  Prisma.UserDelegate,
  UserTypeMap
> {
  constructor(private readonly prisma: PrismaService) {
    super(prisma.user);
  }

  async foo() {
    return await this.count({ where: { email: { contains: '@' } } });
  }
}

I added the prisma.user (which is a Prisma.UserDelegate) within the constructor. This is somehow comparable to the repository known from typeorm. It gives access to the underlying methods, like create(), update() or whatever.

Furthermore, the CRUD methods are available internally and can be properly used. Also, the input is properly typed.
From the developers perspective, the UserService is type-safe, under the hood, however, a lot of unknown stuff is used.

Proposal

Basically, the following steps (from my solution above) could be done by the prisma generator
Step 1. create the Delegate interface
Step 3. create the mapping class
Step 4. add respective mapping interface

This will leave Step 2 (and 5) for the developer that wants to use the CRUD feature. Keep in mind that Step 2 (and 5) and Step 6 depend on the framework used (in my case its NestJS). If you use another framework (for example, pure express or featherJS or whateverJS, this may look completely different.

In this context, i suggest to extend the current prisma-client-js to add these interfaces and classes.


If you have any questions regarding my solution and / or proposal, please contact me. I would be very (!) happy to give more details and discuss this issue including my solution and proposal in person with you.

Thank you very much for taking the time to read my (very) extensive solution and proposal to this issue.
All the best,
Johannes

@matthewmueller
Copy link
Contributor

matthewmueller commented Jan 26, 2021

Thanks for this proposal!

To understand better why you're reaching for the repository pattern, do you mind sharing what you need to do in your RESTful API controllers that's leading to this boilerplate?

An example of the controllers with prisma would be really helpful.

@johannesschobel
Copy link
Contributor Author

johannesschobel commented Jan 26, 2021

Dear @matthewmueller ,
thank you very much for reaching out to me. Consider the following example.

In the API i would need to create a lot of controllers, that expose CRUD routes for each of them.
For example, i have a UserController that allows for creating, reading, ... Users. Those endpoints will be mapped to /users. Additionally, i would have a FaqController that allows for creating, reading, updating, ... FAQs. These routes will be mapped to /faqs, and so on.

One controller class may look like this:

import { Controller, Get, Post, Patch, Delete } from '@nestjs/common';
import { UserService } from './services/user.service.ts';

@Controller('users')
export class UserController {
  constructor(private readonly service: UserService) {}

  @Get(':id')
  async getOne(@Param('id') id: string): string {
    return await this.service.findUnique({where: {id: id}})
  }

  @Post()
  async create(@Body() data: CreateUserRequest) {
    const dto: Prisma.UserCreateArgs = { 
      data: { /* map from request to dto */ } 
    }; 
    return await this.service.create(dto);
  }

  // other API endpoints for update, delete, getMany, ...
}

The UserService in turn, would then look like this (example from above):

import { Injectable } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { CrudService } from './services/crud.service';
import { PrismaService } from './services/prisma.service';
import { UserTypeMap } from './builders/user-map.builder';

@Injectable()
export class UserService extends CrudService<
  Prisma.UserDelegate,
  UserTypeMap
> {
  constructor(private readonly prisma: PrismaService) {
    super(prisma.user);
  }

  // maybe additional (custom) methods that may be specific to methods of the exposed 
  // controller (i.e., get assigned Groups for a particular User).
}

All these CRUD methods within the UserService, FaqService, WhateverService are basically the same; they just read from / write to the database with respect to one particular Delegate (i.e., the UserDelegate). In this context, the CrudService would also be a good point to hook in some exception handling (i.e., if you're requesting a User that does not exist; if a FAQ cannot be updated or deleted, ...) and not have it scattered across multiple services.

I am pretty sure that the controller itself may also be created in a generic way, so that you can omit those basic CRUD method declarations. However, i do consider this as "out of scope" for prisma, because it depends on the underlying framework (in my case NestJS) and structure you're using.

Does this help you?
All the best

@johannesschobel
Copy link
Contributor Author

Dear @matthewmueller and @timsuchanek ,
i put together an example repository to showcase my approach, based on the prisma-example / rest-prisma boilerplate from one of your repositories.

You can find my repository here:

Showcase Repository

In this repository i added 2 additional models (Cat and Dog), that have their own nest modules. For each module, there is one regular Service (the CatsService and DogsService respectively) that directly communicate with the PrismaService to access the database. For the sake of this example, i only added a few methods, but you should get the idea.
All methods within these 2 services are exactly the same (except that the one accesses Cats and the other one Dogs). The overall structure, however, is identical.
Furthermore, each module has a Controller (CatsController and DogsController). These controllers are used to get incoming request data, transform them (i.e., map from the request body to the prisma DTOs) and pass this data to the next layer.

Finally, in each module there is a folder called crud, that contains the simplified version of the Service. This CatsCrudService, for example, extends the CrudService as described in this issue here. Hence, all boilerplate methods for create, findMany, findUnique, updateOne and so on. Furthermore, to showcase the latter, there is an additional CatsCrudController - which is basically exactly the same as the regular CatsController, but only injects the crud version of the service.

Note that there would be the possibility to remove this "boilerplate" code in the Controllers as well, but that is not the scope of Prisma, but rather NestJS.

All the best

@johannesschobel
Copy link
Contributor Author

update for the latest 2.16.0 version

I just updated to the latest prisma version. The code still works, but it lot weirder to "properly" type methods because of the newly method signatures in the autogenerated PrismaClient.

@dimoff66
Copy link

dimoff66 commented Feb 18, 2021

My absolutely typesafe soluition for this problem

type Dict = { [k: string]: any }

type DictWithId = { 
  id?: number 
  [k: string]: any
}

type SelectWithId = {
  id?: boolean 
  [k: string]: any
}

type Delegate = {
  findMany: (arg: {
    select?: SelectWithId | null
    include?: Dict | null
    where?: Dict
    orderBy?: Prisma.Enumerable<any>
    cursor?: Dict
    take?: number
    skip?: number
    distinct?: Prisma.Enumerable<any>
  }) => any,

  findFirst: (arg: {
    select?: SelectWithId | null
    rejectOnNotFound?: Prisma.RejectOnNotFound
    include?: Dict | null
    where?: DictWithId
    orderBy?: Prisma.Enumerable<any>
    cursor?: Dict
    take?: number
    skip?: number
    distinct?: Prisma.Enumerable<any>
  }) => any,

  create: (arg: {
    select?: SelectWithId | null
    include?: Dict | null
    data: any
  }) => any,

  update: (arg: {
    select?: SelectWithId | null
    include?: Dict | null
    data: any,
    where: DictWithId
  }) => any,

  delete: (arg: {
    select?: SelectWithId | null
    include?: Dict | null
    where: DictWithId
  }) => any,

  [k: string]: any
}

type FindManyWhereArg<T extends Delegate> = Parameters<T['findMany']>[0]['where']
type FindFirstWhereArg<T extends Delegate> = Parameters<T['findFirst']>[0]['where']
type CreateDataArg<T extends Delegate> = Parameters<T['create']>[0]['data']
type UpdateDataArg<T extends Delegate> = Parameters<T['update']>[0]['data']

abstract class DaoManager <T extends Delegate>{
  abstract get delegate (): T

  getMany (where: FindManyWhereArg<T>) {
    return this.delegate.findMany({ where })
  }

  getOne (where: FindFirstWhereArg<T>) {
    return this.delegate.findFirst({ where })
  }

  create (data: CreateDataArg<T>) {
    return this.delegate.create({ data })
  }

  update (id: number, data: UpdateDataArg<T>) {
    return this.delegate.update({ data, where: { id } })
  }

  delete (id: number) {
    return this.delegate.delete({ where: { id } })
  }
}

so we can create classes like this for model Category

class Category extends DaoManager<typeof prisma.category> {
  delegate = prisma.category
})
```


@pantharshit00 pantharshit00 added the kind/feature A request for a new feature. label Mar 2, 2021
@floross
Copy link

floross commented Apr 8, 2021

For the moment I use this snippet of code to have a UserService with nestjs:

@Module({
  providers: [
    {
      provide: 'USER_SERVICE',
      useFactory: (databaseService: DatabaseService) => {
        return databaseService.user;
      },
      inject: [DatabaseService]
   }],
})
export class UserModule {}

But I did not found how to properly and elegantly extend it

@matthewmueller matthewmueller added the topic: extend-client Extending the Prisma Client label May 19, 2021
@bosconian-dynamics
Copy link

A Delegate type would be amazing 👍

I'm just getting started with Typescript and Prisma, and spent the day floundering around attempting to achieve this on my own ☹️

@theoludwig
Copy link

Indeed, reusability in Prisma should be easier. 👍

To add some context, I'm trying to reuse in every models, a pagination system.
Here's how I did it currently :

export interface PaginationOptions<T> {
  model: T
  query: {
    limit: number
    before?: number
    after?: number
  }
}

export const pagination = async <T>(
  options: PaginationOptions<T>
): Promise<T[]> => {
  const { model, query } = options
  // @ts-expect-error
  const items = await model.findMany({
    take: query.before != null ? query.limit * -1 : query.limit,
    skip: query.after != null || query.before != null ? 1 : undefined,
    ...(query.after != null && {
      cursor: {
        id: query.after
      }
    }),
    ...(query.before != null && {
      cursor: {
        id: query.before
      }
    })
  })
  return items
}

It works, but it is ugly as the T generic is any type, and don't correspond to a Prisma model, so I need to use // @ts-expect-error.
If I understood well, a potentially Delegate type would allow to have type safe usage of findMany and others methods of a Prisma model inside another function.

Currently to reuse the logic there is a "workaround", you can check a discussion about this there : #7075

@VinayaSathyanarayana
Copy link

It will be nice to get a OpenAPI Specification generated for the prisma schema file so that all of the excellent tooling available in OpenApi Tools can be leveraged

@johannesschobel
Copy link
Contributor Author

Dear @VinayaSathyanarayana ,
i don't think that this is a good idea / feature for the default prisma package. Because the main use-case of prisma is to act as some kind of "database abstraction layer". OpenAPI, however, is a specification about describing your RESTful API (i.e., describe the routes, describe input and output objects). These are, clearly, different domains.

If you are using additional API Specifications like JSON:API (https://jsonapi.org/) this adds additional constraints regarding the structure of the input / output resources (i.e., every attribute must be wrapped in a data.attributes array). In this case, the prisma object model and the json:api model are totally different!

I guess, the best solution would be to add a dedicated package which transforms the model to openapi specifications. But from my perspective, this has nothing to do with the default prisma package

@VinayaSathyanarayana
Copy link

Thanks @johannesschobel - The converter/generator can be dedicated package. Having the ability to generate a OpenAPI Spec will be of great use

@mitsos1os
Copy link

mitsos1os commented Jun 7, 2021

I also believe that this is a really important missing feature as also discussed in:

I find it necessary in order to keep the DRY principle.

However unless I am missing something, the currently proposed workarounds are not fully-type-safe, meaning that they only have the effect of the Typescript compiler not complaining about unknown types but at the same time lose the strict typing of every generated type.

For example accepting a plain object in where param of findMany, is not as effective as the UserWhereInput where the compiler even knows the acceptable fields of the User Entity along with the operators possibly present in the query.

If I am right about this, then it kind of takes away the advertised advantage of Prisma against TypeORM for explicit type safety of the returned and queried fields.

I believe the only way to properly support this, is if the compiler produces all the necessary output supporting conditional typing, where the proper types should be mapped to a generic interface. For example by supplying the <User> generic type in a CrudService, then a WhereInput in the findMany method used in this service, should be mapped behind the scenes to the original UserWhereInput etc...

An other way could be if a Nested Type structure would be the output of the compiler, where IndexedAccess is supported in Typescript and selecting on demand the appropriate interface to implement for each Service - Class.

@Yuliang-Lee
Copy link

Is this has offcial support or solution now?

@ctsstc
Copy link

ctsstc commented Apr 1, 2022

I've found these helpful Prisma.${ModelName}CreateArgs Prisma.${ModelName}CreateInput there's also variations for update, and delete. There seems to be no base type that they extend/implement though, so I cannot utilize them in generics or having a proper interface for them unfortunately. Right now I'm trying to work with the middleware to create a modelhook system and there seems to be no way to have proper typing for Prisma.MiddlewareParams.args since it's typed as any. Not certain if this is completely related, but felt a bit maybe. I can utilize generics without any conditions to them but then you can pass whatever you want in for the generic which defeats the purpose. I'd hope some of the typings here would help solve this.

Edit: I think I found an open issue related to what I'm looking for:
#10422

@aeddie-zapidhire
Copy link

The more I work with in-real-life problems with Prisma, the more I'm starting to realise a CRUD wrapper is not something Prisma core will want to support. I think it's something that will need to be crowd supported. But having worked in Open Source for many years in the past, not having some sort of endorsement from the parent project is always problematic. So I wonder if Prisma would entertain community ideas, and if there is enough hands willing to support them in the community, they'd provide a repo under the Prisma org, and the project could enjoy a level of semi-official status.

In terms of implementation, and unless you consider only the most vanilla of cases, the CRUD wrapper is very tricky (not to mention the added dimension of using workspaces in a mono-repo). I find myself having to jump into raw sql more and more to support language features that Prisma does not. But I'd be interested in sharing my experiences with it, if others were interested. However, I don't think it's possible to organise everything just in "this thread" if that's the way people wanted to go.

Thanks in advance.

@krsbx
Copy link

krsbx commented Jul 17, 2022

for this issue, i think we're not going to have the wrapper since in prisma client extension, they mention it was nice to have and can be solved via an extension. I made prisma-repo so we can have the repository pattern in Prisma by playing around with some regex.

regarding adding all the types to the classes, prisma-repo will handle it in the baseRepository.ts so we can focus on the service that we want to have instead of focusing on passing the types that we needs.

@KyleMoran138
Copy link

Funny, I was trying to also make a CRUD service just now and I started going about looking for a Delegate base interface to base things off of when I found this thread. I would also greatly appreciate some way to extend that interface. That being said, I'm not smart enough to implement that 😅.

Is there no way to implement / extend the models that are generated in a generic manner as to create an execution point for business logic? I've been looking through individual solutions on a case by case basis for most of the Prisma apps I work on, but now I'm here lol.

@aeddie-zapidhire
Copy link

aeddie-zapidhire commented Jul 25, 2022

I think a unified model (allowing for read-write instance out of the gate) will be based on something like the following (if one is aiming for a TypeORM-ish experience):

import { Message, PrismaClient } from '.prisma/client'

class AbstractPrismaModel<T, P> {
  constructor(protected dbReader: P, protected dbWriter: P) {
  }

  public async count(): Promise<number> {
    return this.reader().count()
  }

  public async findMany():Promise<T[]> { /* ... */ }

  public async createOne(data: Partial<T>): Promise<T> {
    return this.writer().create({ data: data as any }  // <-- Haven't found a way to type this easily yet
  }

  abstract protected reader(): Todo {}

  abstract protected writer(): Todo {}
}

class MessageModel extends AbstractPrismaModel<Message, PrismaClient>() {
  protected reader() {
    return this.dbReader.message
  }

  protected writer() {
    return this.dbWriter.message
  }
}

Obviously a lot of typings to sort out, and also allowing for where conditions, etc.

@aeddie-zapidhire
Copy link

aeddie-zapidhire commented Aug 1, 2022

@janpio thanks for reaching out.

The problem I think most of us are trying to solving is that, anecdotally, a lot of us are coming from Sequelise or Waterline or TypeORM, and we are used to having a CRUD model that we can extend from. Prisma presents some challenges with the typings, specifically I believe with the Delegate (others correct me if I'm wrong).

In Prisma, to make a model I'd currently construct each data service/model by hand (which is why several people go down the generator route).

import { PrismaClient, Thing } from '.prisma/client'

class ThingService {
  constructor(private db: PrismaClient) {}

  public async findMany(): Promise<Thing[]> {
    return this.db.thing.findMany()
  }

  // repeat for each separate database operation I need, `count`, `update`, `upsert` etc and so on.
}

This results in a lot of code duplication. What I'd like to do instead is have a base to extend off, something like:

import { GenericDelegate, TheGenericArgsForFindMany } from 'prisma'

class MyBasePrismaService<E> {
  construtor(private repo: GenericDelegate<E>) {}

  public async findMany(args: TheGenericArgsForFindMany<E>): Promise<E[]> {
    return this.delegate.findMany(args)
  }
}

And then I can implement a concrete class like so:

import { Thing } from '.prisma/client'

class MyThingService extends MyBasePrismaService<Thing> {
}

Conceptually anyway.

I guess the first thing I'd ask is how is Prisma intending us to build our data service layers when we have a non-trivial amount of entities to deal with and the amount of effort to explicitly re-implement (coding and unit tests) every CRUD operation is non-trivial? I can abstract my CRUD controllers very easily (usually they are just the constructor), but it is very challenging to abstract the data service classes using Prisma.

How does Prisma see us keeping those service layers DRY? Perhaps a better question is how are you keeping your own projects DRY?

Or to put it another way, is there a way that additional typings can be exposed so that someone can publish their own CRUD wrapper on npm and we can just extend it with our own .prisma/client data models? Ideally aiming for something like:

import { Thing } from '.prisma/client'
import { PrismaCrudModel } from 'awesome-prisma-crud'

class ThingCrudService extends PrismaCrudModel<Thing> {
}

Thanks in advance.

@Yuliang-Lee
Copy link

@janpio thanks for reaching out.

The problem I think most of us are trying to solving is that, anecdotally, a lot of us are coming from Sequelise or Waterline or TypeORM, and we are used to having a CRUD model that we can extend from. Prisma presents some challenges with the typings, specifically I believe with the Delegate (others correct me if I'm wrong).

In Prisma, to make a model I'd currently construct each data service/model by hand (which is why several people go down the generator route).

import { PrismaClient, Thing } from '.prisma/client'

class ThingService {
  constructor(private db: PrismaClient) {}

  public async findMany(): Promise<Thing[]> {
    return this.db.thing.findMany()
  }

  // repeat for each separate database operation I need, `count`, `update`, `upsert` etc and so on.
}

This results in a lot of code duplication. What I'd like to do instead is have a base to extend off, something like:

import { GenericDelegate, TheGenericArgsForFindMany } from 'prisma'

class MyBasePrismaService<E> {
  construtor(private repo: GenericDelegate<E>) {}

  public async findMany(args: TheGenericArgsForFindMany<E>): Promise<E[]> {
    return this.delegate.findMany(args)
  }
}

And then I can implement a concrete class like so:

import { Thing } from '.prisma/client'

class MyThingService extends MyBasePrismaService<Thing> {
}

Conceptually anyway.

I guess the first thing I'd ask is how is Prisma intending us to build our data service layers when we have a non-trivial amount of entities to deal with and the amount of effort to explicitly re-implement (coding and unit tests) every CRUD operation is non-trivial? I can abstract my CRUD controllers very easily (usually they are just the constructor), but it is very challenging to abstract the data service classes using Prisma.

How does Prisma see us keeping those service layers DRY? Perhaps a better question is how are you keeping your own projects DRY?

Or to put it another way, is there a way that additional typings can be exposed so that someone can publish their own CRUD wrapper on npm and we can just extend it with our own .prisma/client data models? Ideally aiming for something like:

import { Thing } from '.prisma/client'
import { PrismaCrudModel } from 'awesome-prisma-crud'

class ThingCrudService extends PrismaCrudModel<Thing> {
}

Thanks in advance.

That's what we need! I think and try so many ways but no one can solve the problem, So now we turn back to typeORM already. LOL

@aeddie-zapidhire
Copy link

@Yuliang-Lee

So now we turn back to typeORM already. LOL

No, because I love the Prisma migrations too much to let it go, LOLs :)

@wodCZ
Copy link

wodCZ commented Aug 30, 2022

Hello there 👋

Just chiming in here with a similar use-case. I'm not (yet) in need of CRUD wrappers, but I do need a way to extend the built-in methods while keeping them type-safe.
As instructed at Prisma Slack, I'm linking a discussion about Query composition (#12047) and my comment with my use-case description, just in case it helps in shaping the "bigger picture".

@millsp
Copy link
Member

millsp commented Aug 31, 2022

Hey everyone, I am excited to share some news on this and I am looking for your feedback. We have a proposal for extending the Prisma Client, and this feature request was part of our research. I propose to solve this via a generic extension:

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

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

Let me know if this works for your use-case and I'd love to get your feedback on the Prisma Client Extensions proposal.

@jacobclarke92
Copy link

jacobclarke92 commented Sep 1, 2022

const prisma = new PrismaClient().$extend({
    $model: {
        $all: {
            getClass<T extends object>(this: T): new () => T {
                return class { /**  Implementation */ } as any
            }
        },
    },
})

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

Let me know if this works for your use-case and I'd love to get your feedback on the Prisma Client Extensions proposal.

Absolutely love this @millsp and think it would be a great addition!
I think it answers @aeddie-zapidhire 's example snippet from above:

import { Thing } from '.prisma/client'
import { PrismaCrudModel } from 'awesome-prisma-crud'

class ThingCrudService extends PrismaCrudModel<Thing> {
}

I might leave a comment on that proposal about other aspects.
Namely I think @hrueger's comment makes a lot of sense, but raises more questions for me when using $all.

@johannesschobel
Copy link
Contributor Author

johannesschobel commented Sep 6, 2022

Dear all, for those who are working with nestjs, you can try out @prisma-utils/prisma-crud-generator (https://github.com/prisma-utils/prisma-utils/tree/main/libs/prisma-crud-generator).

Install it via

npm i -D @prisma-utils/prisma-crud-generator

The package allows for generating a fully typesafe crud service for the models that are defined in the schema file.

Simply add the generator to your schema file and run

npx prisma generate

voila - it creates fully usable stubs for the CRUD generator for you. Don't like the stub that i added to the lib? Easy switch it out with your own custom stub (i.e., different variables, more methods, ...). See the readme file for more configuration options on the generator.

Oh, and by the way..
As an additional bonus, the package also allows to fully generate Input types (i.e., DTOs that are sent by the client to the server) based on the models. You can even add custom validation rules (i.e., @IsEmail()) to fields that are automatically applied to the DTO fields as well.

All the best

@SahasSaurav
Copy link

SahasSaurav commented Sep 25, 2022

Thanks, @johannesschobel for the solution for creating a wrapper function for Prisma
#5273 (comment)

But your solution does not give type hint of the return type of wrapper methods

const User = new UserModel()
User
  .findAll({})
  .then((data) => {
    console.log(data)
  })
  .catch((err) => console.error(err))

just update for solution and this is my solution for create wrapper

Screenshot (28)

import { PrismaClient } from '@prisma/client'

import { Prisma } from '@prisma/client'

const prisma = new PrismaClient()

interface DbMethods {
  findFirst(data: unknown): Promise<unknown>
  findUnique(data: unknown): Promise<unknown>
  findMany(data: unknown): Promise<unknown>
  create(data: unknown): Promise<unknown>
  createMany(data: unknown): Promise<unknown>
  update(data: unknown): Promise<unknown>
  updateMany(data: unknown): Promise<unknown>
  delete(data: unknown): Promise<unknown>
  deleteMany(data: unknown): Promise<unknown>
}

interface DbTypeMap{
  findSingle:unknown,
  findSingleReturn:unknown,
  findUnique: unknown,
  findUniqueReturn: unknown,
  findAll: unknown,
  findAllReturn:unknown,
  create:unknown,
  createReturn:unknown,
  createAll:unknown,
  createAllReturn:unknown,
  updateSingle: unknown,
  updateSingleReturn: unknown,
  updateAll:unknown,
  updateAllReturn:unknown,
  delete:unknown,
  deleteReturn:unknown,
  deleteMany:unknown,
  deleteManyReturn:unknown,
}

abstract class DbService<Db extends DbMethods, T extends DbTypeMap> {
  constructor(protected db: Db) {}

  findSingle(data: T['findSingle']):T['findSingleReturn'] {
    return this.db.findFirst(data)
  }

  findUnique(data:T['findUnique']):T['findUniqueReturn'] {
    return this.db.findUnique(data)
  }

  findAll(data: T['findAll']):T['findAllReturn'] {
    return this.db.findMany(data)
  }

  create(data:T['create']):T['createAllReturn'] {
    return this.db.create(data)
  }

  createAll(data:T['createAll']):T['createAllReturn'] {
    return this.db.createMany(data)
  }

  updateSingle(data:T['updateSingle']):T['updateSingleReturn'] {
    return this.db.update(data)
  }

  updateAll(data:T['updateAll']):T['updateAllReturn'] {
    return this.db.updateMany(data)
  }

  delete(data:T['delete']):T['deleteReturn'] {
    return this.db.delete(data)
  }

  deleteMany(data:T['deleteMany']):T['deleteManyReturn'] {
    return this.db.deleteMany(data)
  }
}

type UserType = typeof prisma.user

interface UserTypeMap extends DbTypeMap {
  findSingle:Prisma.UserFindFirstArgs
  findSingleReturn:ReturnType<UserType['findFirst']>,
  findUnique:Prisma.UserFindUniqueArgs
  findUniqueReturn:ReturnType<UserType['findUnique']>,
  findAll:Prisma.UserFindManyArgs
  findAllReturn: ReturnType<UserType['findMany']>
  create:Prisma.UserCreateArgs,
  createReturn:ReturnType<UserType['create']>,
  createAll:Prisma.UserCreateManyArgs,
  createAllReturn:ReturnType<UserType['createMany']>,
  updateSingle: Prisma.UserUpdateArgs,
  updateSingleReturn: ReturnType<UserType['update']>,
  updateAll:Prisma.UserUpdateManyArgs,
  updateAllReturn:ReturnType<UserType['updateMany']>,
  delete:Prisma.UserDeleteArgs,
  deleteReturn:ReturnType<UserType['delete']>,
  deleteMany:Prisma.UserDeleteManyArgs,
  deleteManyReturn:ReturnType<UserType['deleteMany']>,
}

class UserModel extends DbService<UserType, UserTypeMap> {
  constructor() {
    super(prisma.user)
  }
}

const User = new UserModel()
User
  .findAll({})
  .then((data) => {
    console.log(data)
  })
  .catch((err) => console.error(err))

@stek93
Copy link

stek93 commented Nov 17, 2022

Hi everyone 🙂

Before I start, sorry for spamming - I tried to write on the official Prisma slack channel and they redirected me here. 🙂
Similar problem on my side as well, but wanted to give it a fresh look with the new features that are soon coming in the Prisma version 4.7.0.
I read this issue thread multiple times and I saw that all of us are trying to solve a similar issue.
I wanted to see if the issue I described below is solvable with the upcoming version or if I misunderstood the proposed answer from @millsp .

Similarly, like everyone here I wanted to create a Generic repo layer - I want to create one class with my custom implementations of findUnique, findMany, update, delete, etc.

The idea is to not repeat ourselves (therefore stop violating the DRY principle) and to have one implementation of these methods and respected models will just reuse the same logic from the generic repository.
The thing is - it was not easily solvable until a few days ago. The Prisma team is preparing 4.7.0 release and from that version, it should be relatively easy to solve it (at least that's how I understood it from the response above). I am aware that 4.7.0 is still not officially released, but I am trying to prepare everything until its official release.

Example:

// this client extension is something new coming in the 4.7.0 version
// basically, every model will have the getClass() method which can be
// used for differentiating between specific models (check the UserRepository down below)
const prismaClient = new PrismaClient();
const extendedClient = prismaClient.$extends({
    model: {
        $allModels: {
            getClass<T extends object>(this: T): new () => T {
                return class {} as any;
            },
        },
    },
});

// ISSUE: I need to know the base "class/type/interface" from which every Prisma model extends 
// (I know that there were suggestions to extend from the base Delegate class which I think is not something that's done in the new version)
// Not sure if this generic type is something that's achievable somehow with the new upcoming version. Maybe I am missing something obvious here 
// If I manage to extend from the base model - then the TS compiler won't throw any errors when prisma[T] is used and we'll have full type safety here

export class BaseRepository<T> {
  findUnique() {
    return prisma[T].find({where:...})
  }
  create() {
    return prisma[T].create(...)
  }
  update() {
    return prisma[T].update(...)
  }
  ...
}

// this is the thing that was made possible in the newer version
const UserClass = extendedClient.user.getClass();

class UserRepository extends BaseRepository<UserClass> {
  // all the base CRUD methods are already implemented for user
  // here, only the custom ones should go
}

Basically, in the pseudocode above, just check the comment with the ISSUE tag, that is the main question here - How to get to the point where I can use generics in order to use Prisma types/methods/etc and have full type safety? 🙂

Thanks in advance!

@johannesschobel
Copy link
Contributor Author

Dear @stek93 ,

you can try my library here: https://github.com/prisma-utils/prisma-utils/tree/main/libs/prisma-crud-generator
It basically generates a fully fledged CRUD service for you, that can be used within a nestjs application.

Give it a try ;)

@filipkrw
Copy link

Inspired by @SahasSaurav and @johannesschobel solutions, I came up with a quite succinct solution of my own with the help of generics.

type Operations =
  | 'aggregate'
  | 'count'
  | 'create'
  | 'createMany'
  | 'delete'
  | 'deleteMany'
  | 'findFirst'
  | 'findMany'
  | 'findUnique'
  | 'update'
  | 'updateMany'
  | 'upsert';

export class PrismaRepo<
  D extends { [K in Operations]: (args: unknown) => unknown },
  A extends { [K in Operations]: unknown },
  R extends { [K in Operations]: unknown },
> {
  constructor(protected model: D) {}

  async findUnique(args: A['findUnique']): Promise<R['findUnique']> {
    return this.model.findUnique(args);
  }

  async findFirst(args: A['findFirst']): Promise<R['findFirst']> {
    return this.model.findFirst(args);
  }

  async findMany(args: A['findMany']): Promise<R['findMany']> {
    return this.model.findMany(args);
  }

  async create(args: A['create']): Promise<R['create']> {
    return this.model.create(args);
  }

  async createMany(args: A['createMany']): Promise<R['createMany']> {
    return this.model.createMany(args);
  }

  async update(args: A['update']): Promise<R['update']> {
    return this.model.update(args);
  }

  async delete(args: A['delete']): Promise<R['delete']> {
    return this.model.delete(args);
  }

  async upsert(args: A['upsert']): Promise<R['upsert']> {
    return this.model.upsert(args);
  }

  async count(args: A['count']): Promise<R['count']> {
    return this.model.count(args);
  }

  async aggregate(args: A['aggregate']): Promise<R['aggregate']> {
    return this.model.aggregate(args);
  }

  async deleteMany(args: A['deleteMany']): Promise<R['deleteMany']> {
    return this.model.deleteMany(args);
  }

  async updateMany(args: A['updateMany']): Promise<R['updateMany']> {
    return this.model.updateMany(args);
  }
}
export type DelegateArgs<T> = {
  [K in keyof T]: T[K] extends (args: infer A) => Promise<unknown> ? A : never;
};

export type DelegateReturnTypes<T> = {
  [K in keyof T]: T[K] extends (args: infer A) => Promise<infer R> ? R : never;
};
import { Prisma, PrismaClient } from '@prisma/client';
import { PrismaRepo } from '../../shared/prisma/PrismaRepo';
import { DelegateArgs, DelegateReturnTypes } from '../../shared/prisma/types';

type MarketDelegate = Prisma.MarketDelegate<unknown>;

export class MarketRepo extends PrismaRepo<
  MarketDelegate,
  DelegateArgs<MarketDelegate>,
  DelegateReturnTypes<MarketDelegate>
> {
  constructor() {
    super(new PrismaClient().market);
  }
}

Result:

image

image

I'm not sure about the significance of GlobalRejectSettings type that MarketDelegate generic type takes though.

@krsbx
Copy link

krsbx commented Dec 9, 2022

Just made an alternative of my previous approach on regarding this issue, my first library that can be use is prisma-repo but we need to re run the script every time we create a new migrations files and the other one is @krsbx/prisma-repo which will re-run every time we create a new migrations since it's a prisma generator

I recommend to use the one with prisma generator since it's much faster than the one that read the definition that can be unreliable when we use a different output file destination for the prisma client generator

@SahasSaurav
Copy link

SahasSaurav commented Dec 24, 2022

Inspired by @SahasSaurav and @johannesschobel solutions, I came up with a quite succinct solution of my own with the help of generics.

type Operations =
  | 'aggregate'
  | 'count'
  | 'create'
  | 'createMany'
  | 'delete'
  | 'deleteMany'
  | 'findFirst'
  | 'findMany'
  | 'findUnique'
  | 'update'
  | 'updateMany'
  | 'upsert';

export class PrismaRepo<
  D extends { [K in Operations]: (args: unknown) => unknown },
  A extends { [K in Operations]: unknown },
  R extends { [K in Operations]: unknown },
> {
  constructor(protected model: D) {}

  async findUnique(args: A['findUnique']): Promise<R['findUnique']> {
    return this.model.findUnique(args);
  }

  async findFirst(args: A['findFirst']): Promise<R['findFirst']> {
    return this.model.findFirst(args);
  }

  async findMany(args: A['findMany']): Promise<R['findMany']> {
    return this.model.findMany(args);
  }

  async create(args: A['create']): Promise<R['create']> {
    return this.model.create(args);
  }

  async createMany(args: A['createMany']): Promise<R['createMany']> {
    return this.model.createMany(args);
  }

  async update(args: A['update']): Promise<R['update']> {
    return this.model.update(args);
  }

  async delete(args: A['delete']): Promise<R['delete']> {
    return this.model.delete(args);
  }

  async upsert(args: A['upsert']): Promise<R['upsert']> {
    return this.model.upsert(args);
  }

  async count(args: A['count']): Promise<R['count']> {
    return this.model.count(args);
  }

  async aggregate(args: A['aggregate']): Promise<R['aggregate']> {
    return this.model.aggregate(args);
  }

  async deleteMany(args: A['deleteMany']): Promise<R['deleteMany']> {
    return this.model.deleteMany(args);
  }

  async updateMany(args: A['updateMany']): Promise<R['updateMany']> {
    return this.model.updateMany(args);
  }
}
export type DelegateArgs<T> = {
  [K in keyof T]: T[K] extends (args: infer A) => Promise<unknown> ? A : never;
};

export type DelegateReturnTypes<T> = {
  [K in keyof T]: T[K] extends (args: infer A) => Promise<infer R> ? R : never;
};
import { Prisma, PrismaClient } from '@prisma/client';
import { PrismaRepo } from '../../shared/prisma/PrismaRepo';
import { DelegateArgs, DelegateReturnTypes } from '../../shared/prisma/types';

type MarketDelegate = Prisma.MarketDelegate<unknown>;

export class MarketRepo extends PrismaRepo<
  MarketDelegate,
  DelegateArgs<MarketDelegate>,
  DelegateReturnTypes<MarketDelegate>
> {
  constructor() {
    super(new PrismaClient().market);
  }
}

Result:

image image

I'm not sure about the significance of GlobalRejectSettings type that MarketDelegate generic type takes though.

@filipkrw Good solutions I really like that you decrease the line of code with mapped type and infer keyword
But it has a problem with the solution

when you will create a transaction with prisma it required to have Prisma Promise

Prisma Methods return Prisma Promise which is wrapper around the promise. See the error reference below down
#11277

@SahasSaurav
Copy link

SahasSaurav commented Dec 24, 2022

took inspiration from these solutions #5273 (comment) and #5273 (comment)
Improve Return type of method

import { PrismaClient } from '@prisma/client'
import type { Prisma } from '@prisma/client'

const prisma = new PrismaClient()

type Operation =
  | 'findFirst'
  | 'findUnique'
  | 'findMany'
  | 'create'
  | 'createMany'
  | 'update'
  | 'updateMany'
  | 'delete'
  | 'deleteMany'
  | 'count' 

abstract class DbService<
  Db extends { [Key in Operation]: (data: any) => unknown },
  Args extends { [K in Operation]: unknown },
  Return extends { [K in Operation]: unknown }
> {
  constructor(protected db: Db) {}

  findFirst(data?: Args['findFirst']): Return['findFirst'] {
    return this.db.findFirst(data)
  }

  findUnique(data: Args['findUnique']): Return['findUnique'] {
    return this.db.findUnique(data)
  }

  findMany(data?: Args['findMany']): Return['findMany'] {
    return this.db.findMany(data)
  }

  create(data: Args['create']): Return['create'] {
    return this.db.create(data)
  }

  createMany(data: Args['createMany']): Return['createMany'] {
    return this.db.createMany(data)
  }

  update(data: Args['update']): Return['update'] {
    return this.db.update(data)
  }

  updateMany(data: Args['updateMany']): Return['updateMany'] {
    return this.db.updateMany(data)
  }

  delete(data: Args['delete']): Return['delete'] {
    return this.db.delete(data)
  }

  deleteMany(data?: Args['deleteMany']): Return['deleteMany'] {
    return this.db.deleteMany(data)
  }

  count(data?: Args['count']): Return['count'] {
    return this.db.count(data)
  }
}

type DelegateArgs<T> = {
  [Key in keyof T]: T[Key] extends (args: infer A) => unknown ? A : never
}

type DelegateReturnTypes<T> = {
  [Key in keyof T]: T[Key] extends (...args: any[]) => any ? ReturnType<T[Key]> : never
}

type UserDelegate = Prisma.UserDelegate<Prisma.RejectOnNotFound | Prisma.RejectPerOperation | undefined>

class UserModel extends DbService<UserDelegate, DelegateArgs<UserDelegate>, DelegateReturnTypes<UserDelegate>> {
  constructor() {
    super(prisma.user)
  }
}
const User = new UserModel()

async function main() {
  const userList = await User.findFirst()
}

@jrmyio
Copy link

jrmyio commented Dec 29, 2022

@SahasSaurav your Args and ReturnTypes are not connected, meaning if you do findMany({ select: { username: true }) the return type will be User instead of { username: string }.

@darr1s
Copy link

darr1s commented May 15, 2023

Hi there, based on @SahasSaurav latest solution, the return type is incorrect when we use include or select. Has anyone gotten the generics to work better?

@antoniofisherman-job
Copy link

@darr1s same question
Hi there, based on @SahasSaurav latest solution, the return type is incorrect when we use include or select. Has anyone gotten the generics to work better?

@esmaeilzadeh
Copy link

esmaeilzadeh commented Aug 31, 2023

📦 Repository Approach: Simplified Yet Effective

To address this, I've found a straightforward solution that closely aligns with the principles of the repository pattern. While not adhering strictly to the traditional repository structure, it effectively achieves the primary objectives of decoupling code from infrastructure code (an essential consideration given that the Prisma client is autogenerated. A sudden change in a model name could potentially break your entire codebase and your IDE can't help you to apply your changes.) Additionally, this solution segregates general functionalities (e.g., findUnique, findAll) from domain-specific operations.

📂 Repository Code:

import { Injectable } from '@nestjs/c mon';
import { Prisma, UserDelegate } from '@prisma/client';
import { PrismaService } from 'nestjs-prisma';
import { EmailXORPhone } from '../auth/dto/EmailXORPhone';

@Injectable()
export class UserService {
  private readonly m: UserDelegate;

  constructor(private readonly prisma: PrismaService) {
    this.m = prisma.user;
  }

  get model() {
    return this.m;
  }

  async findByEmailXorPhone(emailOrPhone: EmailXORPhone) {
    return this.model.findUnique({
      where: emailOrPhone,
    });
  }
}

📋 Client Code:

export class AuthService {
  constructor(
      private readonly passwordService:PasswordService,
      private readonly userService:UserService
  ) {

  }
  async register(emailOrPhone: EmailXORPhone, info: RegisterDto): Promise<RegisterOutput> {
    const result = await this.userService.findByEmailXorPhone(emailOrPhone);
  
    if (!result) {
      const code = this.passwordService.getRandomReferral();
      const password = this.passwordService.getRandomPassword();
  
      return this.userService.model.create({
        data: {
          ...emailOrPhone,
          ...info,
          code,
          password,
        },
        select: {
          id: true,
          code: true,
        },
      });
    } else {
      throw new Error(this.i18nService.t("user already registered."));
    }
  }
}

🛡️ This approach with minimum redundant code without any compromising in type safety, empowers you to define a controlled policy in your code, restricting direct access to the Prisma client across your codebase, except within repositories. Additionally, within each repository, you can exclusively query or perform actions on its corresponding model (along with its relationships), ensuring a structured and secure data access pattern.

@lytaitruong
Copy link

I've try on this in Prisma 4.x but failed

Right not I see Prisma go to 5.x and try again. It working as my expected but not perfectly (not clean code 😢)

You can try on it and give another better way to enhance this case

import { Prisma } from '@prisma/client'
import { PrismaService } from './prisma.service'

export abstract class PrismaRepository<T extends Prisma.ModelName> {
  constructor(
    protected readonly prisma: PrismaService,
    protected readonly model: Lowercase<T>,
  ) {}

  findMany(
    input: Prisma.TypeMap['model'][T]['operations']['findMany']['args'],
  ): Promise<Prisma.TypeMap['model'][T]['operations']['findMany']['result']> {
    // It intersection type
    // return this.prisma[this.model].findMany(input)
    return (this.prisma[this.model] as unknown as any).findMany(input)
  }
}

export class UserService extends PrismaRepository<'User'> {
  constructor(protected readonly prisma: PrismaService) {
    super(prisma, 'user')
  }
}

export class UserController {
  constructor(private readonly service: UserService) { }
  
  getAll() {
    return this.service.findMany()
  }
}

But it working good as my expected
Example

@aprilmintacpineda
Copy link

aprilmintacpineda commented Nov 5, 2023

One thing to keep in mind is that Repository pattern is a design pattern that isolates your data layer from the rest of the app. So when we do things like:

class UsersRepository {
  create (input: Prisma.UserCreateInput) {
    return Prisma.user.create(input);
  }
}

Is already anti-pattern. Why? Because now, any other APIs in our app that uses this repository, is directly tied to Prisma. If later down the line when we decide to switch from Prisma to something else, any APIs callingUserRepository.create will likely also have to change unless we ensure that we accept the same exact type for input. If other layers of your app still needed to change after you migrated database / ORM, then you didn't implement repository pattern successfully. So the best thing to do, is to actually have your own API. That should result in something like this:

type BaseModel = {
  id: string;
  createAt: Date;
  updatedAt: Date;
  deletedAt?: Date;
};

type UserModel = BaseModel & {
  name: string;
  email: string;
};

type CompanyModel = BaseModel & {
  name: string;
};

type CreateCompanyInput = {
  name: string;
};

type CreateUserInput = {
  name: string;
  email: string;
} & (
  | {
      connectCompanyId?: string;
      createCompany?: never;
    }
  | {
      connectCompanyId?: never;
      createCompany?: CreateCompanyInput | CreateCompanyInput[];
    }
);

class UsersRepository {
  async create (input: CreateUserInput) {
    const { connectCompanyId, createCompany, ..._input } = input;

    const result = await Prisma.user.create({
      ..._input,
      company: connectCompanyId
        ? {
            connect: {
              id: connectCompanyId,
            },
          }
        : createCompany
        ? {
            create: createCompany,
          }
        : undefined,
    });

    return result as unknown as UserModel;
  }
}

At least this is how I understood the point of repository pattern. My observation is prisma doesn't seem to be very well suited for this because prisma generates everything for us, Models, Inputs, etc, so we will be forced to do things like as unknown as UserModel.

@pankucsi
Copy link

pankucsi commented Nov 8, 2023

I've try on this in Prisma 4.x but failed

Right not I see Prisma go to 5.x and try again. It working as my expected but not perfectly (not clean code 😢)

You can try on it and give another better way to enhance this case

import { Prisma } from '@prisma/client'
import { PrismaService } from './prisma.service'

export abstract class PrismaRepository<T extends Prisma.ModelName> {
  constructor(
    protected readonly prisma: PrismaService,
    protected readonly model: Lowercase<T>,
  ) {}

  findMany(
    input: Prisma.TypeMap['model'][T]['operations']['findMany']['args'],
  ): Promise<Prisma.TypeMap['model'][T]['operations']['findMany']['result']> {
    // It intersection type
    // return this.prisma[this.model].findMany(input)
    return (this.prisma[this.model] as unknown as any).findMany(input)
  }
}

export class UserService extends PrismaRepository<'User'> {
  constructor(protected readonly prisma: PrismaService) {
    super(prisma, 'user')
  }
}

export class UserController {
  constructor(private readonly service: UserService) { }
  
  getAll() {
    return this.service.findMany()
  }
}

But it working good as my expected Example

Hello, @lytaitruong.

Have you improved this code somehow or do you use it like that still ? I like this approach, but if you look at the return types the $Utils.PayloadToResult converts every property to optional, or maybe I miss something ?

@simonsan
Copy link

simonsan commented Nov 21, 2023

I'm coming from another language and I'm new to Prisma and Typescript, so I scratched out some half-way pseudo code. But I ask myself if implementing the Repository pattern could work like this in Typescript. Would be lovely to hear feedback for improvements:

import type { models } from "$lib/sequelize";
import type { playerAttributes } from "$lib/models/player";

type PlayerId = playerAttributes["id"];
type PlayerData = playerAttributes;
type Player = typeof models.player;

interface IPlayerRepositoryInterface {
    getAllPlayers(): Promise<PlayerData[]>;
    getAllPlayersPaginated(): Promise<PlayerData[]>;
    getAllPlayersPartiallyCached(): Promise<Partial<PlayerData[]>>;
    getPlayerById(player_id: PlayerId): Promise<PlayerData | null>;
    createPlayer(player_details: Partial<PlayerData>, user_id: number, actionlog_summary: string): Promise<PlayerId>;
    updatePlayer(player_id: PlayerId, new_player_details: Partial<PlayerData>, user_id: number, actionlog_summary: string): Promise<boolean>;
    deletePlayer(player_id: PlayerId, user_id: number, actionlog_summary: string): Promise<boolean>;
}

export default class PlayerRepository implements IPlayerRepositoryInterface {

    constructor(private readonly player_model: Player) { }

    async getAllPlayers(): Promise<PlayerData[]> {
        return this.player_model.findAll();
    }

    async getAllPlayersPaginated(): Promise<PlayerData[]> {
        throw new Error("Method not implemented.");
    }

    async getAllPlayersPartiallyCached(): Promise<Partial<PlayerData[]>> {
        return this.player_model.findAll({ attributes: ["id", "name"] })
    }

    async getPlayerById(player_id: PlayerId): Promise<PlayerData | null> {
        return this.player_model.findByPk(player_id);
    }

    createPlayer(player_details: Partial<PlayerData>, user_id: number, actionlog_summary: string): Promise<PlayerId> {
        throw new Error("Method not implemented.");
    }

    updatePlayer(player_id: PlayerId, new_player_details: Partial<PlayerData>, user_id: number, actionlog_summary: string): Promise<boolean> {
        throw new Error("Method not implemented.");
    }
    deletePlayer(player_id: PlayerId, user_id: number, actionlog_summary: string): Promise<boolean> {
        throw new Error("Method not implemented.");
    }
}

export class MockPlayerRepository implements IPlayerRepositoryInterface {

    constructor(/* empty */) { }

    async getAllPlayers(): Promise<PlayerData[]> {
        return [{ id: 1, name: "Test", country_id: 123 } as PlayerData, { id: 2, name: "Test2", country_id: 123 } as PlayerData];
    }

    async getAllPlayersPaginated(): Promise<Partial<PlayerData[]>> {
        throw new Error("Method not implemented.");
    }

    async getAllPlayersPartiallyCached(): Promise<PlayerData[]> {
        return [{ id: 1, name: "Test", country_id: 123 } as PlayerData, { id: 2, name: "Test2", country_id: 123 } as PlayerData];
    }

    async getPlayerById(player_id: PlayerId): Promise<PlayerData | null> {
        return { id: player_id, name: "Test", country_id: 123 } as PlayerData;
    }

    createPlayer(player_details: Partial<PlayerData>, user_id: number, actionlog_summary: string): Promise<PlayerId> {
        return Promise.resolve(1);
    }

    updatePlayer(player_id: PlayerId, new_player_details: Partial<PlayerData>, user_id: number, actionlog_summary: string): Promise<boolean> {
        return Promise.resolve(false);
    }

    deletePlayer(player_id: PlayerId, user_id: number, actionlog_summary: string): Promise<boolean> {
        return Promise.resolve(true);
    }
}

@lFitzl
Copy link

lFitzl commented Jan 1, 2024

Check this implementation: #3929 (comment)

@janpio janpio changed the title [Feat]: CRUD Wrapper Service (Help, Idea, Problems, Possible Solutions) CRUD Wrapper Service (Help, Idea, Problems, Possible Solutions) Feb 16, 2024
@janpio janpio changed the title CRUD Wrapper Service (Help, Idea, Problems, Possible Solutions) Service Classes (e.g. for NestJS) Feb 19, 2024
@janpio
Copy link
Member

janpio commented Feb 19, 2024

FYI: I renamed this issue to something that hopefully describes better what it is about. The previous name was very broad, but it seems to be specific for service classes effectively. Let me know if someone disagrees with the new title (optimally with an alternative suggestion).

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. team/client Issue for team Client. tech/typescript Issue for tech TypeScript. topic: extend-client Extending the Prisma Client topic: NestJS
Projects
None yet
Development

No branches or pull requests