Is a repository pattern with generics possible? #3929
-
Is there a way to use generics for repository patterns using a base repository (example A), or should i do it just like example B, where i have to duplicate all the create, update, e.d. methods? Example A// src/repositories/UserRepository.ts
import { User, UserCreateInput } from "@prisma/client";
import { Service } from "typedi";
import PrismaRepository from "./base/PrismaRepository";
@Service()
class UserRepository extends PrismaRepository<User, UserCreateInput> { };
export default UserRepository; // src/repositories/base/PrismaRepository.ts
import prisma from "../../library/prisma";
abstract class PrismaRepository<T, TC> {
public async create(data: TC): Promise<T> {
return await prisma.user.create({ // Is there a way to replace 'user' with a generic?
data,
});
};
};
export default PrismaRepository; Example Bimport { User, UserCreateInput } from "@prisma/client";
import { Service } from "typedi";
import prisma from "../library/prisma";
@Service()
class UserRepository{
public async create(data: UserCreateInput): Promise<User> {
return await prisma.user.create({
data,
});
};
};
export default UserRepository; |
Beta Was this translation helpful? Give feedback.
Replies: 11 comments 29 replies
-
Hey @kevinvdburgt 👋 The example that you mentioned above, it unlikely with Prisma as you are trying to replace I have given a similar answer here on Slack which is quite similar to the pattern that you're following. Do give it a read and let me know if that answers your question :) |
Beta Was this translation helpful? Give feedback.
-
Hi there! I am considering a switch of a project to Prisma, but for example with 4-5 DB Entities that share a common CRUD model, having to create the same Service - Controller patter (Filter - Query, Create, Update, Delete) again and again, just to use the right prisma generated model, seems to me like it violates the DRY principle and also feels a bit unoptimal.... Having a generilization of some kind that would allow to reuse the same code feels more natural. |
Beta Was this translation helpful? Give feedback.
-
IntoPrisma does not support a repository pattern out of the box (for the time being) and i did not like any of the solutions in #5273, so a find a way a better way to code the example B (in my opnion). Better solution for example Bimport { Injectable } from '@nestjs/common';
import { PrismaService } from '../../prisma.service';
@Injectable()
export class UserRepository {
constructor(private prisma: PrismaService) {}
get create() {
return this.prisma.user.create;
}
get delete() {
return this.prisma.user.delete;
}
get deleteMany() {
return this.prisma.user.deleteMany;
}
// And so on...
} Drawbacks
Advantages
So it boils down to your preference, a messier typing, or a messier repository. |
Beta Was this translation helpful? Give feedback.
-
hi I did it like this: create an interface:
create a base repository class:
and finally: create repository class for witch model:
this work for me :) |
Beta Was this translation helpful? Give feedback.
-
import { _Repository } from "./types";
import { PrismaService } from "../prisma/connection";
export abstract class Repository<T> implements _Repository<T> {
private table: string;
private ORM: PrismaService;
constructor(table: string, database: PrismaService) {
this.table = table;
this.ORM = database;
}
_create<Includes = T>(data: Includes): Promise<void | T | any> {
return this.ORM[this.table].create({ ...data });
}
_delete(id: number): Promise<boolean | void | T> {
return this.ORM[this.table].delete({
where: { id },
});
}
_exists<Includes = Partial<T>>(data: Includes): Promise<boolean | T> {
return this.ORM[this.table].findFirst({
where: data,
});
}
_findAll(): Promise<any> {
return this.ORM[this.table].findMany();
}
_findById(id: number): Promise<void | T> {
return this.ORM[this.table].findFirst({
where: {
id,
},
});
}
} |
Beta Was this translation helpful? Give feedback.
-
I'm trying something like this, which gives you the proper args and return types // something that looks like a prisma model
// define what we need to be able to call
type PrismaDelegate<T> = T extends {
findUnique: (args: infer FindUniqueArgs) => infer FindUniqueReturn;
findUniqueOrThrow: (args: infer FindUniqueOrThrowArgs) => infer FindUniqueOrThrowReturn;
update: (args: infer UpdateArgs) => infer UpdateReturn;
}
? {
findUnique: (args: FindUniqueArgs) => FindUniqueReturn;
findUniqueOrThrow: (args: FindUniqueOrThrowArgs) => FindUniqueOrThrowReturn;
update: (args: UpdateArgs) => UpdateReturn;
}
: never; |
Beta Was this translation helpful? Give feedback.
-
I did the following, maybe it will be useful to someone import { PrismaClient } from "@prisma/client";
import { PrismaService } from "../prisma.service";
export class PrismaRepository<K extends Exclude<keyof PrismaClient, symbol | `$${string}`>> {
private readonly model!: K
constructor(private readonly prisma: PrismaService) {}
aggregate(...args: Parameters<PrismaClient[K]['aggregate']>) {
return (this.prisma[this.model].aggregate as any)(...args);
}
count(...args: Parameters<PrismaClient[K]['count']>) {
return (this.prisma[this.model].count as any)(...args);
}
create(...args: Parameters<PrismaClient[K]['create']>) {
return (this.prisma[this.model].create as any)(...args);
}
createMany(...args: Parameters<PrismaClient[K]['createMany']>) {
return (this.prisma[this.model].createMany as any)(...args);
}
delete(...args: Parameters<PrismaClient[K]['delete']>) {
return (this.prisma[this.model].delete as any)(...args);
}
findFirst(...args: Parameters<PrismaClient[K]['findFirst']>) {
return (this.prisma[this.model].findFirst as any)(...args);
}
findFirstOrThrow(...args: Parameters<PrismaClient[K]['findFirstOrThrow']>) {
return (this.prisma[this.model].findFirstOrThrow as any)(...args);
}
findMany(...args: Parameters<PrismaClient[K]['findMany']>) {
return (this.prisma[this.model].findMany as any)(...args);
}
findUnique(...args: Parameters<PrismaClient[K]['findUnique']>) {
return (this.prisma[this.model].findUnique as any)(...args);
}
findUniqueOrThrow(...args: Parameters<PrismaClient[K]['findUniqueOrThrow']>) {
return (this.prisma[this.model].findUniqueOrThrow as any)(...args);
}
update(...args: Parameters<PrismaClient[K]['update']>) {
return (this.prisma[this.model].update as any)(...args);
}
updateMany(...args: Parameters<PrismaClient[K]['updateMany']>) {
return (this.prisma[this.model].updateMany as any)(...args);
}
upsert(...args: Parameters<PrismaClient[K]['upsert']>) {
return (this.prisma[this.model].upsert as any)(...args);
}
} Usage example class UserRepository extends PrismaRepository<'user'> {}
const userRepository = new UserRepository(new PrismaService())
userRepository.create({
data: {
email: "test@gmail.com"
}
}) |
Beta Was this translation helpful? Give feedback.
-
I have also been working on a solution for this for my team. We are satisfied with the solution for now, as almost all aspects are covered. This example is a NestJs API. Versions:
/* base.repository.ts */
import {
IBaseRepository,
ModelArgs,
ModelName,
ModelResult,
} from './base-repository.interface';
import { Prisma } from '@prisma/client';
import { ExtendedPrismaClient } from '@core/database';
export class BaseRepository<T extends ModelName> implements IBaseRepository<T> {
constructor(
private readonly _model: Uncapitalize<T>,
private readonly _prismaService: ExtendedPrismaClient, // We are using Prisma Client Extensions
) {}
findUnique<A extends ModelArgs<T, 'findUnique'>>(
args: Prisma.SelectSubset<A, ModelArgs<T, 'findUnique'>>,
): ModelResult<T, A, 'findUnique'> {
return this._prismaService[this._model as Prisma.ModelName].findUnique(
args,
);
}
findUniqueOrThrow<A extends ModelArgs<T, 'findUniqueOrThrow'>>(
args?: Prisma.SelectSubset<A, ModelArgs<T, 'findUniqueOrThrow'>>,
): ModelResult<T, A, 'findUniqueOrThrow'> {
return this._prismaService[
this._model as Prisma.ModelName
].findUniqueOrThrow(args);
}
findFirst<A extends ModelArgs<T, 'findFirst'>>(
args?: Prisma.SelectSubset<A, ModelArgs<T, 'findFirst'>>,
): ModelResult<T, A, 'findFirst'> {
return this._prismaService[this._model as Prisma.ModelName].findFirst(args);
}
findFirstOrThrow<A extends ModelArgs<T, 'findFirstOrThrow'>>(
args?: Prisma.SelectSubset<A, ModelArgs<T, 'findFirstOrThrow'>>,
): ModelResult<T, A, 'findFirstOrThrow'> {
return this._prismaService[
this._model as Prisma.ModelName
].findFirstOrThrow(args);
}
findMany<A extends ModelArgs<T, 'findMany'>>(
args?: Prisma.SelectSubset<A, ModelArgs<T, 'findMany'>>,
): ModelResult<T, A, 'findMany'> {
return this._prismaService[this._model as Prisma.ModelName].findMany(args);
}
create<A extends ModelArgs<T, 'create'>>(
args: Prisma.SelectSubset<A, ModelArgs<T, 'create'>>,
): ModelResult<T, A, 'create'> {
return this._prismaService[this._model as Prisma.ModelName].create(args);
}
createMany<A extends ModelArgs<T, 'createMany'>>(
args?: Prisma.SelectSubset<A, ModelArgs<T, 'createMany'>>,
): ModelResult<T, A, 'createMany'> {
return this._prismaService[this._model as Prisma.ModelName].createMany(
args,
);
}
update<A extends ModelArgs<T, 'update'>>(
args: Prisma.SelectSubset<A, ModelArgs<T, 'update'>>,
): ModelResult<T, A, 'update'> {
return this._prismaService[this._model as Prisma.ModelName].update(args);
}
updateMany<A extends ModelArgs<T, 'updateMany'>>(
args: Prisma.SelectSubset<A, ModelArgs<T, 'updateMany'>>,
): ModelResult<T, A, 'updateMany'> {
return this._prismaService[this._model as Prisma.ModelName].updateMany(
args,
);
}
delete<A extends ModelArgs<T, 'delete'>>(
args: Prisma.SelectSubset<A, ModelArgs<T, 'delete'>>,
): ModelResult<T, A, 'delete'> {
return this._prismaService[this._model as Prisma.ModelName].delete(args);
}
deleteMany<A extends ModelArgs<T, 'deleteMany'>>(
args?: Prisma.SelectSubset<A, ModelArgs<T, 'deleteMany'>>,
): ModelResult<T, A, 'deleteMany'> {
return this._prismaService[this._model as Prisma.ModelName].deleteMany(
args,
);
}
upsert<A extends ModelArgs<T, 'upsert'>>(
args: Prisma.SelectSubset<A, ModelArgs<T, 'upsert'>>,
): ModelResult<T, A, 'upsert'> {
return this._prismaService[this._model as Prisma.ModelName].upsert(args);
}
} /* base-repository.interface.ts */
import { Prisma, PrismaClient } from '@prisma/client';
import {
GetResult as PrismaGetResult,
Operation,
} from '@prisma/client/runtime/library';
interface Clients<
T extends ModelName,
A extends ModelArgs<T, O>,
O extends Operation,
> {
User: Prisma.Prisma__UserClient<GetResult<T, A, O>, never>;
Post: Prisma.Prisma__PostClient<GetResult<T, A, O>, never>;
Category: Prisma.Prisma__CategoryClient<GetResult<T, A, O>, never>;
}
type MethodsWithParams<T, M extends ModelName> = {
[K in keyof T & Operation]: T[K] extends true
? <A extends ModelArgs<M, K>>(
args?: Prisma.SelectSubset<A, ModelArgs<M, K>>,
) => ModelResult<M, A, K>
: <A extends ModelArgs<M, K>>(
args: Prisma.SelectSubset<A, ModelArgs<M, K>>,
) => ModelResult<M, A, K>;
};
export type IBaseRepository<T extends ModelName> = MethodsWithParams<
Operation,
T
>;
/* Prisma models */
export type ModelName = Prisma.ModelName;
/* Prisma model methods */
export type PrismaModelMethods<T extends ModelName> =
keyof PrismaClient[Uncapitalize<T> extends keyof PrismaClient
? Uncapitalize<T>
: never];
type PrismaModelMethod<T extends ModelName> = {
[K in PrismaModelMethods<T>]: PrismaClient[Uncapitalize<T> extends keyof PrismaClient
? Uncapitalize<T>
: never][K];
};
/* Prisma GetResult */
type GetResult<
T extends ModelName,
A extends ModelArgs<T, O>,
O extends Operation,
> = PrismaGetResult<PrismaPayload<T>, A, O>;
/* Prisma payload */
export type PrismaPayload<T extends ModelName> =
Prisma.TypeMap['model'][T]['payload'];
/* Prisma model arguments */
export type ModelArgs<
T extends ModelName,
O extends Operation,
> = PrismaModelMethod<T>[O extends keyof PrismaModelMethod<T>
? O
: never] extends (args: infer A) => any
? A
: never;
/* Prisma model return types */
export type ModelResult<
T extends ModelName,
A extends ModelArgs<T, O>,
O extends Operation,
> = PrismaModelMethod<T> extends {
[K in O]: (args: A) => any;
}
? Clients<T, A, O>[T extends keyof Clients<T, A, O> ? T : never]
: never; The only thing that needs to be adjusted when more models are added is the You can extend the /* user.service.ts */
import { Inject, Injectable } from '@nestjs/common';
import {
BaseRepository,
ExtendedPrismaClient,
PRISMA_MASTER_CONNECTION,
} from '@core/database';
@Injectable()
export class UserService extends BaseRepository<'User'> {
private readonly _prisma: ExtendedPrismaClient;
constructor(@Inject(PRISMA_MASTER_CONNECTION) prisma: ExtendedPrismaClient) {
super('user', prisma);
this._prisma = prisma;
}
} So in your /* user.service.ts */
import { Inject, Injectable } from '@nestjs/common';
import {
BaseRepository,
ExtendedPrismaClient,
PRISMA_MASTER_CONNECTION,
} from '@core/database';
@Injectable()
export class UserService extends BaseRepository<'User'> {
private readonly _prisma: ExtendedPrismaClient;
constructor(
@Inject(PRISMA_MASTER_CONNECTION) prisma: ExtendedPrismaClient,
private readonly _post: PostService,
) {
super('user', prisma);
this._prisma = prisma;
}
async myFunction() {
const posts = await this._post.findMany();
// Do some logic with posts
return this.create(/* Data */);
}
} There are still some methods that are not type safe (e.g. I would appreciate feedback and suggestions for improvement or even other solutions. Have a nice day! |
Beta Was this translation helpful? Give feedback.
-
@lodeli-lulu I noticed that not all methods have correct types And there are the same in some other methods (findFirst, updateMany, createMany) What can it depend on ? |
Beta Was this translation helpful? Give feedback.
-
Beta Was this translation helpful? Give feedback.
-
https://github.com/krsbx/prisma-repo-next |
Beta Was this translation helpful? Give feedback.
Hey @kevinvdburgt 👋
Prisma is quite different from traditional ORM's in the case that it doesn't offser any way to inherit or extend methods as therer are no classes involved. You directly get the data in the form of a simple JavaScript object and not an instance of any class.
The example that you mentioned above, it unlikely with Prisma as you are trying to replace
User
with a generic. That would require a lot of efffort especially with TypeScript as it would need to know what data needs to be passed based on the model specified.I have given a similar answer here on Slack which is quite similar to the pattern that you're following. Do give it a read and let me know if that answers your …