简体   繁体   中英

NestJs using a service from another Module in a Custom Repository

Learning NestJs actually and facing an issue saving typeorm OneToMany relation. Let's say I have two modules ProjectsModule @ PlansModule

Exists a OneToMany relation between Plan & Project entities

@Entity()
export class Project extends BaseEntity {

  @PrimaryGeneratedColumn('uuid')
  id: string;
  ...
  @OneToMany(type => Plan, plan => plan.project, { eager: true })
  plans: Plan[];
}
@Entity()
export class Plan extends BaseEntity {

  @PrimaryGeneratedColumn('uuid')
  id: string;
  ...
  @ManyToOne(type => Project, project => project.plans, { eager: false } )
  project: Project;

  @Column()
  projectId: string;
}

In the ProjectsModule, I have a ProjectsService with this method:

  async getProjectById(
    id: string,
    user: User
  ): Promise<Project> {
    const found = await this.projectRepository.findOne({ where: { id, ownerId: user.id } });

    if(!found) {
      throw new NotFoundException(`Project with ID "${id}" not found`)
    }

    return found;
  }

My problem is when I try to save a new Plan. My PlansService calls the PlanRepository like that

 async createPlan(
    createPlanDto: CreatePlanDto,
    user: User
  ): Promise<Plan> {
    return this.planRepository.createPlan(createPlanDto, user);
  }

And on the PlanRepository:


  constructor(
    @Inject(ProjectsService)
    private projectsService: ProjectsService
  ) {
    super();
  }

async createPlan(
    createPlanDto: CreatePlanDto,
    user: User
  ): Promise<Plan> {
    const { title, description, project } = createPlanDto;
    const plan = new Plan();

    const projectFound = await this.projectsService.getProjectById(project, user)

    plan.title = title;
    plan.description = description;
    plan.status = PlanStatus.ENABLED;
    plan.owner = user;
    plan.project = project;

    try {
      await plan.save();
    } catch (error) {
      this.logger.error(`Failed to create a Plan for user "${user.email}". Data: ${JSON.stringify(createPlanDto)}`, error.stack);
      throw new InternalServerErrorException();
    }
    delete plan.owner;
    return plan;
  }

Trying this throws me this error when sending a POST request to my plan controller:

TypeError: this.projectsService.getProjectById is not a function

And trying a

console.log('service', this.projectsService)

give me

service EntityManager {
  repositories: [],
  plainObjectToEntityTransformer: PlainObjectToNewEntityTransformer {},
  connection: Connection {

I guess I'm not using the projectsService properly but I don't understand where I could have made a mistake.

On the module's side I'm exporting the ProjectsService in his module:

exports: [ProjectsService]

And importing the full ProjectsModule into the PlansModule:

imports: [
    TypeOrmModule.forFeature([PlanRepository]),
    AuthModule,
    ProjectsModule
  ],

Sorry for the long post, trying to be exhaustive.

import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { User } from '../auth/user.entity';
import { PlanRepository } from './plan.repository';
import { GetPlanFilterDto } from './dto/get-plan-filter.dto';
import { Plan } from './plan.entity';
import { CreatePlanDto } from './dto/create-plan.dto';

@Injectable()
export class PlansService {
  constructor(
    @InjectRepository(PlanRepository)
    private planRepository: PlanRepository,
  ) {}

  async getPlans(filterDto: GetPlanFilterDto, user: User): Promise<Plan[]> {
    return this.planRepository.find({ ...filterDto, ownerId: user.id });
  }

  async getPlanById(id: string, user: User): Promise<Plan> {
    return this.planRepository.findOne({
      where: { id, ownerId: user.id },
    });
  }

  async createPlan(createPlanDto: CreatePlanDto, user: User): Promise<Plan> {
    const { project, ...data } = createPlanDto;

    return this.planRepository
      .create({
        projectId: project,
        ownerId: user.id,
        ...data,
      })
      .save();
  }
}

This PlanService only uses the internal methods of the Repository, if you're logging in the event of an Error, ExceptionFilter would be a suitable option for this: https://docs.nestjs.com/exception-filters .

Instead of checking if the plan had been found, you can use an interceptor:

import {
  CallHandler,
  ExecutionContext,
  Injectable,
  NestInterceptor,
  NotFoundException,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable()
export class PlanNotFoundInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      map(plan => {
        if (!plan) {
          throw new NotFoundException("plan couldn't be found");
        }


        return plan;
      }),
    );
  }
}

Then on your getById (Controller) use @UseInterceptor, this decouples your service, data access, logging, validation, etc..

I've simplified the implementation (for Interceptor), you may need to adjust it slightly to suit your exact need.

yarn run v1.22.4
$ jest
ts-jest[versions] (WARN) Version 24.9.0 of jest installed has not been tested with ts-jest. If you're experiencing issues, consider using a supported version (>=25.0.0 <
26.0.0). Please do not report issues in ts-jest if you are using unsupported versions.
ts-jest[versions] (WARN) Version 24.9.0 of jest installed has not been tested with ts-jest. If you're experiencing issues, consider using a supported version (>=25.0.0 <
26.0.0). Please do not report issues in ts-jest if you are using unsupported versions.
ts-jest[versions] (WARN) Version 24.9.0 of jest installed has not been tested with ts-jest. If you're experiencing issues, consider using a supported version (>=25.0.0 <
26.0.0). Please do not report issues in ts-jest if you are using unsupported versions.
 PASS  src/auth/user.repository.spec.ts
 PASS  src/projects/projects.service.spec.ts
 PASS  src/auth/jwt.strategy.spec.ts
 PASS  src/auth/user.entity.spec.ts

Test Suites: 4 passed, 4 total
Tests:       18 passed, 18 total
Snapshots:   0 total
Time:        3.774s, estimated 4s
Ran all test suites.
Done in 4.58s.

I haven't spent much time reviewing your tests, but the changes made haven't made any breaking changes to the unit tests (can't say the same for e2e, personally don't use Cucumber.js).

The point of this answer isn't to provide you the code needed, but the abstractions you can use to solve the tightly coupled components.

You can also use the Interceptor to validate the request, check if a project is present, check if it exists, if not abort with error. Again decoupling your error handling from your controller/service/whatever.

You also have the option to pluck/add things to the request, for example a .user that's authenticated, or a value from a header. (Could be useful if you want to send the projectId into the Controller via the Request object).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM