Skip to content

Commit

Permalink
feat(di): add inject(), constant(), value() function to inject servic…
Browse files Browse the repository at this point in the history
…e or value in injectable property

value defined by Constant and Value decorator are now available on constructor
injectable services on property are now available in the constructor

BREAKING CHANGE: This change require to set `    "useDefineForClassFields": false` in your tsconfig
Some DI methods are removed like bindInjectableProperties() which is not necessary.
  • Loading branch information
Romakita committed Sep 8, 2024
1 parent 3919da3 commit 343713d
Show file tree
Hide file tree
Showing 41 changed files with 945 additions and 945 deletions.
3 changes: 2 additions & 1 deletion packages/di/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
"test:ci": "vitest run --coverage.thresholds.autoUpdate=true"
},
"dependencies": {
"tslib": "2.6.2"
"tslib": "2.6.2",
"uuid": "9.0.1"
},
"devDependencies": {
"@tsed/barrels": "workspace:*",
Expand Down
8 changes: 5 additions & 3 deletions packages/di/src/common/constants/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export const INJECTABLE_PROP = "DI:INJECTABLE_PROP";
export const DI_PARAMS = "DI:PARAMS";
export const DI_PARAM_OPTIONS = "DI:PARAM:OPTIONS";
export const DI_INVOKE_OPTIONS = Symbol("DI_INVOKE_OPTIONS");
export const DI_INJECTABLE_PROPS = Symbol("DI_INJECTABLE_PROPS");
export const DI_USE_OPTIONS = "DI_USE_OPTIONS";
export const DI_USE_PARAM_OPTIONS = "DI_USE_PARAM_OPTIONS";
export const DI_INTERCEPTOR_OPTIONS = "DI_INTERCEPTOR_OPTIONS";
5 changes: 3 additions & 2 deletions packages/di/src/common/decorators/autoInjectable.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,14 +106,15 @@ describe("AutoInjectable", () => {
logger: Logger;

private value: string;
instances?: InterfaceGroup[];

constructor(initialValue: string, @Inject(TOKEN_GROUPS) instances?: InterfaceGroup[]) {
this.value = initialValue;
expect(instances).toHaveLength(3);
this.instances = instances;
}
}

new Test("test");
expect(new Test("test").instances).toHaveLength(3);
});
it("should return a class that extends the original class (with 3 arguments)", () => {
@AutoInjectable()
Expand Down
30 changes: 27 additions & 3 deletions packages/di/src/common/decorators/autoInjectable.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,37 @@
import {isArray, type Type} from "@tsed/core";
import {LocalsContainer} from "../domain/LocalsContainer.js";
import type {TokenProvider} from "../interfaces/TokenProvider.js";
import {InjectorService} from "../services/InjectorService.js";
import {getConstructorDependencies} from "../utils/getConstructorDependencies.js";

function resolveAutoInjectableArgs(token: Type, args: unknown[]) {
const locals = new LocalsContainer();
const injector = InjectorService.getInstance();
const deps: TokenProvider[] = getConstructorDependencies(token);
const list: any[] = [];
const length = Math.max(deps.length, args.length);

for (let i = 0; i < length; i++) {
if (args[i] !== undefined) {
list.push(args[i]);
} else {
const value = deps[i];
const instance = isArray(value)
? injector!.getMany(value[0], locals, {parent: token})
: injector!.invoke(value, locals, {parent: token});

list.push(instance);
}
}

return list;
}

export function AutoInjectable() {
return <T extends {new (...args: any[]): NonNullable<unknown>}>(constr: T): T => {
return class AutoInjectable extends constr {
constructor(...args: any[]) {
const locals = new LocalsContainer();
super(...InjectorService.resolveAutoInjectableArgs(constr, locals, args));
InjectorService.bind(this, locals);
super(...resolveAutoInjectableArgs(constr, args));
}
} as unknown as T;
};
Expand Down
107 changes: 89 additions & 18 deletions packages/di/src/common/decorators/constant.spec.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,95 @@
import {Store} from "@tsed/core";
import {INJECTABLE_PROP} from "../constants/constants.js";
import {Constant} from "./constant.js";

class Test {}
import {DITest} from "../../node/index.js";
import {constant, Constant} from "./constant.js";

describe("@Constant()", () => {
it("should store metadata", () => {
// WHEN
Constant("expression")(Test, "test");

// THEN
const store = Store.from(Test).get(INJECTABLE_PROP);

expect(store).toEqual({
test: {
bindingType: "constant",
propertyKey: "test",
expression: "expression",
defaultValue: undefined
beforeEach(() =>
DITest.create({
logger: {
level: "off"
}
})
);
afterEach(() => DITest.reset());
describe("when decorator is used as property decorator", () => {
it("should create a getter", async () => {
// WHEN
class Test {
@Constant("logger.level", "default value")
test: string;
}

// THEN

const test = await DITest.invoke<Test>(Test);

expect(test.test).toEqual("off");
});
it("should create a getter with default value", async () => {
// WHEN
class Test {
@Constant("logger.test", "default value")
test: string;
}

// THEN

const test = await DITest.invoke<Test>(Test);

expect(test.test).toEqual("default value");
});
it("shouldn't be possible to modify injected value from injector.settings", async () => {
// WHEN
class Test {
@Constant("logger.level")
test: string;
}

// THEN

const test = await DITest.invoke<Test>(Test);

test.test = "new value";

expect(test.test).toEqual("off");
});
it("should create a getter with native default value", async () => {
// WHEN
class Test {
@Constant("logger.test")
test: string = "default prop";
}

// THEN

const test = await DITest.invoke<Test>(Test);

expect(test.test).toEqual("default prop");
});
});
describe("when constant is used as default value initializer", () => {
it("should inject constant to the property", async () => {
// WHEN
class Test {
test: string = constant("logger.level", "default value");
}

// THEN

const test = await DITest.invoke<Test>(Test);

expect(test.test).toEqual("off");
});
it("should return the default value if expression is undefined", async () => {
// WHEN
class Test {
test: string = constant("logger.test", "default value");
}

// THEN

const test = await DITest.invoke<Test>(Test);

expect(test.test).toEqual("default value");
});
});
});
53 changes: 39 additions & 14 deletions packages/di/src/common/decorators/constant.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,39 @@
import {Store} from "@tsed/core";
import {INJECTABLE_PROP} from "../constants/constants.js";
import type {InjectableProperties} from "../interfaces/InjectableProperties.js";
import {InjectablePropertyType} from "../domain/InjectablePropertyType.js";
import {catchError, deepClone} from "@tsed/core";
import {InjectorService} from "../services/InjectorService.js";

export function constant<Type>(expression: string): Type | undefined;
export function constant<Type>(expression: string, defaultValue: Type | undefined): Type;
export function constant<Type>(expression: string, defaultValue?: Type | undefined): Type | undefined {
return InjectorService.getInstance().settings.get(expression, defaultValue);
}

export function bindConstant(target: any, propertyKey: string | symbol, expression: string, defaultValue?: any) {
const symbol = Symbol();

catchError(() => Reflect.deleteProperty(target, propertyKey));
Reflect.defineProperty(target, propertyKey, {
get() {
if (this[symbol] !== undefined) {
return this[symbol];
}

const value = constant(expression, defaultValue);

this[symbol] = Object.freeze(deepClone(value));

return this[symbol];
},
set(value: unknown) {
const bean = constant(expression, defaultValue) || this[symbol];

if (bean === undefined && value !== undefined) {
this[symbol] = value;
}
},
enumerable: true,
configurable: true
});
}

/**
* Return value from Configuration.
Expand Down Expand Up @@ -38,15 +70,8 @@ import {InjectablePropertyType} from "../domain/InjectablePropertyType.js";
* @returns {(targetClass: any, attributeName: string) => any}
* @decorator
*/
export function Constant(expression: string, defaultValue?: any): any {
return (target: any, propertyKey: string) => {
Store.from(target).merge(INJECTABLE_PROP, {
[propertyKey]: {
bindingType: InjectablePropertyType.CONSTANT,
propertyKey,
expression,
defaultValue
}
} as InjectableProperties);
export function Constant<Type = unknown>(expression: string, defaultValue?: Type): PropertyDecorator {
return (target, propertyKey) => {
return bindConstant(target, propertyKey, expression, defaultValue);
};
}
Loading

0 comments on commit 343713d

Please sign in to comment.