question-mark
Stuck on an issue?

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

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

[Feat]: CRUD Wrapper Service (Help, Idea, Problems, Possible Solutions)

See original GitHub issue

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

Issue Analytics

  • State:open
  • Created 3 years ago
  • Reactions:114
  • Comments:44 (14 by maintainers)

github_iconTop GitHub Comments

32reactions
johannesschobelcommented, Jan 26, 2021

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

25reactions
Yuliang-Leecommented, Dec 1, 2021

Is this has offcial support or solution now?

Read more comments on GitHub >

github_iconTop Results From Across the Web

PIKA-lab / Courses / Distributed Systems / Distributed Systems A.Y. ...
Lab 6 -- Web Services. Project ID: 232 ... What is a resource? A type of entity to be managed through a web...
Read more >
Templates or Scaffolding for CRUD server actions - Read Only ...
What I'm not finding are templates or simple ways of creating a core set of CRUD routines / server actions per entity.
Read more >
Modern Software Over-Engineering Mistakes | by RDX - Medium
If we change the underlying library later, usages of this wrapper everywhere usually end up having to be changed as well. Sometimes we...
Read more >
Spirng Boot CRUD operations with hibernate - Stack Overflow
Your query is also leaving you susceptible to sql injection attack. Look into JQPL and named queries or JPARepository class. – locus2k. Jul...
Read more >
The Grails Framework 2.4.5 - GitHub Pages
See the dependency injection and services docs. Domain classes provided by a plugin will have their default database table name prefixed with the...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

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

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found