Major version of nestjs-custom-injector: Exception if the provider is not set, use a promise in the default value and etc...
Tags: nestjs, inject, provider, typescript
Published: 2022-07-15 13:37:37


## Installation

```bash
npm i --save nestjs-custom-injector
```

## Links

https://github.com/EndyKaufman/nestjs-custom-injector - Source code of library
https://nestjs-custom-injector.site15.ru/api - Demo application with nestjs-custom-injector.
https://github.com/EndyKaufman/nestjs-custom-injector-example - Example generated with nest cli.

## BREAKING CHANGES
 - Change getLastComponentByName and getLastComponentByClass to
   getProvider
 - Change getComponentsByName and getComponentsByClass to
   getProviders
 - Remove static options from CustomInject decorator
 - Rename defaultPropertyValue to defaultProviderValue
 - Because now after start application library search all need providers tokens, you must always import CustomInjectorModule.forRoot() in your root application module, if you want exclude provider from checking, please add options `@CustomInject(PROVIDER_TOKEN,{ lazy:true })`

## Tests all features with multi providers

### Work with multi providers

```ts
interface CProvider {
  type: string;
}
const C_PROVIDER = Symbol('C_PROVIDER');
@Injectable()
class C1 implements CProvider {
  type = 'c1';
}
@Injectable()
class C2 implements CProvider {
  type = 'c2';
}
@Injectable()
class P {
  @CustomInject<CProvider>(C_PROVIDER, { multi: true })
  providers!: CProvider[];
}
const module = await Test.createTestingModule({
  imports: [
    CustomInjectorModule.forRoot(),
    CustomInjectorModule.forFeature({
      providers: [{ provide: C_PROVIDER, useClass: C1 }],
    }),
    CustomInjectorModule.forFeature({
      providers: [{ provide: C_PROVIDER, useClass: C2 }],
    }),
  ],
  providers: [P],
  exports: [P],
}).compile();

const app = module.createNestApplication();
await app.init();
const p = app.get<P>(P);
expect(p.providers.map((o) => o.type)).toEqual(['c1', 'c2']);
await app.close();
```

### Work with multi regular and async providers

```ts
interface CProvider {
  type: string;
}
const C_PROVIDER = Symbol('C_PROVIDER');
@Injectable()
class C1 implements CProvider {
  type = 'c1';
}
@Injectable()
class C2 implements CProvider {
  type = 'c2';
}
@Injectable()
class P {
  @CustomInject<CProvider>(C_PROVIDER, { multi: true })
  providers!: CProvider[];
}
const module = await Test.createTestingModule({
  imports: [
    CustomInjectorModule.forRoot(),
    CustomInjectorModule.forFeature({
      providers: [{ provide: C_PROVIDER, useClass: C1 }],
    }),
    CustomInjectorModule.forFeature({
      providers: [
        {
          provide: C_PROVIDER,
          useFactory: () =>
            new Promise((resolve) => setTimeout(() => resolve(new C2()), 1000)),
        },
      ],
    }),
  ],
  providers: [P],
  exports: [P],
}).compile();
const app = module.createNestApplication();
await app.init();
const p = app.get<P>(P);
expect(p.providers.map((o) => o.type)).toEqual(['c1', 'c2']);
await app.close();
```

### Error if array of providers not set

```ts
interface CProvider {
  type: string;
}
const C_PROVIDER1 = 'C_PROVIDER1';
@Injectable()
class P {
  @CustomInject<CProvider>(C_PROVIDER1, { multi: true })
  providers!: CProvider;
}
const module = await Test.createTestingModule({
  imports: [CustomInjectorModule.forRoot()],
  providers: [P],
  exports: [P],
}).compile();
const app = module.createNestApplication();
try {
  await app.init();
  expect(true).toEqual(false);
} catch (err) {
  expect(err instanceof CustomInjectorError && err.message).toEqual(
    `Providers "C_PROVIDER1" not found!`
  );
  expect(
    err instanceof CustomInjectorError &&
      err.injectedProvidersStorageItem?.token
  ).toEqual(C_PROVIDER1);
}
await app.close();
```

