Comparing version 3.0.0-alpha.2 to 3.0.0
{ | ||
"name": "rsdi", | ||
"version": "3.0.0-alpha.2", | ||
"version": "3.0.0", | ||
"description": "TypeScript dependency injection container. Strong types without decorators.", | ||
@@ -23,3 +23,3 @@ "keywords": [ | ||
"types": "./dist/index.d.ts", | ||
"homepage": "https://github.com/radzserg/rsdi", | ||
"homepage": "https://github.com/radzserg/rsdi3", | ||
"author": "Sergey Radzishevskii <radzserg@gmail.com>", | ||
@@ -26,0 +26,0 @@ "license": "ISC", |
@@ -1,12 +0,12 @@ | ||
# RSDI - Dependency Injection Container | ||
# RSDI - Simple & Strong-Type Dependency Injection Container | ||
Simple and powerful dependency injection container for with strong type checking system. `rsdi` offers strong | ||
type-safety support. | ||
Easily manage your project dependencies with RSDI. This library provides a robust type-checking system. | ||
- [Motivation](#motivation) | ||
- [Features](#features) | ||
- [When to use](#when-to-use) | ||
- [Best Use Cases](#best-use-cases) | ||
- [Architecture](#architecture) | ||
- [How to use](#how-to-use) | ||
- [Strict types](#strict-types) | ||
- - [Best Practices](#best-practices) | ||
- Wiki | ||
@@ -43,3 +43,3 @@ - [Async factory resolver](./docs/async_factory_resolver.md) | ||
## When to use | ||
## Best Use Cases | ||
@@ -51,5 +51,2 @@ `RSDI` is most effective in complex applications. When the complexity of your application is high, it becomes necessary to | ||
You like and respect and use Dependency Injection and TDD. You have to use Dependency Injection in order to have proper | ||
unit tests. Tests that test only one module - class, component, function, but not integration with nested dependencies. | ||
## Architecture | ||
@@ -67,12 +64,11 @@ | ||
An application always has an entry point, whether it is a web application or a CLI application. This is the only place where you | ||
should configure your dependency injection container. The top level components will then have the lower level components | ||
injected. | ||
Every application, whether it's a web app or a command-line tool, starts at an entry point. This is where you should | ||
set up your dependency injection container. Once set up, the top-level parts of your app will automatically get the | ||
lower-level parts they need. For web servers, the dependency injection container will manage a pre-configured router, | ||
which will already include the necessary controllers. | ||
# How to use | ||
Let's take a simple web application as an example. We will cut into a small part of the application that registers a | ||
new user. A real application will consist of dozens of components. The logic of the components will be much more | ||
complicated. This is just a demo. It's up to you to use classes or factory functions for the demonstration, and we'll | ||
use both. | ||
Let's look at a basic web app that registers new users as an example. Keep in mind, a real-world app has many more | ||
parts and the logic is usually more complex. This is just a quick demo to show you the ropes. | ||
@@ -87,3 +83,3 @@ ### Simple use-case | ||
const { foo } = container; // or container.get("foo"); | ||
const { foo } = container; // alternatively container.get("foo"); | ||
``` | ||
@@ -121,3 +117,3 @@ | ||
export function MyDbProviderUserRepository(db: Knex): UserRepository { | ||
export function MyDbProviderUserRepository(db: DbConnection): UserRepository { | ||
return { | ||
@@ -130,4 +126,4 @@ async saveNewUser(userAccountData: SignupData): Promise<void> { | ||
export function buildDbConnection(): Knex { | ||
return knex({ | ||
export function buildDbConnection(): DbConnection { | ||
return connectToDb({ | ||
/* db credentials */ | ||
@@ -159,6 +155,7 @@ }); | ||
`container.get` - return type based on declaration. | ||
When a resolver is called for the first time, it's resolved once and the result is saved. From then on, the saved | ||
result is used. If you want to change a dependency, don't use the add method; use the update method instead. | ||
This way, you won't accidentally replace dependencies. If you need to mock a dependency for testing, that's when | ||
you'd want to override it. | ||
**All resolvers are resolved only once and their result persists over the life of the container.** | ||
Let's map our web application routes to configured controllers | ||
@@ -172,7 +169,7 @@ | ||
) { | ||
const usersController = diContainer.get("UsersController"); | ||
const { usersController } = diContainer; | ||
app | ||
.route("/users") | ||
.get(usersController.list.bind(usersController)) | ||
.post(usersController.create.bind(usersController)); | ||
.get(usersController.list) | ||
.post(usersController.create); | ||
} | ||
@@ -190,5 +187,3 @@ ``` | ||
app.listen(8000, () => { | ||
console.log(`⚡️[server]: Server is running`); | ||
}); | ||
app.listen(8000); | ||
``` | ||
@@ -206,4 +201,34 @@ | ||
## Best practices | ||
As your application expands, you'll likely need to divide your DI container across multiple files for better | ||
organization. You might have a main `diContainer.ts` file for the core DI setup, and a separate `controllers.ts`, | ||
`validators.ts` etc. This approach keeps your code clean and easy to manage. | ||
```typescript | ||
// diContainer.ts | ||
export const configureDI = async () => { | ||
return (await buildDatabaseDependencies()) | ||
.extend(addDataAccessDependencies) | ||
.extend(addValidators); | ||
} | ||
// buildDatabaseDependencies.ts | ||
export type DIWithPool = Awaited<ReturnType<typeof buildDatabaseDependencies>>; | ||
export const buildDatabaseDependencies = async () => { | ||
const pool = await createDatabasePool(); | ||
const longRunningPool = await createLongRunningDatabasePool(); | ||
return new DIContainer() | ||
.add("databasePool", () => pool) | ||
.add("longRunningDatabasePool", () => longRunningPool); | ||
}; | ||
// addValidators.ts | ||
export type DIWithValidators = ReturnType<typeof addValidators>; | ||
export const addValidators = (container: DIWithPool) => { | ||
return container | ||
.add('myValidatorA', ({ a, b, c }) => new MyValidatorA(a, b, c)) | ||
.add('myValidatorB', ({ a, b, c }) => new MyValidatorB(a, b, c)); | ||
}; |
@@ -1,2 +0,2 @@ | ||
import DIContainer from "../../DIContainer"; | ||
import { DIContainer } from "../../DIContainer"; | ||
import { Bar } from "../fakeClasses"; | ||
@@ -26,3 +26,3 @@ import { expectType, expectNotType } from "tsd"; | ||
.add("a", () => "string") | ||
.add("a", () => new Date()); | ||
.update("a", () => new Date()); | ||
@@ -29,0 +29,0 @@ expectType<Date>(container.a); |
import { describe, expect, test } from "vitest"; | ||
import DIContainer from "../"; | ||
import { Bar, Foo } from "./fakeClasses"; | ||
import { DependencyIsMissingError, IncorrectInvocationError } from "../errors"; | ||
import { DenyOverrideDependencyError, DependencyIsMissingError, IncorrectInvocationError } from "../errors"; | ||
import { DIContainer } from "../DIContainer.js"; | ||
@@ -40,9 +40,14 @@ describe("DIContainer typescript type resolution", () => { | ||
test("it allows to override resolvers by key", () => { | ||
test("deny override resolvers by key", () => { | ||
const container = new DIContainer() | ||
.add("key1", () => "key1") | ||
.add("key1", () => "key2"); | ||
.add("key1", () => "value 1") | ||
expect(() => { | ||
container | ||
// @ts-ignore | ||
.add("key1", () => new Date()); | ||
}).toThrow(new DenyOverrideDependencyError("key1")); | ||
const value = container.get("key1"); | ||
expect(value).toEqual("key2"); | ||
expect(value).toEqual("value 1"); | ||
}); | ||
@@ -49,0 +54,0 @@ |
import { | ||
DenyOverrideDependencyError, | ||
DependencyIsMissingError, | ||
@@ -6,37 +7,17 @@ ForbiddenNameError, | ||
} from "./errors.js"; | ||
import { | ||
Container, | ||
DenyInputKeys, | ||
Factory, | ||
ResolvedDependencies, | ||
Resolvers, | ||
StringLiteral, | ||
} from "./types"; | ||
type Factory<ContainerResolvers extends ResolvedDependencies> = ( | ||
resolvers: ContainerResolvers, | ||
) => any; | ||
const containerMethods = ["add", "get", "extend", "update"]; | ||
type ResolvedDependencies = { | ||
[k: string]: any; | ||
}; | ||
type Resolvers<CR extends ResolvedDependencies> = { | ||
[k in keyof CR]?: Factory<CR>; | ||
}; | ||
type StringLiteral<T> = T extends string | ||
? string extends T | ||
? never | ||
: T | ||
: never; | ||
type Container<ContainerResolvers extends ResolvedDependencies> = | ||
DIContainer<ContainerResolvers> & ContainerResolvers; | ||
type ExtendResolvers< | ||
ContainerResolvers extends ResolvedDependencies, | ||
N extends string, | ||
R extends Factory<ContainerResolvers>, | ||
> = N extends keyof ContainerResolvers | ||
? Omit<ContainerResolvers, N> & { [n in N]: ReturnType<R> } | ||
: ContainerResolvers & { [n in N]: ReturnType<R> }; | ||
const containerMethods = ["add", "get", "extend"]; | ||
export class DIContainer< | ||
ContainerResolvers extends ResolvedDependencies = {}, | ||
> { | ||
/** | ||
* Dependency injection container | ||
*/ | ||
export class DIContainer<ContainerResolvers extends ResolvedDependencies = {}> { | ||
private resolvers: Resolvers<ContainerResolvers> = {}; | ||
@@ -50,38 +31,67 @@ | ||
/** | ||
* Adds new dependency resolver to the container. If dependency with given name already exists it will throw an error. | ||
* Use update method instead. It will override existing dependency. | ||
* | ||
* @param name | ||
* @param resolver | ||
*/ | ||
public add<N extends string, R extends Factory<ContainerResolvers>>( | ||
name: StringLiteral<DenyInputKeys<N, keyof ContainerResolvers>>, | ||
resolver: R, | ||
): Container<ContainerResolvers & { [n in N]: ReturnType<R> }> { | ||
if (containerMethods.includes(name)) { | ||
throw new ForbiddenNameError(name); | ||
} | ||
if (this.has(name)) { | ||
throw new DenyOverrideDependencyError(name); | ||
} | ||
return this.setValue(name, resolver) as this & | ||
Container<ContainerResolvers & { [n in N]: ReturnType<R> }>; | ||
} | ||
/** | ||
* Updates existing dependency resolver. If dependency with given name does not exist it will throw an error. | ||
* In most cases you don't need to override dependencies and should use add method instead. This approach will | ||
* help you to avoid overriding dependencies by mistake. | ||
* | ||
* You may want to override dependency if you want to mock it in tests. | ||
* | ||
* @param name | ||
* @param resolver | ||
*/ | ||
public update< | ||
N extends keyof ContainerResolvers, | ||
R extends Factory<ContainerResolvers>, | ||
>( | ||
name: StringLiteral<N>, | ||
resolver: R, | ||
): Container<ExtendResolvers<ContainerResolvers, N, R>> { | ||
): Container<Omit<ContainerResolvers, N> & { [n in N]: ReturnType<R> }> { | ||
if (containerMethods.includes(name)) { | ||
throw new ForbiddenNameError(name); | ||
} | ||
this.resolvers = { | ||
...this.resolvers, | ||
[name]: resolver, | ||
}; | ||
let updatedObject = this; | ||
if (!this.hasOwnProperty(name)) { | ||
updatedObject = Object.defineProperty(this, name, { | ||
get() { | ||
return this.get(name); | ||
}, | ||
}); | ||
if (this.has(name)) { | ||
throw new DependencyIsMissingError(name); | ||
} | ||
this.context = new Proxy(this, { | ||
get(target, property) { | ||
if (containerMethods.includes(property.toString())) { | ||
throw new IncorrectInvocationError(); | ||
} | ||
// @ts-ignore | ||
return target[property]; | ||
}, | ||
}) as unknown as ContainerResolvers; | ||
return this.setValue(name, resolver) as this & | ||
Container<Omit<ContainerResolvers, N> & { [n in N]: ReturnType<R> }>; | ||
} | ||
return updatedObject as this & | ||
DIContainer<ExtendResolvers<ContainerResolvers, N, R>> & | ||
ExtendResolvers<ContainerResolvers, N, R>; | ||
/** | ||
* Checks if dependency with given name exists | ||
* @param name | ||
*/ | ||
public has(name: string): boolean { | ||
return this.resolvers.hasOwnProperty(name); | ||
} | ||
/** | ||
* Resolve dependency by name. Alternatively you can use property access to resolve dependency. | ||
* For example: const { a, b } = container; | ||
* @param dependencyName | ||
*/ | ||
public get<Name extends keyof ContainerResolvers>( | ||
@@ -104,2 +114,23 @@ dependencyName: Name, | ||
/** | ||
* Extends container with given function. It will pass container as an argument to the function. | ||
* Function should return new container with extended resolvers. | ||
* It is useful when you want to split your container into multiple files. | ||
* You can create a file with resolvers and extend container with it. | ||
* You can also use it to create multiple containers with different resolvers. | ||
* | ||
* For example: | ||
* | ||
* const container = new DIContainer() | ||
* .extend(addValidators) | ||
* | ||
* export type DIWithValidators = ReturnType<typeof addValidators>; | ||
* export const addValidators = (container: DIWithDataAccessors) => { | ||
* return container | ||
* .add('myValidatorA', ({ a, b, c }) => new MyValidatorA(a, b, c)) | ||
* .add('myValidatorB', ({ a, b, c }) => new MyValidatorB(a, b, c)); | ||
* }; | ||
* | ||
* @param f | ||
*/ | ||
public extend<E extends (container: Container<ContainerResolvers>) => any>( | ||
@@ -111,2 +142,30 @@ f: E, | ||
private setValue(name: string, resolver: Factory<ContainerResolvers>) { | ||
this.resolvers = { | ||
...this.resolvers, | ||
[name]: resolver, | ||
}; | ||
let updatedObject = this; | ||
if (!this.hasOwnProperty(name)) { | ||
updatedObject = Object.defineProperty(this, name, { | ||
get() { | ||
return this.get(name); | ||
}, | ||
}); | ||
} | ||
this.context = new Proxy(this, { | ||
get(target, property) { | ||
if (containerMethods.includes(property.toString())) { | ||
throw new IncorrectInvocationError(); | ||
} | ||
// @ts-ignore | ||
return target[property]; | ||
}, | ||
}) as unknown as ContainerResolvers; | ||
return updatedObject; | ||
} | ||
private toContainer(): Container<ContainerResolvers> { | ||
@@ -113,0 +172,0 @@ return this as unknown as Container<ContainerResolvers>; |
export class DependencyIsMissingError extends Error { | ||
constructor(name: string) { | ||
super(`Dependency with name ${name} is not defined`); | ||
super(`Dependency resolver with name ${name} is not defined`); | ||
} | ||
@@ -9,3 +9,3 @@ } | ||
constructor(name: string) { | ||
super(`Dependency with name ${name} is not allowed`); | ||
super(`Dependency resolver with name ${name} is not allowed`); | ||
} | ||
@@ -18,2 +18,9 @@ } | ||
} | ||
} | ||
} | ||
export class DenyOverrideDependencyError extends Error { | ||
constructor(name: string) { | ||
super(`Dependency resolver with name ${name} is already defined, use update method instead`); | ||
} | ||
} | ||
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Deprecated
MaintenanceThe maintainer of the package marked it as deprecated. This could indicate that a single version should not be used, or that the package is no longer maintained and any new vulnerabilities will not be fixed.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
576610
18
448
0
224
0
1