-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #646 from magieno/add-a-body-transform-decorator
- Loading branch information
Showing
10 changed files
with
386 additions
and
0 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
69 changes: 69 additions & 0 deletions
69
packages/data-mapping/src/decorators/body-mapping.decorator.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import {bodyMapping, bodyMappingDecoratorMetadataKeyname} from "./body-mapping.decorator"; | ||
import {MetadataUtil} from "@pristine-ts/common"; | ||
import { | ||
ClassTransformerBodyMappingContextInterface, DataMappingBuilderBodyMappingContextInterface, | ||
FunctionBodyMappingContextInterface | ||
} from "../interfaces/body-mapping-context.interface"; | ||
import {DataMappingBuilder} from "../builders/data-mapping.builder"; | ||
import {LowercaseNormalizer} from "../normalizers/lowercase.normalizer"; | ||
|
||
class Class {} | ||
|
||
describe("Body Mapping Decorator", () =>{ | ||
it("should properly set the context for a ClassType", () => { | ||
class Test { | ||
@bodyMapping(Class) | ||
route() {} | ||
} | ||
|
||
const context: { [bodyMappingDecoratorMetadataKeyname]: ClassTransformerBodyMappingContextInterface } = MetadataUtil.getRouteContext(Test.prototype, "route"); | ||
|
||
expect(context[bodyMappingDecoratorMetadataKeyname].type).toBe("classType"); | ||
expect(context[bodyMappingDecoratorMetadataKeyname].classType).toBe(Class); | ||
}) | ||
|
||
it("should properly set the context for a function", () => { | ||
const method:((body: any) => any) = (body) => {return {...body, "a": true}}; | ||
|
||
class Test2 { | ||
@bodyMapping(method) | ||
route() {} | ||
} | ||
|
||
const context: { [bodyMappingDecoratorMetadataKeyname]: FunctionBodyMappingContextInterface } = MetadataUtil.getRouteContext(Test2.prototype, "route"); | ||
|
||
expect(context[bodyMappingDecoratorMetadataKeyname].type).toBe("function"); | ||
expect(context[bodyMappingDecoratorMetadataKeyname].function).toBe(method); | ||
}) | ||
it("should properly set the context for a function", () => { | ||
const dataMappingBuilder = new DataMappingBuilder(); | ||
|
||
dataMappingBuilder | ||
.add() | ||
.setSourceProperty("title") | ||
.setDestinationProperty("name") | ||
.excludeNormalizer(LowercaseNormalizer.name) | ||
.end() | ||
.add() | ||
.setSourceProperty("rank") | ||
.setDestinationProperty("position") | ||
.excludeNormalizer(LowercaseNormalizer.name) | ||
.end() | ||
.add() | ||
.setSourceProperty("lastName") | ||
.setDestinationProperty("familyName") | ||
.excludeNormalizer(LowercaseNormalizer.name) | ||
.end() | ||
.end() | ||
|
||
class Test3 { | ||
@bodyMapping({builder: dataMappingBuilder}) | ||
route() {} | ||
} | ||
|
||
const context: { [bodyMappingDecoratorMetadataKeyname]: DataMappingBuilderBodyMappingContextInterface } = MetadataUtil.getRouteContext(Test3.prototype, "route"); | ||
|
||
expect(context[bodyMappingDecoratorMetadataKeyname].type).toBe("DataMappingBuilder"); | ||
expect(context[bodyMappingDecoratorMetadataKeyname].dataMappingBuilder).toBe(dataMappingBuilder); | ||
}) | ||
}) |
58 changes: 58 additions & 0 deletions
58
packages/data-mapping/src/decorators/body-mapping.decorator.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import {MetadataUtil} from "@pristine-ts/common"; | ||
import {DataMappingBuilder} from "../builders/data-mapping.builder"; | ||
import { | ||
ClassTransformerBodyMappingContextInterface, DataMappingBuilderBodyMappingContextInterface, | ||
FunctionBodyMappingContextInterface | ||
} from "../interfaces/body-mapping-context.interface"; | ||
import {ClassConstructor} from "class-transformer"; | ||
|
||
export const bodyMappingDecoratorMetadataKeyname = "@bodyMappingDecorator"; | ||
|
||
/** | ||
* The bodyMapping decorator can be used to map the body to another object. | ||
*/ | ||
export const bodyMapping = (argument: ClassConstructor<any> | {builder: DataMappingBuilder, destination?: ClassConstructor<any>} | ((body: any) => any) ) => { | ||
return ( | ||
/** | ||
* The class on which the decorator is used. | ||
*/ | ||
target: any, | ||
|
||
/** | ||
* The method on which the decorator is used. | ||
*/ | ||
propertyKey: string | symbol, | ||
|
||
/** | ||
* The descriptor of the property. | ||
*/ | ||
descriptor: PropertyDescriptor | ||
) => { | ||
let context: ClassTransformerBodyMappingContextInterface | FunctionBodyMappingContextInterface | DataMappingBuilderBodyMappingContextInterface; | ||
|
||
|
||
if(argument.hasOwnProperty("builder")) { | ||
context = { | ||
type: "DataMappingBuilder", | ||
|
||
// @ts-ignore It will exist if it has the property above. | ||
dataMappingBuilder: argument.builder as DataMappingBuilder, | ||
|
||
// @ts-ignore If it doesn't exist, it shouldn't worry about it, but it does... | ||
destination: argument.destination ?? undefined, | ||
} | ||
} else if(typeof argument === "function" && argument.hasOwnProperty("prototype") && argument.prototype.hasOwnProperty("constructor")) { | ||
context = { | ||
type: "classType", | ||
classType: argument as ClassConstructor<any>, | ||
} | ||
} else { | ||
context = { | ||
type: "function", | ||
function: argument as ((body: any) => any), | ||
} | ||
} | ||
|
||
MetadataUtil.setToRouteContext(bodyMappingDecoratorMetadataKeyname, context, target, propertyKey); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from "./body-mapping.decorator" |
24 changes: 24 additions & 0 deletions
24
packages/data-mapping/src/interfaces/body-mapping-context.interface.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import {DataMappingBuilder} from "../builders/data-mapping.builder"; | ||
import {ClassConstructor} from "class-transformer"; | ||
|
||
export interface BodyMappingContextInterface { | ||
type: "function" | "classType" | "DataMappingBuilder"; | ||
} | ||
|
||
export interface FunctionBodyMappingContextInterface extends BodyMappingContextInterface { | ||
type: "function"; | ||
|
||
function: ((body: any) => any); | ||
} | ||
|
||
export interface ClassTransformerBodyMappingContextInterface extends BodyMappingContextInterface { | ||
type: "classType"; | ||
|
||
classType: ClassConstructor<any>; | ||
} | ||
export interface DataMappingBuilderBodyMappingContextInterface extends BodyMappingContextInterface { | ||
type: "DataMappingBuilder"; | ||
|
||
dataMappingBuilder: DataMappingBuilder; | ||
destination?: ClassConstructor<any>; | ||
} |
140 changes: 140 additions & 0 deletions
140
packages/data-mapping/src/request-interceptors/body-mapping.request-interceptor.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
import {BodyMappingRequestInterceptor} from "./body-mapping.request-interceptor"; | ||
import {LogHandlerInterface} from "@pristine-ts/logging"; | ||
import {DataMapper} from "../mappers/data.mapper"; | ||
import {HttpMethod, Request} from "@pristine-ts/common"; | ||
import {Type} from "class-transformer"; | ||
import {MethodRouterNode, Route} from "@pristine-ts/networking"; | ||
import {bodyMappingDecoratorMetadataKeyname} from "../decorators/body-mapping.decorator"; | ||
import { | ||
ClassTransformerBodyMappingContextInterface, DataMappingBuilderBodyMappingContextInterface, | ||
FunctionBodyMappingContextInterface | ||
} from "../interfaces/body-mapping-context.interface"; | ||
import {DataMappingBuilder} from "../builders/data-mapping.builder"; | ||
import {LowercaseNormalizer} from "../normalizers/lowercase.normalizer"; | ||
|
||
const mockLogHandler: LogHandlerInterface = { | ||
critical(message: string, extra?: any, module?: string): void { | ||
}, debug(message: string, extra?: any, module?: string): void { | ||
}, error(message: string, extra?: any, module?: string): void { | ||
}, info(message: string, extra?: any, module?: string): void { | ||
}, terminate(): void { | ||
}, warning(message: string, extra?: any, module?: string): void { | ||
} | ||
|
||
} | ||
|
||
describe("Body Mapping Request Interceptor", () => { | ||
it("should map a body when a class is passed", async () => { | ||
class Nested { | ||
nestedProperty: string; | ||
} | ||
|
||
class Test { | ||
@Type(() => Nested) | ||
nested: Nested; | ||
|
||
@Type(() => Date) | ||
date: Date; | ||
} | ||
|
||
const request: Request = new Request(HttpMethod.Get, ""); | ||
request.body = { | ||
"nested": { | ||
"nestedProperty": "nested", | ||
}, | ||
"date": "2023-12-01", | ||
} | ||
|
||
const bodyMappingRequestInterceptor = new BodyMappingRequestInterceptor(mockLogHandler, new DataMapper([], [])); | ||
const route = new Route(null, ""); | ||
route.context = {}; | ||
route.context[bodyMappingDecoratorMetadataKeyname] = { | ||
type: "classType", | ||
classType: Test, | ||
} as ClassTransformerBodyMappingContextInterface; | ||
|
||
// @ts-ignore | ||
const methodNode = new MethodRouterNode(undefined, HttpMethod.Get, route, 0); | ||
|
||
const request2 = await bodyMappingRequestInterceptor.interceptRequest(request, methodNode); | ||
expect(request2.body instanceof Test).toBeTruthy() | ||
expect(request2.body.nested instanceof Nested).toBeTruthy() | ||
expect(request2.body.date instanceof Date).toBeTruthy() | ||
}) | ||
it("should map a body when a function is passed", async () => { | ||
const spy = jest.fn(); | ||
|
||
const bodyMapping = (body: any) => { | ||
spy(); | ||
return new Date(); | ||
} | ||
|
||
const request: Request = new Request(HttpMethod.Get, ""); | ||
request.body = { | ||
"nested": { | ||
"nestedProperty": "nested", | ||
}, | ||
"date": "2023-12-01", | ||
} | ||
|
||
const bodyMappingRequestInterceptor = new BodyMappingRequestInterceptor(mockLogHandler, new DataMapper([], [])); | ||
const route = new Route(null, ""); | ||
route.context = {}; | ||
route.context[bodyMappingDecoratorMetadataKeyname] = { | ||
type: "function", | ||
function: bodyMapping, | ||
} as FunctionBodyMappingContextInterface; | ||
|
||
// @ts-ignore | ||
const methodNode = new MethodRouterNode(undefined, HttpMethod.Get, route, 0); | ||
|
||
const request2 = await bodyMappingRequestInterceptor.interceptRequest(request, methodNode); | ||
expect(request2.body instanceof Date).toBeTruthy() | ||
expect(spy).toHaveBeenCalled() | ||
}) | ||
|
||
it("should map a body when a DataMappingBuilder is passed", async () => { | ||
class Test { | ||
name: string; | ||
|
||
position: number; | ||
} | ||
|
||
const request: Request = new Request(HttpMethod.Get, ""); | ||
request.body = { | ||
"title": "The Title", | ||
"rank": 2, | ||
} | ||
|
||
const dataMappingBuilder = new DataMappingBuilder(); | ||
|
||
dataMappingBuilder | ||
.add() | ||
.setSourceProperty("title") | ||
.setDestinationProperty("name") | ||
.addNormalizer(LowercaseNormalizer.name) | ||
.end() | ||
.add() | ||
.setSourceProperty("rank") | ||
.setDestinationProperty("position") | ||
.end() | ||
.end() | ||
|
||
const bodyMappingRequestInterceptor = new BodyMappingRequestInterceptor(mockLogHandler, new DataMapper([new LowercaseNormalizer()], [])); | ||
const route = new Route(null, ""); | ||
route.context = {}; | ||
route.context[bodyMappingDecoratorMetadataKeyname] = { | ||
type: "DataMappingBuilder", | ||
dataMappingBuilder, | ||
destination: Test, | ||
} as DataMappingBuilderBodyMappingContextInterface; | ||
|
||
// @ts-ignore | ||
const methodNode = new MethodRouterNode(undefined, HttpMethod.Get, route, 0); | ||
|
||
const request2 = await bodyMappingRequestInterceptor.interceptRequest(request, methodNode); | ||
expect(request2.body instanceof Test).toBeTruthy() | ||
expect(request2.body.name).toBe("the title") | ||
expect(request2.body.position).toBe(2) | ||
}) | ||
}) |
Oops, something went wrong.