### Custom error if array of providers not set

```ts
interface CProvider {
    type: string;
}
class CustomError extends CustomInjectorError<CProvider> {
    constructor(public override message: string) {
    super(message);
    }
}
const C_PROVIDER1 = 'C_PROVIDER1';
@Injectable()
class P {
    @CustomInject<CProvider>(C_PROVIDER1, {
    multi: true,
    errorFactory: (message: string) => new CustomError(message),
    })
    providers!: CProvider;
}
const module = await Test.createTestingModule({
    imports: [CustomInjectorModule.forRoot()],
    providers: [P],
    exports: [P],
}).compile();
const app = module.createNestApplication();
try {
    await app.init();
    expect(true).toEqual(false);
} catch (err) {
    expect(err instanceof CustomError && err.message).toEqual(
    `Providers "C_PROVIDER1" not found!`
    );
    expect(
    err instanceof CustomError && err.injectedProvidersStorageItem?.token
    ).toEqual(undefined);
}
await app.close();
});
```

### Create multi providers with factory

```ts
interface CProvider {
  type: string;
}
const C_PROVIDER = Symbol('C_PROVIDER');
@Injectable()
class C1 {
  name = 'c1';
}
@Injectable()
class C2 {
  name = 'c2';
}
@Injectable()
class P {
  @CustomInject<CProvider>(C_PROVIDER, {
    multi: true,
    providerFactory: (data) => ({ type: data.name }),
  })
  providers!: CProvider[];
}
const module = await Test.createTestingModule({
  imports: [
    CustomInjectorModule.forRoot(),
    CustomInjectorModule.forFeature({
      providers: [{ provide: C_PROVIDER, useClass: C1 }],
    }),
    CustomInjectorModule.forFeature({
      providers: [{ provide: C_PROVIDER, useClass: C2 }],
    }),
  ],
  providers: [P],
  exports: [P],
}).compile();
const app = module.createNestApplication();
await app.init();
const p = app.get<P>(P);
expect(p.providers.map((o) => o.type)).toEqual(['c1', 'c2']);
await app.close();
```

### Create multi providers with async factory

```ts
interface CProvider {
  type: string;
}
const C_PROVIDER = Symbol('C_PROVIDER');
@Injectable()
class C1 {
  name = 'c1';
}
@Injectable()
class C2 {
  name = 'c2';
}
@Injectable()
class P {
  @CustomInject<CProvider>(C_PROVIDER, {
    multi: true,
    providerFactory: (data) =>
      new Promise((resolve) =>
        setTimeout(() => resolve({ type: data.name }), 1000)
      ),
  })
  providers!: CProvider[];
}
const module = await Test.createTestingModule({
  imports: [
    CustomInjectorModule.forRoot(),
    CustomInjectorModule.forFeature({
      providers: [{ provide: C_PROVIDER, useClass: C1 }],
    }),
    CustomInjectorModule.forFeature({
      providers: [{ provide: C_PROVIDER, useClass: C2 }],
    }),
  ],
  providers: [P],
  exports: [P],
}).compile();
const app = module.createNestApplication();
await app.init();
const p = app.get<P>(P);
expect(p.providers.map((o) => o.type)).toEqual(['c1', 'c2']);
await app.close();
```

### Load multi providers with lazy option on getting data, without set them from application bootstrap

```ts
interface CProvider {
  type: string;
}
const C_PROVIDER = Symbol('C_PROVIDER');
@Injectable()
class C1 implements CProvider {
  type = 'c1';
}
@Injectable()
class C2 implements CProvider {
  type = 'c2';
}
@Injectable()
class P {
  @CustomInject<CProvider>(C_PROVIDER, { multi: true, lazy: true })
  providers!: CProvider[];
}
const module = await Test.createTestingModule({
  imports: [
    CustomInjectorModule.forRoot(),
    CustomInjectorModule.forFeature({
      providers: [{ provide: C_PROVIDER, useClass: C1 }],
    }),
    CustomInjectorModule.forFeature({
      providers: [{ provide: C_PROVIDER, useClass: C2 }],
    }),
  ],
  providers: [P],
  exports: [P],
}).compile();
const app = module.createNestApplication();
// we don't need start application, because providers marked with lazy options will be ignored when the application bootstrap
// await app.init();
const p = app.get<P>(P);
expect(p.providers.map((o) => o.type)).toEqual(['c1', 'c2']);
await app.close();
```

### Update multi providers on runtime

```ts
interface CProvider {
    type: string;
}
const C_PROVIDER_NEW = Symbol('C_PROVIDER_NEW');
@Injectable()
class C1 implements CProvider {
    type = 'c1';
}
@Injectable()
class C2 implements CProvider {
    type = 'c2';
}
@Injectable()
class P {
    @CustomInject<CProvider>(C_PROVIDER_NEW, { multi: true })
    providers!: CProvider[];
}
const module = await Test.createTestingModule({
    imports: [
    CustomInjectorModule.forRoot(),
    CustomInjectorModule.forFeature({
        providers: [{ provide: C_PROVIDER_NEW, useValue: new C1() }],
    }),
    CustomInjectorModule.forFeature({
        providers: [{ provide: C_PROVIDER_NEW, useClass: C2 }],
    }),
    ],
    providers: [P],
    exports: [P],
}).compile();
const app = module.createNestApplication();
await app.init();
const p = app.get<P>(P);
expect(p.providers.map((o) => o.type)).toEqual(['c1', 'c2']);
p.providers.forEach((o) => {
    o.type = `updated ${o.type}`;
});
const p2 = app.get<P>(P);
expect(p2.providers.map((o) => o.type)).toEqual([
    'updated c1',
    'updated c2',
]);
await app.close();
});
```

### Update multi providers created with factory on runtime

```ts
interface CProvider {
    type: string;
}
const C_PROVIDER_NEW = Symbol('C_PROVIDER_NEW');
@Injectable()
class C1 implements CProvider {
    type = 'c1';
}
@Injectable()
class C2 implements CProvider {
    type = 'c2';
}
@Injectable()
class P {
    @CustomInject<CProvider>(C_PROVIDER_NEW, {
    multi: true,
    providerFactory: (data) => ({ ...data, type: `factory ${data.type}` }),
    })
    providers!: CProvider[];
}
const module = await Test.createTestingModule({
    imports: [
    CustomInjectorModule.forRoot(),
    CustomInjectorModule.forFeature({
        providers: [{ provide: C_PROVIDER_NEW, useValue: new C1() }],
    }),
    CustomInjectorModule.forFeature({
        providers: [{ provide: C_PROVIDER_NEW, useClass: C2 }],
    }),
    ],
    providers: [P],
    exports: [P],
}).compile();
const app = module.createNestApplication();
await app.init();
const providers = app.get<P>(P).providers;
expect(providers.map((o) => o.type)).toEqual(['factory c1', 'factory c2']);
providers.forEach((o) => {
    o.type = `updated ${o.type}`;
});
const currentProviders = [...app.get<P>(P).providers];
expect(currentProviders.map((o) => o.type)).toEqual([
    'updated factory c1',
    'updated factory c2',
]);
await app.close();
});
```

### Use default providers if providers for token not found

```ts
interface CProvider {
    type: string;
}
const C_PROVIDER = Symbol('C_PROVIDER');
@Injectable()
class P {
    @CustomInject<CProvider>(C_PROVIDER, {
    multi: true,
    defaultProvidersValue: [],
    })
    providers!: CProvider[];
}
const module = await Test.createTestingModule({
    imports: [CustomInjectorModule.forRoot()],
    providers: [P],
    exports: [P],
}).compile();
const app = module.createNestApplication();
await app.init();
const providers = app.get<P>(P).providers;
expect(providers).toEqual([]);
await app.close();
});
```

## Tests all features with multi providers and sub property

### Work with multi providers

