Socket
Socket
Sign inDemoInstall

rsdi

Package Overview
Dependencies
0
Maintainers
1
Versions
41
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 3.0.0-alpha.2 to 3.0.0

src/types.ts

4

package.json
{
"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`);
}
}
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc