"Nothing is lost, nothing is created, everything is transformed" _Lavoisier
Metamorphosis-NestJS is the NodeJS version of Metamorphosis, an utility library to ease conversions of objects, provided as java, javascript and NestJS as well.
Metamorphosis-NestJS has been adapted to the popular framework NestJS, so it exports a conversion service, that you can import and use into your application as hub of all convertions: for example, entities to DTOs and/or viceversa
npm install --save @fabio.formosa/metamorphosis-nest
import { MetamorphosisNestModule } from '@fabio.formosa/metamorphosis-nest';
@Module({
imports: [MetamorphosisModule.register()],
...
}
export class MyApp{
}
Create a new converter class, implementing the interface Converter<Source, Target>
and decorate the class with @Convert
import { Convert, Converter } from '@fabio.formosa/metamorphosis';
@Injectable()
@Convert(Car, CarDto)
export default class CarToCarDtoConverter implements Converter<Car, CarDto> {
public convert(source: Car): CarDto {
const target = new CarDto();
target.color = source.color;
target.model = source.model;
target.manufacturerName = source.manufacturer.name;
return target;
}
}
In the above example, the converter is @Injectable()
, so is a NestJs Service.
When your converters are instanciated by NestJs, they will be registered into the conversion service
.
The conversion service
is the hub for all conversions. You can inject it and invoke the convert method.
import { ConversionService } from '@fabio.formosa/metamorphosis-nest';
@Injectable()
class CarService{
constructor(private convertionService: ConvertionService){}
public async getCar(id: string): CarDto{
const car: Car = await CarModel.findById(id);
return <CarDto> await this.convertionService.convert(car, CarDto);
}
}
const cars: Car[] = ...
const carDtos = <CarDto[]> await this.convertionService.convertAll(cars, CarDto);
If your converter must be async (eg. it must retrieve entities from DB):
@Injectable()
@Convert(PlanetDto, Planet)
export default class PlanetDtoToPlanet implements Converter<PlanetDto, Promise<Planet>> {
async convert(source: PlanetDto): Promise<Planet> {
...
}
}
- Define Planet as target type in
@Convert
- declare
Promise<Planet>
inConverter interface
. - The convert method will be
async
.
When you invoke conversionService you must apply await
if you know for that conversion is returned a Promise
.
const planet = <Planet> await conversionService.convert(planetDto, Planet);
or in case of conversion of an array:
const planets = <Planet[]> await Promise.all(conversionService.convert(planetDto, Planet));
If you have to convert mongoose document into DTO, it's recommended to use Typegoose and class-transformer.
-
Add dependency:
npm install --save @fabio.formosa/metamorphosis-typegoose-plugin
-
Register conversion service with
metamorphosis-typegoose-plugin
import { MetamorphosisNestModule } from '@fabio.formosa/metamorphosis-nest'; import TypegoosePlugin from '@fabio.formosa/metamorphosis-typegoose-plugin/dist/typegoose-plugin'; import { MetamorphosisPlugin } from '@fabio.formosa/metamorphosis'; const typegoosePlugin = new TypegoosePlugin(); @Module({ imports: [MetamorphosisModule.register({logger: false, plugins: [typegoosePlugin])], ... } export class MyApp{ }
-
Define the type of your model and the moongose schema using decorators (
@prop
). (note: team is annotate by@Type
decorator provided by class-transformer in order to useplainToClass
function)@modelOptions({ existingMongoose: mongoose, collection: 'players' }) class Player{ _id: ObjectID; @prop({require : true}) name: string; @prop() score: number; @Type(() => Team) @prop({require : true}) team: Team; } class Team{ _id: ObjectID; @prop({require : true}) name: string; @prop({require : true}) city: string; }
-
Define your DTOs
class PlayerDto{ id: string; name: string; team: string; } class TeamDto{ id: string; name: string; city: string; }
-
Create converters
import {Converter, Convert} from '@fabio.formosa/metamorphosis'; @Convert(Player, PlayerDto) class PlayerConverterTest implements Converter<Player, PlayerDto> { public convert(source: Player): PlayerDto { const target = new PlayerDto(); target.id = source._id.toString(); target.name = source.name; target.team = source.team.name; return target; } } @Convert(Team, TeamDto) class TeamConverterTest implements Converter<Team, TeamDto> { public convert(source: Team): TeamDto { const target = new TeamDto(); target.id = source._id.toString(); target.name = source.name; target.city = source.city; return target; } }
-
Use ConversionService
import {ConversionService} from '@fabio.formosa/metamorphosis-nest'; @Injectable() class MyService{ constructor(private readonly ConversionService conversionService){} doIt(){ const foundPlayerModel = await PlayerModel.findOne({'name': 'Baggio'}).exec() || player; const playerDto = <PlayerDto> await this.conversionService.convert(foundPlayerModel, PlayerDto); //if you want convert only the team (and not also the Player) const teamDto = <TeamDto> await conversionService.convert(foundPlayer.team, TeamDto); }
typeORM is supported by metamorphosis. Following an example (it doesn't show the converter because it's trivial, if you read above the doc):
let product = new Product();
product.name = 'smartphone';
product.pieces = 50;
const productRepository = connection.getRepository(Product);
product = await productRepository.save(product);
const productDto: ProductDto = <ProductDto> await conversionService.convert(product, ProductDto);
or if your entity extends BaseEntity
let product = new Product();
product.name = 'smartphone';
product.pieces = 50;
product = await product.save(product);
const productDto: ProductDto = <ProductDto> await conversionService.convert(product, ProductDto);
In case of conversion from DTO to entity:
@Injectable()
@Convert(ProductDto, Product)
export default class ProductDtoConverterTest implements Converter<ProductDto, Promise<Product>> {
constructor(private readonly connection: Connection){}
public async convert(source: ProductDto): Promise<Product> {
const productRepository: Repository<Product> = this.connection.getRepository(Product);
const target: Product | undefined = await productRepository.findOne(source.id);
if(!target)
throw new Error(`not found any product by id ${source.id}`);
target.name = source.name;
return target;
}
}
To activate debug mode
import { MetamorphosisNestModule } from '@fabio.formosa/metamorphosis-nest';
@Module({
imports: [MetamorphosisModule.register({logger: true})],
...
}
export class MyApp{
}
In this case, metamorphosis will send log to console. Otherwise, you can pass a custom debug function (msg: string) => void
, e.g:
import { MetamorphosisNestModule } from '@fabio.formosa/metamorphosis-nest';
const myCustomLogger = {
debug: (msg: string) => {
winston.logger(msg); //example
}
}
@Module({
imports: [MetamorphosisModule.register({logger: myCustomLogger.debug})],
...
}
export class MyApp{
}
At the moment, MetamorphosisNestModule uses console to log. Soon, it will be possible to pass a custom logger.
- TypeScript 3.2+
- Node 8, 10+
- emitDecoratorMetadata and experimentalDecorators must be enabled in tsconfig.json
Red Chameleon in this README file is a picture of ph. George Lebada (pexels.com)