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

fix(schema,core): Fix controller inheritance #1545

Merged
merged 1 commit into from
Sep 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 29 additions & 5 deletions packages/core/src/domain/Store.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {DecoratorTypes} from "../domain/DecoratorTypes";
import {decoratorTypeOf, deepClone, deepMerge, descriptorOf, isSymbol, nameOf} from "../utils";
import {Metadata} from "./Metadata";
import type {Type} from "./Type";

/**
* @ignore
Expand Down Expand Up @@ -64,7 +65,7 @@ function defineStore(args: any[]): Store {
}

export class Store {
#entries = new Map<string, any>();
private _entries = new Map<string, any>();

/**
* Create or get a Store from args {target + methodName + descriptor}
Expand All @@ -85,14 +86,28 @@ export class Store {
return Store.from(target, propertyKey, descriptorOf(target, propertyKey));
}

static mergeStoreFrom(target: Type<any>, source: Type<any>, ...args: any[]) {
const store = Store.from(target, ...args);

Store.from(source, ...args)._entries.forEach((value, key) => {
store.merge(key, value);
});

return store;
}

static mergeStoreMethodFrom(target: Type<any>, source: Type<any>, propertyKey: string | symbol) {
return this.mergeStoreFrom(target, source, propertyKey, descriptorOf(target, propertyKey));
}

/**
* The get() method returns a specified element from a Map object.
* @param key Required. The key of the element to return from the Map object.
* @param defaultValue
* @returns {T} Returns the element associated with the specified key or undefined if the key can't be found in the Map object.
*/
get<T = any>(key: any, defaultValue?: any): T {
return this.#entries.get(nameOf(key)) || defaultValue;
return this._entries.get(nameOf(key)) || defaultValue;
}

/**
Expand All @@ -101,7 +116,7 @@ export class Store {
* @returns {boolean}
*/
has(key: any): boolean {
return this.#entries.has(nameOf(key));
return this._entries.has(nameOf(key));
}

/**
Expand All @@ -110,7 +125,7 @@ export class Store {
* @param metadata Required. The value of the element to add to the Map object.
*/
set(key: any, metadata: any): Store {
this.#entries.set(nameOf(key), metadata);
this._entries.set(nameOf(key), metadata);

return this;
}
Expand All @@ -121,7 +136,7 @@ export class Store {
* @returns {boolean} Returns true if an element in the Map object existed and has been removed, or false if the element does not exist.
*/
delete(key: string): boolean {
return this.#entries.delete(nameOf(key));
return this._entries.delete(nameOf(key));
}

/**
Expand All @@ -144,4 +159,13 @@ export class Store {

return this;
}

toJson() {
return [...this._entries.entries()].reduce((obj, [key, value]) => {
return {
...obj,
[key]: value
};
}, {});
}
}
45 changes: 44 additions & 1 deletion packages/core/src/utils/decorators/decorateMethodsOf.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {descriptorOf, Store} from "@tsed/core";
import {descriptorOf, Store, StoreMerge, StoreSet} from "@tsed/core";
import {expect} from "chai";
import {decorateMethodsOf} from "./decorateMethodsOf";

Expand Down Expand Up @@ -33,4 +33,47 @@ describe("decorateMethodsOf", () => {

expect(new Test().test2("1")).to.eq("test1");
});
it("should decorate all methods and copy store metadata to the new property", () => {
function decorate() {
return (target: any) => {
decorateMethodsOf(target, (klass: any, property: any, descriptor: any) => {
Store.from(klass, property, descriptor).set("test", property);
});
};
}

class TestParent {
@StoreSet("options", {parent: "test", override: "parent"})
test(a: any) {}

@StoreSet("options", {parent: "test2"})
test2(a: any) {}
}

// WHEN
@decorate()
class Test extends TestParent {
@StoreMerge("options", {children: "test", override: "child"})
test() {}
}

// THEN
const storeObj2 = Store.fromMethod(Test, "test2").toJson();
expect(storeObj2).to.deep.eq({
options: {
parent: "test2"
},
test: "test2"
});

const storeObj = Store.fromMethod(Test, "test").toJson();
// store aren't merged
expect(storeObj).to.deep.eq({
options: {
children: "test",
override: "child"
},
test: "test"
});
});
});
3 changes: 3 additions & 0 deletions packages/core/src/utils/decorators/decorateMethodsOf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {classOf} from "../objects/classOf";
import {methodsOf} from "../objects/methodsOf";
import {prototypeOf} from "../objects/prototypeOf";
import {descriptorOf} from "../objects/descriptorOf";
import {Store} from "../../domain/Store";

export function decorateMethodsOf(klass: any, decorator: any) {
methodsOf(klass).forEach(({target, propertyKey}) => {
Expand All @@ -11,6 +12,8 @@ export function decorateMethodsOf(klass: any, decorator: any) {
return prototypeOf(target)[propertyKey].apply(this, args);
}
});

Store.mergeStoreMethodFrom(klass, target, propertyKey);
}

decorator(prototypeOf(klass), propertyKey, descriptorOf(klass, propertyKey));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {ancestorsOf} from "../objects/ancestorOf";
import {ancestorsOf} from "../objects/ancestorsOf";

export function inheritedDescriptorOf(target: any, propertyKey: string): PropertyDescriptor | undefined {
for (const klass of ancestorsOf(target)) {
Expand Down
24 changes: 5 additions & 19 deletions packages/core/src/utils/objects/ancestorOf.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,7 @@
import {classOf} from "./classOf";
import {getInheritedClass} from "./getInheritedClass";
import {nameOf} from "./nameOf";

/**
*
* @param target
* @returns {Array}
*/
export function ancestorsOf(target: any) {
const classes = [];

let currentTarget = classOf(target);

while (nameOf(currentTarget) !== "") {
classes.unshift(currentTarget);
currentTarget = getInheritedClass(currentTarget);
}
export function ancestorOf(target: any): any {
return Object.getPrototypeOf(target);
}

return classes;
export function getInheritedClass(target: any): any {
return ancestorOf(target);
}
21 changes: 21 additions & 0 deletions packages/core/src/utils/objects/ancestorsOf.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {classOf} from "./classOf";
import {getInheritedClass} from "./ancestorOf";
import {nameOf} from "./nameOf";

/**
*
* @param target
* @returns {Array}
*/
export function ancestorsOf(target: any) {
const classes = [];

let currentTarget = classOf(target);

while (nameOf(currentTarget) !== "") {
classes.unshift(currentTarget);
currentTarget = getInheritedClass(currentTarget);
}

return classes;
}
3 changes: 0 additions & 3 deletions packages/core/src/utils/objects/getInheritedClass.ts

This file was deleted.

3 changes: 2 additions & 1 deletion packages/core/src/utils/objects/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from "./ancestorOf";
export * from "./ancestorsOf";
export * from "./classOf";
export * from "./cleanObject";
export * from "./constructorOf";
Expand All @@ -8,7 +9,7 @@ export * from "./deepMerge";
export * from "./getClassOrSymbol";
export * from "./getConstructorArgNames";
export * from "./getEnumerableKeys";
export * from "./getInheritedClass";
export * from "./ancestorOf";
export * from "./getValue";
export * from "./isArray";
export * from "./isArrowFn";
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/utils/objects/isInheritedFrom.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {classOf} from "./classOf";
import {getInheritedClass} from "./getInheritedClass";
import {ancestorOf} from "./ancestorOf";
import {nameOf} from "./nameOf";

export function isInheritedFrom(target: any, from: any, deep = 5): boolean {
Expand All @@ -18,7 +18,7 @@ export function isInheritedFrom(target: any, from: any, deep = 5): boolean {
return true;
}

target = getInheritedClass(target);
target = ancestorOf(target);
deep--;
}

Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/utils/objects/methodsOf.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {ancestorsOf} from "./ancestorOf";
import {classOf} from "./classOf";
import {ancestorsOf} from "./ancestorsOf";
import {prototypeOf} from "./prototypeOf";

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {BodyParams, Controller, Get, PathParams, PlatformTest, Post} from "@tsed/common";
import {BodyParams, Controller, Get, Middleware, PathParams, PlatformTest, Post, UseAuth} from "@tsed/common";
import {NotFound} from "@tsed/exceptions";
import {Description, MaxLength, MinLength, Property, Returns, Summary} from "@tsed/schema";
import {expect} from "chai";
Expand Down Expand Up @@ -69,14 +69,37 @@ export class ResourcesCtrl extends BaseController<Resource> {
}
}

@Middleware()
class AuthMiddleware {
use() {
return true;
}
}

abstract class AttachmentController {
@Get("/:parentID/attachments")
getAll(@PathParams("parentID") parentID: string) {
return `All attachments of ${parentID}`;
}
}

@Controller("/findings")
@UseAuth(AuthMiddleware)
export class FindingsController extends AttachmentController {
@Get("/")
get() {
return "hello Finding";
}
}

export function testInheritanceController(options: PlatformTestOptions) {
let request: SuperTest.SuperTest<SuperTest.Test>;

before(
PlatformTest.bootstrap(options.server, {
...options,
mount: {
"/rest": [ResourcesCtrl]
"/rest": [ResourcesCtrl, FindingsController]
}
})
);
Expand All @@ -85,34 +108,50 @@ export function testInheritanceController(options: PlatformTestOptions) {
});
after(PlatformTest.reset);

it("should return list", async () => {
const {body} = await request.get("/rest/resources").expect(200);
describe("Scenario 1:", () => {
it("should return list", async () => {
const {body} = await request.get("/rest/resources").expect(200);

expect(body).to.deep.eq([{id: "1", name: "John"}]);
});
expect(body).to.deep.eq([{id: "1", name: "John"}]);
});

it("should return a resource", async () => {
const {body} = await request.get("/rest/resources/1").expect(200);
it("should return a resource", async () => {
const {body} = await request.get("/rest/resources/1").expect(200);

expect(body).to.deep.eq({
id: "1",
name: "John hello You!"
expect(body).to.deep.eq({
id: "1",
name: "John hello You!"
});
});

it("should add a resource", async () => {
const {body} = await request
.post("/rest/resources")
.send({
name: "july"
})
.expect(201);

expect(body.name).to.deep.eq("july");
expect(body.id).to.be.a("string");

const {body: resource} = await request.get(`/rest/resources/${body.id}`).expect(200);

expect(resource.id).to.deep.eq(body.id);
});
});

it("should add a resource", async () => {
const {body} = await request
.post("/rest/resources")
.send({
name: "july"
})
.expect(201);
describe("scenario2: FindingsController", () => {
it("should call /rest/findings/:parentID/attachments", async () => {
const {text} = await request.get("/rest/findings/1/attachments").expect(200);

expect(body.name).to.deep.eq("july");
expect(body.id).to.be.a("string");
expect(text).to.deep.eq("All attachments of 1");
});

const {body: resource} = await request.get(`/rest/resources/${body.id}`).expect(200);
it("should call /rest/findings/", async () => {
const {text} = await request.get("/rest/findings").expect(200);

expect(resource.id).to.deep.eq(body.id);
expect(text).to.deep.eq("hello Finding");
});
});
}
Loading