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
Ability to extend PrismaClient
class w/ Client Extensions before instantiation
#18628
Comments
Related #16500 (comment) #16500 (comment) |
@millsp thanks for pointing me to this issue. I'd support this for simplicity's sake and because I think it will make future post-generation changes and extensions to the Prisma Client easier to support in NestJS. For what it's worth, my team is using client extensions in NestJS currently with type-safety via the following workaround:
export type PrismaService = ReturnType<BasePrismaService["withExtensions"]>;
export class BasePrismaService extends PrismaClient {
// ... optionally, a constructor that performs custom connection or middleware setup
withExtensions() {
return this.$extends(/* Client Extensions Here */);
}
// ... startup + shutdown hooks omitted
}
export const PRISMA_INJECTION_TOKEN = "PrismaService";
@Module({
providers: [
{
provide: PRISMA_INJECTION_TOKEN,
useFactory(): PrismaService {
return new BasePrismaService().withExtensions();
}
}
],
exports: [PRISMA_INJECTION_TOKEN]
})
export class PrismaModule {} The pro of this workaround is that it creates the same with-extension typesafety this Issue is seeking to replicate using only currently-available interfaces. The con is that fully-dynamic dependency injection is not possible because the "type" |
Hey NestJS users, I tried to come up with a workaround today so that you can easily load an extended client while keeping it type-safe. The trick was to use a helper function that would create a wrapper class while preserving type-safety. class MyService extends getExtendedClient() {
async getUserInfo(id: number) {
const user = await this.user.findFirst({
where: { id },
})
}
}
function getExtendedClient() {
const client = () => new PrismaClient().$extends({
// your extension configuration
})
return class { // wrapper with type-safety 🎉
constructor() { return client() }
} as (new () => ReturnType<typeof client>)
} Let me know if that works for you. |
Thanks for your suggestion @millsp, it didn't work off the bat for me, but with a bit more effort I ended up with a solution that I'm quite happy with: function extendPrismaClient() {
const logger = new Logger('Prisma');
const prisma = new PrismaClient();
return prisma.$extends({
client: {
async onModuleInit() {
// Uncomment this to establish a connection on startup, this is generally not necessary
// https://www.prisma.io/docs/concepts/components/prisma-client/working-with-prismaclient/connection-management#connect
// await Prisma.getExtensionContext(this).$connect();
},
async enableShutdownHooks(app: INestApplication) {
Prisma.getExtensionContext(prisma).$on('beforeExit', async () => {
await app.close();
});
}
},
query: {
$allModels: {
async $allOperations({ operation, model, args, query }) {
const start = performance.now();
const result = await query(args);
const end = performance.now();
const time = end - start;
logger.debug(`${model}.${operation} took ${time}ms`);
return result;
}
}
}
});
}
// https://github.com/prisma/prisma/issues/18628
const ExtendedPrismaClient = class {
constructor() {
return extendPrismaClient();
}
} as new () => ReturnType<typeof extendPrismaClient>;
@Injectable()
export class PrismaService extends ExtendedPrismaClient {}
@Global()
@Module({
exports: [PrismaService],
providers: [PrismaService]
})
export class PrismaModule {} |
I agree that this is sub-optimal not Nest users. We will get to future requests when we get to prioritizing them. In the mean time I'd like to be able to provide a decent-enough workaround for NestJS users. I am not super happy that you had to move Nest logic into the Prisma Client extension. Let's see if I can help further. I created this snippet, would you mind trying it? import { Prisma, PrismaClient } from "@prisma/client";
class MyService extends getExtendedClient() {
async displayUserModelName() {
this.user.test()
}
}
function getExtendedClient() {
const client = () => new PrismaClient().$extends({
// your extension configuration (example)
model: {
$allModels: {
test() {
const ctx = Prisma.getExtensionContext(this)
console.log(`${ctx.name} is the model being called`)
}
}
}
})
// some proxy magic to make the workaround
return new Proxy(class {}, {
construct(target, args, newTarget) {
return Object.assign(Reflect.construct(target, args, newTarget), client())
}
}) as (new () => ReturnType<typeof client>)
}
const service = new MyService()
async function main() {
const data = await service.displayUserModelName()
}
void main() |
@gamedevsam I'm surprised your ts compiler isn't yelling about the typing issue on
|
I'm using the latest TypeScript version (5.1.3), and latest prisma & client (4.16.0). @millsp unfortunately your solution still doesn't work for me, I get a runtime error when getting the extension context (also need a forced typecast to get around typing issues):
|
Scratch that, I was able to get it to work. Can't say I love the idea of wrapping all prisma operations in another Proxy, but it's definitely an improvement over the first attempt. I'm looking forward to seeing a more elegant / official solution to integrate extended prisma clients with NestJS. Here's the resulting working code: const prisma = new PrismaClient();
const logger = new Logger("Prisma");
function extendedClient() {
const extendClient = () =>
prisma.$extends({
query: {
$allModels: {
async $allOperations({ operation, model, args, query }) {
const start = performance.now();
const result = await query(args);
const end = performance.now();
const time = end - start;
logger.debug(
`${model}.${operation}(${JSON.stringify(args)}) took ${time}ms`
);
return result;
},
},
},
});
// https://github.com/prisma/prisma/issues/18628#issuecomment-1601958220
return new Proxy(class {}, {
construct(target, args, newTarget) {
return Object.assign(
Reflect.construct(target, args, newTarget),
extendClient()
);
},
}) as new () => ReturnType<typeof extendClient>;
}
@Injectable()
export class PrismaService extends extendedClient() {
async onModuleInit() {
// Uncomment this to establish a connection on startup, this is generally not necessary
// https://www.prisma.io/docs/concepts/components/prisma-client/working-with-prismaclient/connection-management#connect
// await Prisma.getExtensionContext(prisma).$connect();
}
async enableShutdownHooks(app: INestApplication) {
Prisma.getExtensionContext(prisma).$on("beforeExit", async () => {
await app.close();
});
}
} |
Yes @gamedevsam I expect this feature request to be top-priority once we get to implementing more features for Client Extensions. |
@andrewmurraydavid are you using the latest version |
@millsp could you provide a suggestion to extend the solution @gamedevsam most recently posted to include getting access to an injected service in the extendedClient function. For example, if we get the isProd variable from the configService from |
At the time of writing my first comment, I was using |
I've been trying to play around with Client Extensions, but I got some issues with it (maybe somebody else have had this problem before). ProblemOur project uses Once I call
When I remove Does anyone have an idea about this?
ReproduceI've got a playground repo with the reproduced issue: https://github.com/alex-deity/test-nest-prisma (the main file is app.module.ts). |
this code makes |
@ThallesP that's not happening for me, for some reason I cannot Ctrl+Click to go to definition, but I do get proper types for the function: TypeScript correctly picks up type information: You should double check that both Prisma and TypeScript are updated to their latest version. |
Typescript types are working, but when calling the function it breaks. |
Ohh yeah that could be, I didn't test this out at runtime as I don't yet have a use for |
The health check package You can work around it by creating a separate export of the client (before extending) and using it directly in the health controller but that avoids using nest's dependency injection and strays from documentation. |
hello guys i came across this issue too but i didnt face any issue with the following code @prisma/client:
import { DynamicModule, Logger, Module } from '@nestjs/common';
import { PRISMA_MASTER_CONNECTION, PRISMA_READ_CONNECTION } from './constants';
import { PrismaService } from './prisma.service';
import { prismaExtensionFactory } from './prisma.extension';
import { ConfigService } from '@nestjs/config';
@Module({})
export class PrismaModule {
static forRoot(): DynamicModule {
return {
global: true,
module: PrismaModule,
providers: [
{
provide: PRISMA_MASTER_CONNECTION,
useFactory: () => {
const logger = new Logger(PRISMA_MASTER_CONNECTION);
return prismaExtensionFactory(
new PrismaService({
log: [{ emit: 'event', level: 'query' }],
}),
logger,
);
},
},
{
provide: PRISMA_READ_CONNECTION,
useFactory: (configService: ConfigService) => {
const logger = new Logger(PRISMA_READ_CONNECTION);
return prismaExtensionFactory(
new PrismaService({
datasources: {
db: {
url: configService.get('READ_REPLICATION_DATABASE_URL'),
},
},
log: [{ emit: 'event', level: 'query' }],
}),
logger,
);
},
inject: [ConfigService],
},
],
exports: [PRISMA_MASTER_CONNECTION, PRISMA_READ_CONNECTION],
};
}
}
import { Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
async onModuleInit() {
await this.$connect();
}
}
import { INestApplication, Logger } from '@nestjs/common';
import { Prisma, PrismaClient } from '@prisma/client';
export const prismaExtensionFactory = (
client: PrismaClient<Prisma.PrismaClientOptions, 'query'>,
logger: Logger,
) => {
client.$on('query', (e) => {
logger.debug(`${e.query} -- ${e.params} duration: ${e.duration}ms`);
});
return client.$extends({
client: {
async enableShutdownHooks(app: INestApplication) {
Prisma.getExtensionContext(client).$on('beforeExit', async () => {
logger.log('Gracefully shutdown prisma');
await app.close();
});
},
},
});
};
export type ExtendedPrismaClient = ReturnType<typeof prismaExtensionFactory>; |
@jl9404 The approach you shared is typesafe and didn't throw errors in the healthcheck module, and allows the use of injected dependencies by passing them through to the factory. Works well for me |
In my case, using @jl9404’s approach with EventEmitter (@nestjs/@nestjs/event-emitter) throws an error.
Just import modules in AppModule like below. …
@Module({
imports: [
PrismaModule.forRoot(),
EventEmitterModule.forRoot(),
…
],
…
})
export class AppModule {}
… Am I using it wrong? Please give me a solution. |
i guess you have to patch the package cause the event emitter module scan the provider and check the prototype of prisma client diff --git a/node_modules/@nestjs/event-emitter/dist/events-metadata.accessor.js b/node_modules/@nestjs/event-emitter/dist/events-metadata.accessor.js
index 53f038e..c06eae9 100644
--- a/node_modules/@nestjs/event-emitter/dist/events-metadata.accessor.js
+++ b/node_modules/@nestjs/event-emitter/dist/events-metadata.accessor.js
@@ -18,6 +18,9 @@ let EventsMetadataAccessor = class EventsMetadataAccessor {
this.reflector = reflector;
}
getEventHandlerMetadata(target) {
+ if (!target) {
+ return undefined;
+ }
const metadata = this.reflector.get(constants_1.EVENT_LISTENER_METADATA, target);
if (!metadata) {
return undefined; |
Good job, @gamedevsam! I decided to get your implementation and mix it up with mine. Here is what I got and 100% type-safe. // database.module.ts
import { Global, Module } from '@nestjs/common'
import { DatabaseService } from '@/database/database.service'
@Global()
@Module({
providers: [DatabaseService],
exports: [DatabaseService],
})
export class DatabaseModule {} // database.service.ts
import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common'
import { Prisma, PrismaClient } from '@prisma/client'
export const prismaExtendedClient = (prismaClient: PrismaClient) =>
prismaClient.$extends({
model: {
$allModels: {
async softDelete<M, A>(
this: M,
where: Prisma.Args<M, 'update'>['where'],
): Promise<Prisma.Result<M, A, 'update'>> {
const context = Prisma.getExtensionContext(this)
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- There is no way to type a Prisma model
return (context as any).update({
where,
data: {
deletedAt: new Date(),
},
})
},
async isDeleted<M>(this: M, where: Prisma.Args<M, 'findUnique'>['where']): Promise<boolean> {
const context = Prisma.getExtensionContext(this)
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- There is no way to type a Prisma model
const result = await (context as any).findUnique({ where })
return !!result.deletedAt
},
},
},
})
@Injectable()
export class DatabaseService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
readonly extendedClient = prismaExtendedClient(this)
constructor() {
super()
new Proxy(this, {
get: (target, property) => Reflect.get(property in this.extendedClient ? this.extendedClient : target, property),
})
}
async onModuleInit() {
await this.$connect()
}
async onModuleDestroy() {
await this.$disconnect()
}
} Now, I can use the extended methods like this: await this.databaseService.extendedClient.user.softDelete({ id }) And, I also can use like the normal way: return await this.databaseService.user.findFirstOrThrow({
where: { id },
}) |
Nicely done, I feel we could keep trimming things down and try to keep everything contained within the service, but to be frank the core of the solution is the Proxy + separate extended client for type safety. Everything else is just icing on the cake. Hopefully the Prisma team takes this pattern and makes it more widely available to NestJS users via official docs. |
Just a last tip. It's possible to split all extended Prisma methods into separate files, but you must declare the methods using a function instead of an arrow function. Doing that will make it much easier to maintain the code. const softDelete = async function <M, A>(
this: M,
where: Prisma.Args<M, 'update'>['where'],
): Promise<Prisma.Result<M, A, 'update'>> {
const context = Prisma.getExtensionContext(this)
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- There is no way to type a Prisma model
const result = (context as any).update({
where,
data: {
deletedAt: new Date(),
},
})
return result
}
const isDeleted = async function <M>(this: M, where: Prisma.Args<M, 'findUnique'>['where']): Promise<boolean> {
const context = Prisma.getExtensionContext(this)
// eslint-disable-next-line -- There is no way to type a Prisma model
const result = await (context as any).findUnique({ where })
return !!result.deletedAt
}
export const prismaExtendedClient = (prismaClient: PrismaClient) =>
prismaClient.$extends({
model: {
$allModels: {
softDelete,
isDeleted,
},
},
}) |
Thanks, looks like the simplest solution. Any way to make it work with typescript? |
@MustagheesButt look here. The code works with Typescript, and it is type-safe. |
I feel my solution, or Leandro's or some combination (they are largely the same, with minor differences in style / conciseness) is a "good enough" solution. I recommend closing this issue. |
This worked for me. @Global()
@Module({
providers: [
{
provide: PrismaService,
useFactory: async (config: ConfigService<EnvironmentVariables, true>) => {
return new PrismaService(config).initPrismaWithClientExtension();
},
inject: [ConfigService<EnvironmentVariables, true>],
},
],
exports: [PrismaService],
})
export class PrismaModule {} import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Prisma, PrismaClient } from '@prisma/client';
import { EnvironmentVariables } from '../config';
import { prismaExtendedClient } from './prisma.extension';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
private static instance: PrismaService;
private readonly logger = new Logger(PrismaService.name);
constructor(
private readonly config: ConfigService<EnvironmentVariables, true>,
) {
if (PrismaService.instance) {
return PrismaService.instance;
}
super({
log: [
{ emit: 'event', level: 'query' },
{ emit: 'stdout', level: 'error' },
{ emit: 'stdout', level: 'info' },
{ emit: 'stdout', level: 'warn' },
],
datasources: { db: { url: config.get('DATABASE_URL', { infer: true }) } },
});
PrismaService.instance = this;
}
async initPrismaWithClientExtension() {
return await prismaExtendedClient(this);
}
async onModuleInit() {
await this.$connect();
if (this.config.get('PRISMA_LOG', { infer: true })) {
//@ts-expect-error: known bug in prism missing proper type
this.$on('query', (e: Prisma.QueryEvent) => {
this.logger.log(
JSON.stringify({
duration: `${e.duration}ms`,
query: e.query,
params: e.params,
}),
);
});
}
}
} export const prismaExtendedClient = async (client: PrismaClient) =>
client.$extends({}) |
does this still work? I've just tried it with Prisma 5.6.0 and Prisma client 5.6.0 and it hangs at |
Try a solution based on this example, customize it for your needs. I haven't yet tried to update Prisma, but previous examples were not good starting points imo (avoid anything with |
Also this thread if getting out of hand, anyone can close it? Not everyone needs to share their minor variation on the same solution. Use mine as a starting point and customize it as you like. Only share your solution if it's significantly different plz (to combat noise). |
@gamedevsam While I agree I do think this should be something official, not something the community has to hack around. |
I think there needs to be a solution from Prisma to this issue because the variants on @gamedevsam's solution still don't address the underlying issue - until Prisma gives us a way to create a
None of our solutions thus far (afaict) avoid all three of these, and I think that's why we want to keep this issue open until Prisma gets around to giving us an extension tool that can be typed prior to instantiation. |
Is the issue resolved? |
This thread is not out of hand. While your solution is somewhat acceptable, it's not the best way to make things work. The best way is Prisma team to take a look on this and provide a way to "class"ify the client and provide ways to extend it thru this mechanism while keeping all the types. |
Same problem, have u solved this ? |
I ran up against this issue today and ended up with a solution that seems to meet my needs. The actual extension and some types were adapted from this comment. I'm a bit late to the party and this thread has gotten pretty long, so I apologize in advance if I'm rehashing a solution that's already been discussed. But I believe this approach addresses all 3 of @zackdotcomputer's concerns:
Additionally:
For reference, I'm using // src/prisma/extensions/find-many-count.extension.ts
import { Prisma } from "@prisma/client";
import type { PrismaClient } from "@prisma/client";
export function createFindManyAndCountExtension<TModel = any, TArgs = any>(prisma: PrismaClient) {
return {
name: "findManyAndCount",
model: {
$allModels: {
findManyAndCount(
this: TModel,
args?: Prisma.Exact<TArgs, Prisma.Args<TModel, "findMany">>
): Promise<[Prisma.Result<TModel, TArgs, "findMany">, number]> {
const context = Prisma.getExtensionContext(this);
return prisma.$transaction([
(context as any).findMany(args),
(context as any).count({ where: (args as any)?.where }),
]);
},
},
},
};
}
export type FindManyAndCount<TModel, TArgs> = ReturnType<
typeof createFindManyAndCountExtension<TModel, TArgs>
>["model"]["$allModels"]["findManyAndCount"]; // src/prisma/extended-client.ts
import { PrismaClient } from "@prisma/client";
import { createFindManyAndCountExtension, type FindManyAndCount } from "./extensions/find-many-count.extension";
type ModelsWithExtensions = {
[Model in keyof PrismaClient]: PrismaClient[Model] extends { findMany: (args: infer TArgs) => Promise<any> }
? {
findManyAndCount: FindManyAndCount<PrismaClient[Model], TArgs>;
} & PrismaClient[Model]
: PrismaClient[Model];
};
class UntypedExtendedClient extends PrismaClient {
constructor(options?: ConstructorParameters<typeof PrismaClient>[0]) {
super(options);
return this.$extends(createFindManyAndCountExtension(this)) as this;
}
}
const ExtendedPrismaClient = UntypedExtendedClient as unknown as new (
options?: ConstructorParameters<typeof PrismaClient>[0]
) => PrismaClient & ModelsWithExtensions;
export { ExtendedPrismaClient }; // src/prisma/prisma.service.ts
import type { OnModuleInit, OnModuleDestroy } from "@nestjs/common";
import { Injectable } from "@nestjs/common";
import { ExtendedPrismaClient } from "./extended-client";
@Injectable()
export class PrismaService extends ExtendedPrismaClient implements OnModuleInit, OnModuleDestroy {
async onModuleInit() {
await this.$connect();
}
async onModuleDestroy() {
await this.$disconnect();
}
} // src/prisma/prisma.module.ts
import { Global, Module } from "@nestjs/common";
import { PrismaService } from "./prisma.service";
@Global()
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {} I'm sure there's a gotcha here somewhere that I haven't hit yet, but hopefully this helps! |
yeah this works completely fine! |
PrismaClient
class w/ Client Extensions before instantiation
Just wanted to follow up on my earlier reply with some improvements. That solution wasn't leveraging Prisma's built-in extension helpers, and it relied on some pretty ugly manual typing that very quickly became a pain to manage. This modified approach provides all the same benefits, but leans more heavily on Prisma's built-in types and requires much less boilerplate: // src/prisma/extensions/exists.extension.ts
import { Prisma } from "@prisma/client";
export const existsExtension = Prisma.defineExtension({
name: "exists",
model: {
$allModels: {
async exists<TModel, TArgs extends Prisma.Args<TModel, "findUniqueOrThrow">>(
this: TModel,
args?: Prisma.Exact<TArgs, Prisma.Args<TModel, "findUniqueOrThrow">>
): Promise<boolean> {
const context = Prisma.getExtensionContext(this);
try {
await (context as any).findUniqueOrThrow(args);
return true;
} catch {
return false;
}
},
},
},
}); // src/prisma/extensions/find-many-count.extension.ts
import { Prisma } from "@prisma/client";
import type { FindManyAndCountResult } from "~types";
export const findManyAndCountExtension = Prisma.defineExtension((client) => {
return client.$extends({
name: "findManyAndCount",
model: {
$allModels: {
async findManyAndCount<TModel, TArgs extends Prisma.Args<TModel, "findMany">>(
this: TModel,
args?: Prisma.Exact<TArgs, Prisma.Args<TModel, "findMany">>
): Promise<FindManyAndCountResult<Prisma.Result<TModel, TArgs, "findMany">>> {
const context = Prisma.getExtensionContext(this);
const [records, totalRecords] = await client.$transaction([
(context as any).findMany(args),
(context as any).count({ where: (args as any)?.where }),
]);
const take = (args as any)?.take;
let totalPages = totalRecords === 0 ? 0 : 1;
if (take === 0) {
totalPages = 0;
} else if (typeof take === "number") {
totalPages = Math.ceil(totalRecords / take);
}
return [records, totalRecords, totalPages];
},
},
},
});
}); // src/prisma/extended-client.ts
import { PrismaClient } from "@prisma/client";
import { existsExtension } from "./extensions/exists.extension";
import { findManyAndCountExtension } from "./extensions/find-many-count.extension";
function extendClient(base: PrismaClient) {
// Add as many as you'd like - no ugly types required!
return base.$extends(existsExtension).$extends(findManyAndCountExtension);
}
class UntypedExtendedClient extends PrismaClient {
constructor(options?: ConstructorParameters<typeof PrismaClient>[0]) {
super(options);
return extendClient(this) as this;
}
}
const ExtendedPrismaClient = UntypedExtendedClient as unknown as new (
options?: ConstructorParameters<typeof PrismaClient>[0]
) => ReturnType<typeof extendClient>;
export { ExtendedPrismaClient }; // src/prisma/prisma.service.ts
import type { OnModuleInit, OnModuleDestroy } from "@nestjs/common";
import { Injectable } from "@nestjs/common";
import { ExtendedPrismaClient } from "./extended-client";
@Injectable()
export class PrismaService extends ExtendedPrismaClient implements OnModuleInit, OnModuleDestroy {
async onModuleInit() {
await this.$connect();
}
async onModuleDestroy() {
await this.$disconnect();
}
} // src/prisma/prisma.module.ts
import { Global, Module } from "@nestjs/common";
import { PrismaService } from "./prisma.service";
@Global()
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {} |
I'm using this method until there's a better one:
Usage for Nestjs
|
Implemented an example of custom client integration for NestJS: https://github.com/dminglv/nestjs-prisma-postgres-cluster |
Problem
Within NestJS, the common solution to implementing an injectable Prisma Client instance is to extend
PrismaClient
and add theonModuleInit
andenableShutdownHooks
functions required by NestJS.This typically looks like:
You can use
clientExtensions
to add these functions, however, they can not adjust the type ofPrismaClient
and this scenario requires you to export a properly typed, uninstantiated class for NestJS to use.An example of adding the functions is below:
Suggested solution
Add a static method to
PrismaClient
that is available when using theclientExtensions
preview features that allows you to add methods to the class (and adjust the type) before instantiation.This resulting class describe above would then look something like:
Alternatives
Additional context
This example focuses on NestJS, however, this functionality could prove useful elsewhere as well.
The text was updated successfully, but these errors were encountered: