Skip to content

Commit

Permalink
Update container.getAll with options (#1671)
Browse files Browse the repository at this point in the history
* feat: add GetAllOptions

* feat: update container with optional GetAllOptions param.

* docs: update docs

* docs: update changelog
  • Loading branch information
notaphplover authored Dec 4, 2024
1 parent 59aac5c commit 30314de
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 8 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Added
- Added `interfaces.GetAllOptions`.

### Changed
- Updated `container.getAll` with `options` optional param.
- Updated `container.getAllAsync` with `options` optional param.

### Fixed

Expand Down
13 changes: 9 additions & 4 deletions src/container/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -447,16 +447,20 @@ class Container implements interfaces.Container {

// Resolves a dependency by its runtime identifier
// The runtime identifier can be associated with one or multiple bindings
public getAll<T>(serviceIdentifier: interfaces.ServiceIdentifier<T>): T[] {
const getArgs: GetArgs<T> = this._getAllArgs(serviceIdentifier);
public getAll<T>(
serviceIdentifier: interfaces.ServiceIdentifier<T>,
options?: interfaces.GetAllOptions,
): T[] {
const getArgs: GetArgs<T> = this._getAllArgs(serviceIdentifier, options);

return this._getButThrowIfAsync<T>(getArgs) as T[];
}

public async getAllAsync<T>(
serviceIdentifier: interfaces.ServiceIdentifier<T>,
options?: interfaces.GetAllOptions,
): Promise<T[]> {
const getArgs: GetArgs<T> = this._getAllArgs(serviceIdentifier);
const getArgs: GetArgs<T> = this._getAllArgs(serviceIdentifier, options);

return this._getAll(getArgs);
}
Expand Down Expand Up @@ -830,9 +834,10 @@ class Container implements interfaces.Container {

private _getAllArgs<T>(
serviceIdentifier: interfaces.ServiceIdentifier<T>,
options: interfaces.GetAllOptions | undefined,
): GetArgs<T> {
const getAllArgs: GetArgs<T> = {
avoidConstraints: true,
avoidConstraints: !(options?.enforceBindingConstraints ?? false),
isMultiInject: true,
serviceIdentifier,
};
Expand Down
14 changes: 12 additions & 2 deletions src/interfaces/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,10 @@ namespace interfaces {
skipBaseClassChecks?: boolean;
}

export interface GetAllOptions {
enforceBindingConstraints?: boolean;
}

export interface Container {
id: number;
parent: Container | null;
Expand Down Expand Up @@ -251,7 +255,10 @@ namespace interfaces {
key: string | number | symbol,
value: unknown,
): T;
getAll<T>(serviceIdentifier: ServiceIdentifier<T>): T[];
getAll<T>(
serviceIdentifier: ServiceIdentifier<T>,
options?: GetAllOptions,
): T[];
getAllTagged<T>(
serviceIdentifier: ServiceIdentifier<T>,
key: string | number | symbol,
Expand All @@ -271,7 +278,10 @@ namespace interfaces {
key: string | number | symbol,
value: unknown,
): Promise<T>;
getAllAsync<T>(serviceIdentifier: ServiceIdentifier<T>): Promise<T[]>;
getAllAsync<T>(
serviceIdentifier: ServiceIdentifier<T>,
options?: GetAllOptions,
): Promise<T[]>;
getAllTaggedAsync<T>(
serviceIdentifier: ServiceIdentifier<T>,
key: string | number | symbol,
Expand Down
105 changes: 105 additions & 0 deletions src/test/bugs/issue_1670.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { expect } from 'chai';
import { describe, it } from 'mocha';

import { Container, interfaces } from '../../index';

type StorageConfig = 'all' | 'disk' | 'redis';

type Storage = 'disk-storage' | 'redis-storage';

/**
* Creates a condition function for storage bindings based on a specific storage configuration.
* This function is used with Inversify's `.when` method to dynamically control which bindings apply.
*
* @param config - The specific storage type to match. Cannot be 'all' since it acts as a wildcard.
* @returns A function that checks whether the current storage configuration matches the provided type.
*/
function isStorageConfig(
config: Exclude<StorageConfig, 'all'>,
): (request: interfaces.Request) => boolean {
return ({ parentContext: { container } }: interfaces.Request) => {
const storageConfig: StorageConfig =
container.get<StorageConfig>('storage-config');
return storageConfig === 'all' || storageConfig === config;
};
}

describe('Issue 1670', () => {
it('should get expected services with custom binding constraints (disk)', async () => {
const config: StorageConfig = 'disk';
const expectedStorageServices: Storage[] = ['disk-storage'];

const container: Container = new Container({ defaultScope: 'Singleton' });

container
.bind<StorageConfig>('storage-config')
.toDynamicValue(() => config);

container
.bind<Storage>('storage')
.toDynamicValue(() => 'disk-storage')
.when(isStorageConfig('disk'));

container
.bind<Storage>('storage')
.toDynamicValue(() => 'redis-storage')
.when(isStorageConfig('redis'));

expect(
container.getAll<Storage>('storage', { enforceBindingConstraints: true }),
).to.deep.eq(expectedStorageServices);
});

it('should get expected services with custom binding constraints (redis)', async () => {
const config: StorageConfig = 'redis';
const expectedStorageServices: Storage[] = ['redis-storage'];

const container: Container = new Container({ defaultScope: 'Singleton' });

container
.bind<StorageConfig>('storage-config')
.toDynamicValue(() => config);

container
.bind<Storage>('storage')
.toDynamicValue(() => 'disk-storage')
.when(isStorageConfig('disk'));

container
.bind<Storage>('storage')
.toDynamicValue(() => 'redis-storage')
.when(isStorageConfig('redis'));

expect(
container.getAll<Storage>('storage', { enforceBindingConstraints: true }),
).to.deep.eq(expectedStorageServices);
});

it('should get expected services with custom binding constraints (all)', async () => {
const config: StorageConfig = 'all';
const expectedStorageServices: Storage[] = [
'disk-storage',
'redis-storage',
];

const container: Container = new Container({ defaultScope: 'Singleton' });

container
.bind<StorageConfig>('storage-config')
.toDynamicValue(() => config);

container
.bind<Storage>('storage')
.toDynamicValue(() => 'disk-storage')
.when(isStorageConfig('disk'));

container
.bind<Storage>('storage')
.toDynamicValue(() => 'redis-storage')
.when(isStorageConfig('redis'));

expect(
container.getAll<Storage>('storage', { enforceBindingConstraints: true }),
).to.deep.eq(expectedStorageServices);
});
});
32 changes: 30 additions & 2 deletions wiki/container_api.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ let katana = await container.getTaggedAsync<Weapon>("Weapon", "faction", "samura
let shuriken = await container.getTaggedAsync<Weapon>("Weapon", "faction", "ninja");
```

## container.getAll\<T>(serviceIdentifier: interfaces.ServiceIdentifier\<T>): T[]
## container.getAll\<T>(serviceIdentifier: interfaces.ServiceIdentifier\<T>, options?: interfaces.GetAllOptions): T[]

Get all available bindings for a given identifier. All the bindings must be synchronously resolved, otherwise an error is thrown:

Expand All @@ -217,7 +217,21 @@ container.bind<Weapon>("Weapon").to(Shuriken);
let weapons = container.getAll<Weapon>("Weapon"); // returns Weapon[]
```

## container.getAllAsync\<T>(serviceIdentifier: interfaces.ServiceIdentifier\<T>): Promise\<T[]>
Keep in mind `container.getAll` doesn't enforce binding contraints by default in the root level, enable the `enforceBindingConstraints` flag to force this binding constraint check:

```ts
let container = new Container();
container.bind<Weapon>("Weapon").to(Katana).when(() => true);
container.bind<Weapon>("Weapon").to(Shuriken).when(() => false);

let allWeapons = container.getAll<Weapon>("Weapon"); // returns [new Katana(), new Shuriken()]
let notAllWeapons = container.getAll<Weapon>(
"Weapon",
{ enforceBindingConstraints: true },
); // returns [new Katana()]
```

## container.getAllAsync\<T>(serviceIdentifier: interfaces.ServiceIdentifier\<T>, options?: interfaces.GetAllOptions): Promise\<T[]>

Get all available bindings for a given identifier:

Expand All @@ -229,6 +243,20 @@ container.bind<Weapon>("Weapon").toDynamicValue(async () => new Shuriken());
let weapons = await container.getAllAsync<Weapon>("Weapon"); // returns Promise<Weapon[]>
```

Keep in mind `container.getAll` doesn't enforce binding contraints by default in the root level, enable the `enforceBindingConstraints` flag to force this binding constraint check:

```ts
let container = new Container();
container.bind<Weapon>("Weapon").to(Katana).when(() => true);
container.bind<Weapon>("Weapon").to(Shuriken).when(() => false);

let allWeapons = await container.getAllAsync<Weapon>("Weapon"); // returns Promise.resolve([new Katana(), new Shuriken()])
let notAllWeapons = container.getAllAsync<Weapon>(
"Weapon",
{ enforceBindingConstraints: true },
); // returns Promise.resolve([new Katana()])
```

## container.getAllNamed\<T>(serviceIdentifier: interfaces.ServiceIdentifier\<T>, named: string | number | symbol): T[]

Resolves all the dependencies by its runtime identifier that matches the given named constraint. All the binding must be synchronously resolved, otherwise an error is thrown:
Expand Down

0 comments on commit 30314de

Please sign in to comment.