Skip to content

Commit

Permalink
Merge pull request #2857 from tsedio/feat-injectable-function
Browse files Browse the repository at this point in the history
Feat injectable function
  • Loading branch information
Romakita authored Oct 11, 2024
2 parents 84540b1 + ef18457 commit 7565189
Show file tree
Hide file tree
Showing 20 changed files with 219 additions and 192 deletions.
3 changes: 2 additions & 1 deletion commitlint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ export default {
extends: ["@commitlint/config-conventional"],
rules: {
"scope-enum": [RuleConfigSeverity.Error, "always", findPackages()],
"header-max-length": [0, "always", 120]
"header-max-length": [0, "always", 120],
"footer-max-line-length": [0, "always", 200],
},
ignores: [
(message) =>
Expand Down
14 changes: 3 additions & 11 deletions packages/di/src/common/decorators/controller.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
import {isArrayOrArrayClass, Type, useDecorators} from "@tsed/core";
import {Children, Path} from "@tsed/schema";

import type {ControllerMiddlewares} from "../domain/ControllerProvider.js";
import {controller} from "../fn/injectable.js";
import {ProviderOpts} from "../interfaces/ProviderOpts.js";
import {registerController} from "../registries/ProviderRegistry.js";

export type PathType = string | RegExp | (string | RegExp)[];

export interface ControllerMiddlewares {
useBefore: any[];
use: any[];
useAfter: any[];
}

export interface ControllerOptions extends Partial<ProviderOpts<any>> {
path?: PathType;
children?: Type<any>[];
Expand Down Expand Up @@ -61,10 +56,7 @@ export function Controller(options: PathType | ControllerOptions): ClassDecorato

return useDecorators(
(target: Type) => {
registerController({
provide: target,
...opts
});
controller(target, opts);
},
path && Path(path as any),
Children(...children)
Expand Down
5 changes: 3 additions & 2 deletions packages/di/src/common/decorators/injectable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@ import type {ProviderOpts} from "../interfaces/ProviderOpts.js";
*/
export function Injectable(options: Partial<ProviderOpts> = {}): ClassDecorator {
return (target: any) => {
injectable({
const opts = {
...options,
...(options.provide ? {useClass: target} : {provide: target})
});
};
injectable(opts.provide, opts);
};
}
4 changes: 2 additions & 2 deletions packages/di/src/common/decorators/interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import {ProviderType} from "../domain/ProviderType.js";
import {Injectable} from "./injectable.js";

/**
* The decorators `@Service()` declare a new service can be injected in other service or controller on there `constructor`.
* All services annotated with `@Service()` are constructed one time.
* The decorators `@Interceptor()` declare a new service can be injected in other service or controller on there `constructor`.
* All services annotated with `@Interceptor()` are constructed one time.
*
* > `@Service()` use the `reflect-metadata` to collect and inject service on controllers or other services.
*
Expand Down
2 changes: 1 addition & 1 deletion packages/di/src/common/domain/Container.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ describe("Container", () => {

container = new Container();
container.addProvider(MyMiddleware, {type: ProviderType.MIDDLEWARE});
container.addProvider(MyService, {type: ProviderType.SERVICE});
container.addProvider(MyService, {type: ProviderType.PROVIDER});
container.addProvider(MyController, {type: ProviderType.CONTROLLER});

// await container.load();
Expand Down
6 changes: 3 additions & 3 deletions packages/di/src/common/domain/ControllerProvider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,10 @@ describe("ControllerProvider", () => {

it("should have a middlewares", () => {
expect(Array.isArray(controllerProvider.middlewares.use)).toBe(true);
expect(controllerProvider.middlewares.use[0]).toBeInstanceOf(Function);
expect(controllerProvider.middlewares.use![0]).toBeInstanceOf(Function);
expect(Array.isArray(controllerProvider.middlewares.useAfter)).toBe(true);
expect(controllerProvider.middlewares.useAfter[0]).toBeInstanceOf(Function);
expect(controllerProvider.middlewares.useAfter![0]).toBeInstanceOf(Function);
expect(Array.isArray(controllerProvider.middlewares.useBefore)).toBe(true);
expect(controllerProvider.middlewares.useBefore[0]).toBeInstanceOf(Function);
expect(controllerProvider.middlewares.useBefore![0]).toBeInstanceOf(Function);
});
});
11 changes: 8 additions & 3 deletions packages/di/src/common/domain/ControllerProvider.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import {ControllerMiddlewares} from "../decorators/controller.js";
import {TokenProvider} from "../interfaces/TokenProvider.js";
import {Provider} from "./Provider.js";
import {ProviderType} from "./ProviderType.js";

export interface ControllerMiddlewares {
useBefore: TokenProvider[];
use: TokenProvider[];
useAfter: TokenProvider[];
}

export class ControllerProvider<T = any> extends Provider<T> {
public tokenRouter: string;

Expand All @@ -15,7 +20,7 @@ export class ControllerProvider<T = any> extends Provider<T> {
*
* @returns {any[]}
*/
get middlewares(): ControllerMiddlewares {
get middlewares(): Partial<ControllerMiddlewares> {
return Object.assign(
{
use: [],
Expand All @@ -30,7 +35,7 @@ export class ControllerProvider<T = any> extends Provider<T> {
*
* @param middlewares
*/
set middlewares(middlewares: ControllerMiddlewares) {
set middlewares(middlewares: Partial<ControllerMiddlewares>) {
const mdlwrs = this.middlewares;
const concat = (key: string, a: any, b: any) => (a[key] = a[key].concat(b[key]));

Expand Down
8 changes: 7 additions & 1 deletion packages/di/src/common/domain/Provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ export class Provider<T = any> implements ProviderOpts<T> {
this.provide = token;
this.useClass = token as Type<T>;

Object.assign(this, options);
Object.assign(this, {
...options
});
}

get token() {
Expand Down Expand Up @@ -130,6 +132,10 @@ export class Provider<T = any> implements ProviderOpts<T> {
return this.store.get("childrenControllers", []);
}

set children(children: TokenProvider[]) {
this.store.set("childrenControllers", children);
}

get(key: string) {
return this.store.get(key) || this._tokenStore.get(key);
}
Expand Down
2 changes: 0 additions & 2 deletions packages/di/src/common/domain/ProviderType.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
export enum ProviderType {
VALUE = "value",
FACTORY = "factory",
SERVICE = "service",
PROVIDER = "provider",
MODULE = "module",
CONTROLLER = "controller",
Expand Down
2 changes: 1 addition & 1 deletion packages/di/src/common/fn/inject.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type {InvokeOptions} from "../interfaces/InvokeOptions.js";
import {TokenProvider} from "../interfaces/TokenProvider.js";
import type {TokenProvider} from "../interfaces/TokenProvider.js";
import {injector} from "./injector.js";
import {invokeOptions, localsContainer} from "./localsContainer.js";

Expand Down
80 changes: 80 additions & 0 deletions packages/di/src/common/fn/injectable.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import {DITest, logger} from "../../node/index.js";
import {ProviderScope} from "../domain/ProviderScope.js";
import {ProviderType} from "../domain/ProviderType.js";
import {inject} from "./inject.js";
import {controller, injectable, interceptor} from "./injectable.js";

class Nested {
get() {
return "hello";
}
}

class MyClass {
nested = inject(Nested);
logger = logger();
}

class MyController {}

injectable(Nested).scope(ProviderScope.SINGLETON).class(Nested);
injectable(MyClass);

describe("injectable", () => {
describe("injectable()", () => {
it("should define a singleton scope", async () => {
const instance = await DITest.invoke(MyClass);

expect(instance.nested).toBeInstanceOf(Nested);
expect(instance.nested.get()).toEqual("hello");
});
it("should create a factory", async () => {
const builder = injectable(Symbol.for("Test")).factory(() => "test");
const provider = builder.inspect();

expect(provider.type).toEqual(ProviderType.PROVIDER);
expect(builder.token()).toEqual(Symbol.for("Test"));
});
it("should create an async factory", async () => {
const builder = injectable(Symbol.for("Test")).asyncFactory(() => Promise.resolve("test"));
const provider = builder.inspect();

expect(provider.type).toEqual(ProviderType.PROVIDER);
expect(builder.token()).toEqual(Symbol.for("Test"));
});
it("should create a value", async () => {
const builder = injectable(Symbol.for("Test")).value({
test: "test"
});
const provider = builder.inspect();

expect(provider.type).toEqual(ProviderType.VALUE);
expect(builder.token()).toEqual(Symbol.for("Test"));
});
});

describe("controller()", () => {
it("should define a singleton scope", async () => {
const builder = controller(MyController)
.path("/my-controller")
.scope(ProviderScope.REQUEST)
.middlewares({
use: [() => {}]
});

const provider = builder.inspect();

expect(provider.type).toEqual(ProviderType.CONTROLLER);
expect(provider.scope).toEqual(ProviderScope.REQUEST);
});
});

describe("interceptor()", () => {
it("should define a singleton scope", async () => {
const builder = interceptor(MyController);
const provider = builder.inspect();

expect(provider.type).toEqual(ProviderType.INTERCEPTOR);
});
});
});
102 changes: 97 additions & 5 deletions packages/di/src/common/fn/injectable.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,98 @@
import {registerProvider} from "../registries/ProviderRegistry.js";
import "../registries/ProviderRegistry.js";

/**
* @alias {registerProvider} Alias of registerProvider
*/
export const injectable = registerProvider;
import {Store, type Type} from "@tsed/core";

import {ControllerProvider} from "../domain/ControllerProvider.js";
import type {Provider} from "../domain/Provider.js";
import {ProviderType} from "../domain/ProviderType.js";
import type {ProviderOpts} from "../interfaces/ProviderOpts.js";
import type {TokenProvider} from "../interfaces/TokenProvider.js";
import {GlobalProviders} from "../registries/GlobalProviders.js";

type ProviderBuilder<Token extends TokenProvider, BaseProvider, T extends object> = {
[K in keyof T as T[K] extends (...args: any[]) => any ? never : K]: (value: T[K]) => ProviderBuilder<Token, BaseProvider, T>;
} & {
inspect(): BaseProvider;
store(): Store;
token(): Token;
factory(f: (...args: unknown[]) => unknown): ProviderBuilder<Token, BaseProvider, T>;
asyncFactory(f: (...args: unknown[]) => Promise<unknown>): ProviderBuilder<Token, BaseProvider, T>;
value(v: unknown): ProviderBuilder<Token, BaseProvider, T>;
class(c: Type): ProviderBuilder<Token, BaseProvider, T>;
};

export function providerBuilder<Provider, Picked extends keyof Provider>(props: string[], baseOpts: Partial<ProviderOpts<Provider>> = {}) {
return <Token extends TokenProvider>(
token: Token,
options: Partial<ProviderOpts<Type>> = {}
): ProviderBuilder<Token, Provider, Pick<Provider, Picked>> => {
const provider = GlobalProviders.merge(token, {
...options,
...baseOpts,
provide: token
});

return props.reduce(
(acc, prop) => {
return {
...acc,
[prop]: function (value: any) {
(provider as any)[prop] = value;
return this;
}
};
},
{
factory(factory: any) {
provider.useFactory = factory;
return this;
},
asyncFactory(asyncFactory: any) {
provider.useAsyncFactory = asyncFactory;
return this;
},
value(value: any) {
provider.useValue = value;
provider.type = ProviderType.VALUE;
return this;
},
class(k: any) {
provider.useClass = k;
return this;
},
store() {
return provider.store;
},
inspect() {
return provider;
},
token() {
return provider.token as Token;
}
} as ProviderBuilder<Token, Provider, Pick<Provider, Picked>>
);
};
}

type PickedProps =
| "scope"
| "path"
| "alias"
| "useFactory"
| "useAsyncFactory"
| "useValue"
| "useClass"
| "hooks"
| "deps"
| "resolvers"
| "imports"
| "configuration";

const Props = ["type", "scope", "path", "alias", "hooks", "deps", "resolvers", "imports", "configuration"];
export const injectable = providerBuilder<Provider, PickedProps | "type">(Props);
export const interceptor = providerBuilder<Provider, PickedProps | "type">(Props, {
type: ProviderType.INTERCEPTOR
});
export const controller = providerBuilder<ControllerProvider, PickedProps | "children" | "middlewares">([...Props, "middlewares"], {
type: ProviderType.CONTROLLER
});
7 changes: 1 addition & 6 deletions packages/di/src/common/registries/GlobalProviders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,7 @@ export class GlobalProviderRegistry extends Map<TokenProvider, Provider> {
const meta = this.createIfNotExists(target, options);

Object.keys(options).forEach((key) => {
let value = (options as never)[key];
// if (key === "type") {
// value = String(value);
// }

meta[key] = value;
meta[key] = (options as never)[key];
});

this.set(target, meta);
Expand Down
Loading

0 comments on commit 7565189

Please sign in to comment.