[Feat]: CRUD Wrapper Service (Help, Idea, Problems, Possible Solutions)
See original GitHub issueDear 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 Service
s, 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:
- all
modelDelegate
methods (i.e,create()
,update()
,findMany()
, …) are unknown, because thedelegate
is not known - the
UserService
with all its generics looks very ugly
Suggested solution
Regarding the drawbacks discussed earlier, i would suggest:
- All generated
Delegate
s (i.e.,UserDelegate
) should extend a basicDelegate
that holds all method descriptions. This way, we could use this basicDelegate
within theCrudService
like so:
@Injectable()
export abstract class CrudService<E, M extends Delegate, C, R, U, D> {
constructor(protected modelDelegate: m) {}
// ...
}
- Maybe, I could create a new
Decorator
, that automatically creates all this “boilerplate code” from theextends 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
Issue Analytics
- State:
- Created 3 years ago
- Reactions:114
- Comments:44 (14 by maintainers)
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. CreatingRepositories
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
InterfaceFirst, i created a
Delegate
Interface that simply contains all methods a respectiveDelegate
(fromPrisma
) offers (i.e., theUserDelegate
. 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.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:
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 theCrudService
. Note that i still need to havedata: 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:
This is basically just some kind of mapping; i.e., the
create
param would usePrisma.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 theCrudService
. However, when doing this, i was not able to use theextends Delegate
anymore, which made all methods unknown again. Maybe we can figure out another solution for this.4. Add a
CrudMapType
InterfaceIn order to make it more accessible (i.e., autocomplete, …) i created an additional interface for the
UserTypeMap
. Again, not very beautiful (i.e., everything isunknown
) but i guess it works, haha 😆5. Refactor
CrudService
With this additional information it is now able to get rid of the
unkown
typings in theCrudService
. Lets review the updated version of my code.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 thatT.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
:I added the
prisma.user
(which is aPrisma.UserDelegate
) within the constructor. This is somehow comparable to therepository
known from typeorm. It gives access to the underlying methods, likecreate()
,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 ofunknown
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 themapping
class Step 4. add respective mapping interfaceThis 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 itsNestJS
). If you use another framework (for example, pureexpress
orfeatherJS
orwhateverJS
, 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
Is this has offcial support or solution now?