nestjs / typeorm

TypeORM module for Nest framework (node.js) 🍇

Home Page:https://nestjs.com

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

DataSource not available outside of scope of NestJS modules

mango-martin opened this issue · comments

Is there an existing issue that is already proposing this?

  • I have searched the existing issues

Is your feature request related to a problem? Please describe it

In a custom validator, DataSource is not injectable. This was not an issue before when you could call getConnection or any other of the available helper methods. But with the changes of TypeORM 0.3+ it's not possible to use a NestJS instantiated DataSource outside of the context of NestJS modules. You can still use one of the deprecated functions but it looks to me like the NestJS switch to the new version was already done. In this context I think this is an issue that should be addressed.

Note: I might just be missing something and it's possible to do what I proclaim is not working. In this case I am sorry. But then please consider this as a suggestion to clear up the Docs on this matter.

Repro: https://github.com/mango-martin/repro-datasource

main.ts

import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(3000);
}
bootstrap();

app.module.ts replace with anything that works for you

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'root',
      database: 'test',
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

app.controller.ts

import { Body, Controller, Post } from '@nestjs/common';
import { DataSource } from 'typeorm';
import { AppService } from './app.service';
import { TestDto } from './test.dto';

@Controller()
export class AppController {
  constructor(
    private readonly appService: AppService,
    private _dataSource: DataSource,
  ) {
    console.warn(_dataSource); // <-- Works as expected
  }

  @Post()
  test(@Body() _: TestDto): string {
    return this.appService.test();
  }
}

test.dto.ts

import { Constraint } from './validatorconstraint';

export class TestDto {
  @Constraint('test')
  field: any;
}

validatorconstraint.ts

import {
  registerDecorator,
  ValidationOptions,
  ValidatorConstraint,
  ValidatorConstraintInterface,
} from 'class-validator';
import { DataSource } from 'typeorm';

@ValidatorConstraint()
export class SomeConstraint implements ValidatorConstraintInterface {
  constructor(private _dataSource: DataSource) {
    console.warn(_dataSource); // <-- Undefined
  }

  validate() {
    return false;
  }
}

export function Constraint(
  _: any,
  validationOptions?: ValidationOptions,
  compareColumn = 'id',
) {
  return function (object: any, propertyName: string) {
    object[`class_entity_${propertyName}`] = _;
    registerDecorator({
      propertyName,
      target: object.constructor,
      options: validationOptions,
      constraints: [compareColumn],
      validator: SomeConstraint,
    });
  };
}

Describe the solution you'd like

The internally created DataSource instance should be available to inject across the entire project (without needing to import any modules) as it is stated in the docs. Or provide a helper function to get the datasource.

Teachability, documentation, adoption, migration strategy

What is the motivation / use case for changing the behavior?

With the adoption of the changes in TypeORM repo, it would be beneficial to provide some way to access the central DataSource that is instantiated by NestJS wherever you need it.

SomeConstraint instances are not handled by NestJS container, thus you just can't inject things on that class. This isn't related to @nestjs/typeorm btw

@micalevisk
The point is that I can't access the DataSource instance, can I?

In a "vanilla" TypeORM application, they would expect me to have smth like this, right?:

export const myDataSource = new DataSource(options)

In this case I would be able to import/instantiate the datasource wherever I need it.

But since this is handled internally by the TypeOrmModule and not exported I can't access it.

I didn't follow,

image

here you said you could access the instance, now you're saying that you can't 🤔

Where is the useContainers call in your main.ts that tells nest and class-validator to work together on the custom constraints?

import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { useContainer } from 'class-validator';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe());
  useContainer(app.select(Appmodule), { fallbackOnErrors: true })
  await app.listen(3000);
}
bootstrap();

That's what allows custom constraints to let Nest inject values into them.

This came up as I migrated old code to Nest 9 and with it updated TypeORM dependency.

Before we had the accepted { getConnection } method which worked. But now there is no equivalent to that anymore, unless I'm understanding something wrong. My understanding is that you need a direct relation to an instance of a DataSource object

@jmcdo29
I'm getting from your post that there's just something I'm missing. So already I'd like to apologize. I don't want to waste anyone's time. But I did try your suggestion and it's the same result with the code that I provided.

If you can add a docker-compose to your repro I'll take a look and see what's missing (or migrate it from mysql to sqlite)

@jmcdo29
Updated to sqlite. I appreciate your help.

Edit: There is a floating @Inject in the Constraint constructor from an earlier try, but removing it also doesn't help.

You need to add SomeConstraint to a providers array so that Nest knows about the class and can inject the values into it

@jmcdo29
That did it. Thank you so much. Closing this issue. I think it would be cool to append this to the docs somewhere. I might have missed it but I went over pretty much everything and didn't see this case mentioned.
Anyway, thank you so much. 🙏

While this is something that is supported, usually we don't outright recommend it. My personal preference is that class-validator should validate the schema of the incoming request and if you need extra business logic driven validations you should make a dedicated pipe for it. The benefit being you aren't trying to hack into class-validators domain, and you can make the pipe request scoped if need be, which isn't possible with custom validators.

Thank you for your insight. In this case I just wanted to port old code to a working state. But I'll definitely think about what you said when implementing the same scenario in the future.