Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(di): value defined by Constant and Value decorator are now available on constructor #2812

Merged
merged 8 commits into from
Sep 8, 2024
6 changes: 5 additions & 1 deletion packages/core/src/domain/Type.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* An example of a `Type` is `MyCustomComponent` filters, which in JavaScript is be represented by
* An example of a `Type` is `MyCustomComponent` filters, which in JavaScript is represented by
* the `MyCustomComponent` constructor function.
*/
// tslint:disable-next-line: variable-name
Expand All @@ -14,3 +14,7 @@ export const Type = Function;

// @ts-ignore
global.Type = Type;

export interface AbstractType<T> extends Function {
prototype: T;
}
2 changes: 1 addition & 1 deletion packages/core/src/utils/objects/descriptorOf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* @returns {PropertyDescriptor}
*/
export function descriptorOf(target: any, propertyKey: string | symbol): PropertyDescriptor {
return Object.getOwnPropertyDescriptor((target && target.prototype) || target, propertyKey)!;
return Reflect.getOwnPropertyDescriptor((target && target.prototype) || target, propertyKey)!;
}

export function isMethodDescriptor(target: any, propertyKey: string | symbol) {
Expand Down
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
Loading