```ts
interface CProvider {
    type: string;
}
const C_PROVIDER = Symbol('C_PROVIDER');
@Injectable()
class C1 implements CProvider {
    type = 'c1';
}
@Injectable()
class C2 implements CProvider {
    type = 'c2';
}
@Injectable()
class P {
    @CustomInject<CProvider>(C_PROVIDER, {
    multi: true,
    propertyName: 'type',
    })
    providers!: CProvider['type'][];
}
const module = await Test.createTestingModule({
    imports: [
    CustomInjectorModule.forRoot(),
    CustomInjectorModule.forFeature({
        providers: [{ provide: C_PROVIDER, useClass: C1 }],
    }),
    CustomInjectorModule.forFeature({
        providers: [{ provide: C_PROVIDER, useClass: C2 }],
    }),
    ],
    providers: [P],
    exports: [P],
}).compile();
const app = module.createNestApplication();
await app.init();
const p = app.get<P>(P);
expect(p.providers).toEqual(['c1', 'c2']);
await app.close();
});
```

### Work with multi regular and async providers

```ts
interface CProvider {
  type: string;
}
const C_PROVIDER = Symbol('C_PROVIDER');
@Injectable()
class C1 implements CProvider {
  type = 'c1';
}
@Injectable()
class C2 implements CProvider {
  type = 'c2';
}
@Injectable()
class P {
  @CustomInject<CProvider>(C_PROVIDER, {
    multi: true,
    propertyName: 'type',
  })
  providers!: CProvider['type'][];
}
const module = await Test.createTestingModule({
  imports: [
    CustomInjectorModule.forRoot(),
    CustomInjectorModule.forFeature({
      providers: [{ provide: C_PROVIDER, useClass: C1 }],
    }),
    CustomInjectorModule.forFeature({
      providers: [
        {
          provide: C_PROVIDER,
          useFactory: () =>
            new Promise((resolve) => setTimeout(() => resolve(new C2()), 1000)),
        },
      ],
    }),
  ],
  providers: [P],
  exports: [P],
}).compile();
const app = module.createNestApplication();
await app.init();
const p = app.get<P>(P);
expect(p.providers).toEqual(['c1', 'c2']);
await app.close();
```

### Error if array of providers not set

```ts
interface CProvider {
  type: string;
}
const C_PROVIDER1 = 'C_PROVIDER1';
@Injectable()
class P {
  @CustomInject<CProvider>(C_PROVIDER1, {
    multi: true,
    propertyName: 'type',
  })
  providers!: CProvider['type'];
}
const module = await Test.createTestingModule({
  imports: [CustomInjectorModule.forRoot()],
  providers: [P],
  exports: [P],
}).compile();
const app = module.createNestApplication();
try {
  await app.init();
  expect(true).toEqual(false);
} catch (err) {
  expect(err instanceof CustomInjectorError && err.message).toEqual(
    `Providers "C_PROVIDER1" not found!`
  );
  expect(
    err instanceof CustomInjectorError &&
      err.injectedProvidersStorageItem?.token
  ).toEqual(C_PROVIDER1);
}
await app.close();
```

### Custom error if array of providers not set

```ts
interface CProvider {
  type: string;
}
class CustomError extends CustomInjectorError<CProvider> {
  constructor(public override message: string) {
    super(message);
  }
}
const C_PROVIDER1 = 'C_PROVIDER1';
@Injectable()
class P {
  @CustomInject<CProvider>(C_PROVIDER1, {
    multi: true,
    propertyName: 'type',
    errorFactory: (message: string) => new CustomError(message),
  })
  providers!: CProvider['type'];
}
const module = await Test.createTestingModule({
  imports: [CustomInjectorModule.forRoot()],
  providers: [P],
  exports: [P],
}).compile();
const app = module.createNestApplication();
try {
  await app.init();
  expect(true).toEqual(false);
} catch (err) {
  expect(err instanceof CustomError && err.message).toEqual(
    `Providers "C_PROVIDER1" not found!`
  );
  expect(
    err instanceof CustomError && err.injectedProvidersStorageItem?.token
  ).toEqual(undefined);
}
await app.close();
```

### Create multi providers with factory

```ts
interface CProvider {
  type: string;
}
const C_PROVIDER = Symbol('C_PROVIDER');
@Injectable()
class C1 {
  name = 'c1';
}
@Injectable()
class C2 {
  name = 'c2';
}
@Injectable()
class P {
  @CustomInject<CProvider>(C_PROVIDER, {
    multi: true,
    propertyName: 'type',
    providerFactory: (data) => ({ type: data.name }),
  })
  providers!: CProvider['type'][];
}
const module = await Test.createTestingModule({
  imports: [
    CustomInjectorModule.forRoot(),
    CustomInjectorModule.forFeature({
      providers: [{ provide: C_PROVIDER, useClass: C1 }],
    }),
    CustomInjectorModule.forFeature({
      providers: [{ provide: C_PROVIDER, useClass: C2 }],
    }),
  ],
  providers: [P],
  exports: [P],
}).compile();
const app = module.createNestApplication();
await app.init();
const p = app.get<P>(P);
expect(p.providers).toEqual(['c1', 'c2']);
await app.close();
```

### Create multi providers with async factory

```ts
interface CProvider {
  type: string;
}
const C_PROVIDER = Symbol('C_PROVIDER');
@Injectable()
class C1 {
  name = 'c1';
}
@Injectable()
class C2 {
  name = 'c2';
}
@Injectable()
class P {
  @CustomInject<CProvider>(C_PROVIDER, {
    multi: true,
    propertyName: 'type',
    providerFactory: (data) =>
      new Promise((resolve) =>
        setTimeout(() => resolve({ type: data.name }), 1000)
      ),
  })
  providers!: CProvider['type'][];
}
const module = await Test.createTestingModule({
  imports: [
    CustomInjectorModule.forRoot(),
    CustomInjectorModule.forFeature({
      providers: [{ provide: C_PROVIDER, useClass: C1 }],
    }),
    CustomInjectorModule.forFeature({
      providers: [{ provide: C_PROVIDER, useClass: C2 }],
    }),
  ],
  providers: [P],
  exports: [P],
}).compile();
const app = module.createNestApplication();
await app.init();
const p = app.get<P>(P);
expect(p.providers).toEqual(['c1', 'c2']);
await app.close();
```

### Load multi providers with lazy option on getting data, without set them from application bootstrap

```ts
interface CProvider {
    type: string;
}
const C_PROVIDER = Symbol('C_PROVIDER');
@Injectable()
class C1 implements CProvider {
    type = 'c1';
}
@Injectable()
class C2 implements CProvider {
    type = 'c2';
}
@Injectable()
class P {
    @CustomInject<CProvider>(C_PROVIDER, {
    multi: true,
    propertyName: 'type',
    lazy: true,
    })
    providers!: CProvider['type'][];
}
const module = await Test.createTestingModule({
    imports: [
    CustomInjectorModule.forRoot(),
    CustomInjectorModule.forFeature({
        providers: [{ provide: C_PROVIDER, useClass: C1 }],
    }),
    CustomInjectorModule.forFeature({
        providers: [{ provide: C_PROVIDER, useClass: C2 }],
    }),
    ],
    providers: [P],
    exports: [P],
}).compile();
const app = module.createNestApplication();
// we don't need start application, because providers marked with lazy options will be ignored when the application bootstrap
// await app.init();
const p = app.get<P>(P);
expect(p.providers).toEqual(['c1', 'c2']);
await app.close();
});
```

### Use default providers if providers for token not found

```ts
interface CProvider {
    type: string;
}
const C_PROVIDER = Symbol('C_PROVIDER');
@Injectable()
class P {
    @CustomInject<CProvider>(C_PROVIDER, {
    multi: true,
    propertyName: 'type',
    defaultProvidersValue: [],
    })
    providers!: CProvider['type'][];
}
const module = await Test.createTestingModule({
    imports: [CustomInjectorModule.forRoot()],
    providers: [P],
    exports: [P],
}).compile();
const app = module.createNestApplication();
await app.init();
const providers = app.get<P>(P).providers;
expect(providers).toEqual([]);
await app.close();
});
```