3

I want to create a method checkAddressEnterContestBefore to check some requirement. this function requires to connect to database (mysql) but I cannot inject repository. What could I do in this situation?

import { Contest } from '../models';
import { CONTEST_STATUS } from './constants';
import { ContestEntryRepository } from '../repositories';
import { inject } from '@loopback/core';

export function checkEnterContest(contest: Contest, address: string): boolean {
    ....
    if (!checkAddressEnterContestBefore(contest, address)) return false;
    ...
    return true;
}

export async function checkAddressEnterContestBefore(
    @inject(ContestEntryRepository) contestEntryRepository: ContestEntryRepository,
    contest: Contest, address: string): Promise<boolean> {
    const contestEntry = await contestEntryRepository.findOne({
        where: {
            and: [
                { contestId: contest.id },
                { address: address }
            ]
        }
    });

    return contestEntry == null
}

Miroslav Bajtoš
  • 10,667
  • 1
  • 41
  • 99

1 Answers1

2

The dependency injection has two parts:

  1. Code receiving dependencies is decorated with @inject decorator(s) to specify what to inject
  2. Callers of such code must resolve the dependencies from the IoC containers

In your code snippet, you are decorating checkAddressEnterContestBefore to receive some arguments via DI (that's the first item on my list above). Does the code compile at all? AFAIK, decorators cannot be applied on regular functions, only on class members (static or instance-level).

An example using a static class method:

export class CheckAddressEnterContestBefore {
  static async run(
    @inject(ContestEntryRepository) contestEntryRepository: ContestEntryRepository,
    contest: Contest, address: string): Promise<boolean> {
      const contestEntry = await contestEntryRepository.findOne({
        where: {
            and: [
                { contestId: contest.id },
                { address: address }
            ]
        }
      });

      return contestEntry == null
  }
}

Now checkEnterContest is NOT implementing dependency injection/resolution (the second item on my list above).

LoopBack provides a helper function invokeMethod that you can use to invoke a method with dependency injection. You will need the current Context object as a source where to get dependencies from.

import {invokeMethod} from '@loopback/core';

export function checkEnterContest(ctx: Context, contest: Contest, address: string): boolean {
    ....
    const checkResult = await invokeMethod(
      CheckAddressEnterContestBefore,
      'run',
      // context for resolving dependencies
      ctx, 
      // additional (non-injected) arguments
      [contest, address], 
    );

    if (!checkResult) return false;
    ...
    return true;
}

Personally, I consider it as an anti-pattern to resolve dependencies from the context deep inside the code base. Instead, I recommend to use regular function/method arguments to pass dependencies between different parts of your codebase, and use @inject-based dependency injection at the top level only - typically in controller methods.

This will make your code easier to test in isolation, because each function makes it clear what dependencies it needs and the compiler can warn you at compile time if your test is not providing all required dependencies. When you resolve dependencies from context, you will discover missing dependencies only at runtime, when they cannot be resolved because your test did not bind them in the context.

With that said, here is how I would rewrite your code:

export function checkEnterContest(
  contestEntryRepository: ContestEntryRepository,
  contest: Contest, 
  address: string
): boolean {
    ....
    if (!checkAddressEnterContestBefore(contestEntryRepository, contest, address)) return false;
    ...
    return true;
}

export async function checkAddressEnterContestBefore(
  contestEntryRepository: ContestEntryRepository,
  contest: Contest, 
  address: string
): Promise<boolean> {
    const contestEntry = await contestEntryRepository.findOne({
        where: {
            and: [
                { contestId: contest.id },
                { address: address }
            ]
        }
    });

    return contestEntry == null
}

Depending on how you like to structure your code, it may be even better to implement checkEnterContest and checkAddressEnterContestBefore as ContestEntryRepository methods.

Miroslav Bajtoš
  • 10,667
  • 1
  • 41
  • 99