Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

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

Using extension packages (read replica) #77

Closed
MorenoMdz opened this issue Sep 29, 2023 · 7 comments
Closed

Using extension packages (read replica) #77

MorenoMdz opened this issue Sep 29, 2023 · 7 comments

Comments

@MorenoMdz
Copy link

First, thanks for the solid work here!

I would like to see if it is already possible to use this service with read replicas and or see if it would be possible to add support for this extension.

https://github.com/prisma/extension-read-replicas

Thank you!

@marcjulian
Copy link
Member

Hi @MorenoMdz, this is already possible with Prisma Client Extension. I have added it to the Extensions Example.

import { Prisma, PrismaClient } from '@prisma/client';
// used for Read Replicas example
import { readReplicas } from '@prisma/extension-read-replicas';
export const extendedPrismaClient = new PrismaClient<
Prisma.PrismaClientOptions,
'query' | 'info' | 'warn' | 'error'
>({
log: [
{ level: 'query', emit: 'event' },
{ level: 'info', emit: 'event' },
{ level: 'warn', emit: 'event' },
{ level: 'error', emit: 'event' },
],
})
// FIXME - `$on()` returns void
// .$on('error', (e) => {})
.$extends({
model: {
user: {
findByEmail: async (email: string) => {
console.log('extension findByEmail');
return extendedPrismaClient.user.findFirstOrThrow({
where: { email },
});
},
},
},
})
// Read Replicas example, change datasource prodiver (prisma/schema.prisma) to a supported database
// More details in the blog - https://www.prisma.io/blog/read-replicas-prisma-client-extension-f66prwk56wow
// .$extends(
// readReplicas({
// url: 'postgres://localhost:5432/prisma',
// }),
// );
export type extendedPrismaClient = typeof extendedPrismaClient;

Let me know if this helps you. I will add a section to the docs too.

@marcjulian
Copy link
Member

Checkout the new section for Read Replica in the docs.

@MorenoMdz
Copy link
Author

This is great, I will give it a shot today, thanks again @marcjulian !

@MorenoMdz
Copy link
Author

MorenoMdz commented Oct 4, 2023

@marcjulian I might have to redo our setup, currently we have a PrismaConfigService.ts which we inject in all our services:

import { Injectable } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import { PrismaOptionsFactory, PrismaServiceOptions } from 'nestjs-prisma'
import { getEnvironment, isProduction, isDevelopment } from './utils'

@Injectable()
export class PrismaConfigService implements PrismaOptionsFactory {
  constructor(private configService: ConfigService) {}

  createPrismaOptions(): PrismaServiceOptions | Promise<PrismaServiceOptions> {
    const environment = getEnvironment()
    const isProd = environment === 'production'
    const urlSuffix = isProduction()
      ? '_PROD'
      : // When running locally, and we want to use the DEV database
      isDevelopment() || process.env.FIREBASE_ENV === 'DEV'
      ? '_DEV'
      : ''
    const DB_URL = this.configService.get(`DATABASE_URL${urlSuffix}`)

    return {
      prismaOptions: {
        log: isProd ? undefined : ['info', 'query'],
        datasources: {
          db: {
            url: DB_URL,
          },
        },
      },
      explicitConnect: this.configService.get('DB_EXPLICIT_CONNECT') || false,
    }
  }
}

And then the app.module is initialized like this:

... 
imports: [
    PrismaModule.forRootAsync({
      isGlobal: true,
      useClass: PrismaConfigService,
    }),
    ...
    ]

Should I just redo the setup and or try to plug in the extensions on top of this at the App.module imports section?

@marcjulian
Copy link
Member

marcjulian commented Oct 4, 2023

You will need to use CustomPrismaModule instead of PrismaModule. The reason is, that PrismaModule provides PrismaService which is extended by the default PrismaClient and it cannot be used together with $extends.

Use CustomPrismaModule and inject your ConfigService which you can access in the factory method and pass any options directly to your PrismaClient instance.

import { Module } from '@nestjs/common';
import { CustomPrismaModule } from 'nestjs-prisma';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { extendedPrismaClient } from './prisma.extension';

@Module({
  imports: [
    CustomPrismaModule.forRootAsync({
      name: 'PrismaService',
      useFactory: (config: ConfigService) => {
        const options = {};
        return extendedPrismaClient(options);
      },
      inject: [ConfigService],
      // import ConfigModule when `isGlobal` not true
      // imports: [ConfigModule],
    }),
    ConfigModule.forRoot({
      isGlobal: true,
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
import { Prisma, PrismaClient } from '@prisma/client';
// used for Read Replicas example
import { readReplicas } from '@prisma/extension-read-replicas';

// add more parameters like read replicas urls
export const extendedPrismaClient = (options?: Prisma.PrismaClientOptions, ...) =>
  new PrismaClient<
    Prisma.PrismaClientOptions,
    'query' | 'info' | 'warn' | 'error'
  >(options)
// .$extends(
//   readReplicas({
//     url: 'postgres://localhost:5432/prisma',
//   }),
// );

// add ReturnType because extendedPrismaClient is now a function
export type extendedPrismaClient = ReturnType<typeof extendedPrismaClient>;

Let me know if that helps. I might need to update the docs to make it more clear to use CustomPrismaModule and CustomPrismaService when using $extends.

@MorenoMdz
Copy link
Author

MorenoMdz commented Oct 4, 2023

Thanks again for the response. I am still not sure I get how to configure this with our current setup.

  1. Do I keep the current PrismaConfigService file that is creating the Prisma client options as well as defining the DB_URL? If so, ideally we want to define the read replica URL here too instead of at the extension file. What I am not sure is when using an extension, where do we configure the base Prisma settings? Would I have the setup in all three places, the PrismaConfigService, App module, and the new Prisma extension file?

  2. Do I have to change all places that are currently loading the PrismaService to access the db to use CustomPrismaService<extendedPrismaClient> instead? Seems very verbose, shouldn't PrismaService already access the instance of the custom service everywhere?

Also, the way we have been injecting the Prisma service in our services is like private prisma: PrismaService I would assume that @Inject('PrismaService') is the same?

@marcjulian
Copy link
Member

To clear it up a bit more.

PrismaModule and PrismaService

  1. PrismaService has only access to the default PrismaClient, it does not work with $extends at all.
    Because PrismaService extends PrismaClient, the PrismaClient is created and it cannot be $extends further.
import { Inject, Injectable, Optional } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import { PrismaServiceOptions } from './interfaces';
import { PRISMA_SERVICE_OPTIONS } from './prisma.constants';

@Injectable()
export class PrismaService extends PrismaClient {
  constructor(
    @Optional()
    @Inject(PRISMA_SERVICE_OPTIONS)
    private readonly prismaServiceOptions: PrismaServiceOptions = {},
  ) {
    super(prismaServiceOptions.prismaOptions);

    ...
  }
 
  ...
}
  1. PrismaModule allows passing options to the PrismaClient via forRoot or forRootAsync. forRootAsync allows to pass a config class e.g. PrismaConfigService
  2. Use PrismaService to access the database

CustomPrismaModule and CustomPrismaService

  1. Use CustomPrismaModule and CustomPrismaService when you need to use $extends(...)
import { PrismaClientLike } from './custom-prisma-options';
import { Inject, Injectable } from '@nestjs/common';
import { CUSTOM_PRISMA_CLIENT } from './custom-prisma.constants';

@Injectable()
export class CustomPrismaService<Client extends PrismaClientLike> {
  constructor(
    @Inject(CUSTOM_PRISMA_CLIENT)
    public client: Client,
  ) {}
}

CustomPrismaService gives you access to your extended PrismaClient.

  1. You have access to the PrismaClient instance directly and you can pass your options to it new PrismaClient(YOUR_OPTIONS)
  2. Use CustomPrismaService to access the database
  3. You can provide a config class to CustomPrismaModule
import { ExtendedPrismaConfigService } from './extended-prisma-config.service';

// app.module.ts
CustomPrismaModule.forRootAsync({
      name: 'PrismaService',
      useClass: ExtendedPrismaConfigService,
})


// extended-prisma-config.service.ts
import { Injectable } from '@nestjs/common';
import { CustomPrismaClientFactory } from 'nestjs-prisma';
import { extendedPrismaClient } from './prisma.extension';

@Injectable()
export class ExtendedPrismaConfigService
  implements CustomPrismaClientFactory<extendedPrismaClient>
{
  constructor() {
    // inject config service here
  }

  createPrismaClient(): extendedPrismaClient {
    return extendedPrismaClient;
  }
}

CustomPrismaModule does not provide the extended PrismaClient for PrismaService, you must use CustomPrismaService

I will move this into a discussion as this might help others too.

@marcjulian marcjulian changed the title [Feature request] Add support to read replicas extension Using extension packages (read replica) Oct 5, 2023
@notiz-dev notiz-dev locked and limited conversation to collaborators Oct 5, 2023
@marcjulian marcjulian converted this issue into discussion #78 Oct 5, 2023

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants