How to create and use a terminal command with nest-commander? - javascript

So, I have this NestJS project, and for learning purposes I want to create a command with nest-commander that would be executable on terminal (that way I could call a function from other services), also for learning purposes, whenever I call this command, it should call a function on a service file that get's a user from the database.
It would look like this :
> run myCommand -username UsernameString
Whenever that command is called from the terminal, I would call getUser() from AnotherService to find my user with that specific UsernameString.
I read the docs and couldn't figure much of it, so...
How do I call a command from terminal?
Is it possible to call the same command within the application?

So basically if we take this example:
import { Module } from '#nestjs/common';
import { Command, CommandFactory, CommandRunner, Option } from 'nest-commander';
interface BasicCommandOptions {
string?: string;
boolean?: boolean;
number?: number;
}
#Command({ name: 'basic', description: 'A parameter parse' })
export class BasicCommand extends CommandRunner {
async run(
passedParam: string[],
options?: BasicCommandOptions,
): Promise<void> {
if (options?.number) {
this.runWithNumber(passedParam, options.number);
} else if (options?.string) {
this.runWithString(passedParam, options.string);
} else {
this.runWithNone(passedParam);
}
}
#Option({
flags: '-n, --number [number]',
description: 'A basic number parser',
})
parseNumber(val: string): number {
return Number(val);
}
#Option({
flags: '-s, --string [string]',
description: 'A string return',
})
parseString(val: string): string {
return val;
}
#Option({
flags: '-b, --boolean [boolean]',
description: 'A boolean parser',
})
parseBoolean(val: string): boolean {
return JSON.parse(val);
}
runWithString(param: string[], option: string): void {
console.log({ param, string: option });
}
runWithNumber(param: string[], option: number): void {
console.log({ param, number: option });
}
runWithNone(param: string[]): void {
console.log({ param });
}
}
#Module({
providers: [BasicCommand],
})
export class AppModule {}
async function bootstrap() {
await CommandFactory.run(AppModule);
}
bootstrap();
You can run it using that method:
ts-node ./test.ts basic -s test-value -n 1234
First you call the name of the command then the params

Related

NestJs using a service from another Module in a Custom Repository

Learning NestJs actually and facing an issue saving typeorm OneToMany relation.
Let's say I have two modules ProjectsModule # PlansModule
Exists a OneToMany relation between Plan & Project entities
#Entity()
export class Project extends BaseEntity {
#PrimaryGeneratedColumn('uuid')
id: string;
...
#OneToMany(type => Plan, plan => plan.project, { eager: true })
plans: Plan[];
}
#Entity()
export class Plan extends BaseEntity {
#PrimaryGeneratedColumn('uuid')
id: string;
...
#ManyToOne(type => Project, project => project.plans, { eager: false } )
project: Project;
#Column()
projectId: string;
}
In the ProjectsModule, I have a ProjectsService with this method:
async getProjectById(
id: string,
user: User
): Promise<Project> {
const found = await this.projectRepository.findOne({ where: { id, ownerId: user.id } });
if(!found) {
throw new NotFoundException(`Project with ID "${id}" not found`)
}
return found;
}
My problem is when I try to save a new Plan.
My PlansService calls the PlanRepository like that
async createPlan(
createPlanDto: CreatePlanDto,
user: User
): Promise<Plan> {
return this.planRepository.createPlan(createPlanDto, user);
}
And on the PlanRepository :
constructor(
#Inject(ProjectsService)
private projectsService: ProjectsService
) {
super();
}
async createPlan(
createPlanDto: CreatePlanDto,
user: User
): Promise<Plan> {
const { title, description, project } = createPlanDto;
const plan = new Plan();
const projectFound = await this.projectsService.getProjectById(project, user)
plan.title = title;
plan.description = description;
plan.status = PlanStatus.ENABLED;
plan.owner = user;
plan.project = project;
try {
await plan.save();
} catch (error) {
this.logger.error(`Failed to create a Plan for user "${user.email}". Data: ${JSON.stringify(createPlanDto)}`, error.stack);
throw new InternalServerErrorException();
}
delete plan.owner;
return plan;
}
Trying this throws me this error when sending a POST request to my plan controller :
TypeError: this.projectsService.getProjectById is not a function
And trying a
console.log('service', this.projectsService)
give me
service EntityManager {
repositories: [],
plainObjectToEntityTransformer: PlainObjectToNewEntityTransformer {},
connection: Connection {
I guess I'm not using the projectsService properly but I don't understand where I could have made a mistake.
On the module's side I'm exporting the ProjectsService in his module:
exports: [ProjectsService]
And importing the full ProjectsModule into the PlansModule:
imports: [
TypeOrmModule.forFeature([PlanRepository]),
AuthModule,
ProjectsModule
],
Sorry for the long post, trying to be exhaustive.
import { Injectable, NotFoundException } from '#nestjs/common';
import { InjectRepository } from '#nestjs/typeorm';
import { User } from '../auth/user.entity';
import { PlanRepository } from './plan.repository';
import { GetPlanFilterDto } from './dto/get-plan-filter.dto';
import { Plan } from './plan.entity';
import { CreatePlanDto } from './dto/create-plan.dto';
#Injectable()
export class PlansService {
constructor(
#InjectRepository(PlanRepository)
private planRepository: PlanRepository,
) {}
async getPlans(filterDto: GetPlanFilterDto, user: User): Promise<Plan[]> {
return this.planRepository.find({ ...filterDto, ownerId: user.id });
}
async getPlanById(id: string, user: User): Promise<Plan> {
return this.planRepository.findOne({
where: { id, ownerId: user.id },
});
}
async createPlan(createPlanDto: CreatePlanDto, user: User): Promise<Plan> {
const { project, ...data } = createPlanDto;
return this.planRepository
.create({
projectId: project,
ownerId: user.id,
...data,
})
.save();
}
}
This PlanService only uses the internal methods of the Repository, if you're logging in the event of an Error, ExceptionFilter would be a suitable option for this: https://docs.nestjs.com/exception-filters.
Instead of checking if the plan had been found, you can use an interceptor:
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
NotFoundException,
} from '#nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
#Injectable()
export class PlanNotFoundInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map(plan => {
if (!plan) {
throw new NotFoundException("plan couldn't be found");
}
return plan;
}),
);
}
}
Then on your getById (Controller) use #UseInterceptor, this decouples your service, data access, logging, validation, etc..
I've simplified the implementation (for Interceptor), you may need to adjust it slightly to suit your exact need.
yarn run v1.22.4
$ jest
ts-jest[versions] (WARN) Version 24.9.0 of jest installed has not been tested with ts-jest. If you're experiencing issues, consider using a supported version (>=25.0.0 <
26.0.0). Please do not report issues in ts-jest if you are using unsupported versions.
ts-jest[versions] (WARN) Version 24.9.0 of jest installed has not been tested with ts-jest. If you're experiencing issues, consider using a supported version (>=25.0.0 <
26.0.0). Please do not report issues in ts-jest if you are using unsupported versions.
ts-jest[versions] (WARN) Version 24.9.0 of jest installed has not been tested with ts-jest. If you're experiencing issues, consider using a supported version (>=25.0.0 <
26.0.0). Please do not report issues in ts-jest if you are using unsupported versions.
PASS src/auth/user.repository.spec.ts
PASS src/projects/projects.service.spec.ts
PASS src/auth/jwt.strategy.spec.ts
PASS src/auth/user.entity.spec.ts
Test Suites: 4 passed, 4 total
Tests: 18 passed, 18 total
Snapshots: 0 total
Time: 3.774s, estimated 4s
Ran all test suites.
Done in 4.58s.
I haven't spent much time reviewing your tests, but the changes made haven't made any breaking changes to the unit tests (can't say the same for e2e, personally don't use Cucumber.js).
The point of this answer isn't to provide you the code needed, but the abstractions you can use to solve the tightly coupled components.
You can also use the Interceptor to validate the request, check if a project is present, check if it exists, if not abort with error. Again decoupling your error handling from your controller/service/whatever.
You also have the option to pluck/add things to the request, for example a .user that's authenticated, or a value from a header. (Could be useful if you want to send the projectId into the Controller via the Request object).

Is there a way to get the details of a class in a typescipt program so that I can pass these details to a logger(winston)?

I have written a wrapper over winston.In order to test this I have also written a "Greeting class" wherein I make a call to logger.info().I want my log to also display the class name.
For this when I invoke logger.info() I also have to pass the name of the class so that my wrapper knows which class the logger was invoked at.But the problem is that I do not want the programmer to specify that.I want to create a level of abstraction.My wrapper should be user friendly.
Is there a way to do this?
If the above mentioned problem is not solvable is there a way to get all details of a class and its methods.I tried passing "this" but im getting something like {greeting:world}
Sorry if im not using correct terminology.I am new to javascript and node.js.
wrapper:
import "reflect-metadata";
import { injectable } from "inversify";
import { createLogger, format, transports, Logger as Iwinston } from "winston";
const { combine, timestamp, metadata, json, errors, label, printf } = format;
#injectable()
export default class Logger {
public logger: Iwinston;
private selfInfo: string;
constructor() {
this.selfInfo = "winston version >= 3.x";
this.logger = createLogger(this.readOptions());
this.addTransportConsole(this.getTransportConsoleOptions());
}
public info(message: string, data?: any): void {
console.log(data);
this.logger.info(message,data);
}
public error(message: string, data?: any): void {
this.logger.error(message, data);
}
public getCoreVersion(): string {
return this.selfInfo;
}
private readOptions(): Object {
return {
format: combine(
label({ label: process.env.APP_NAME || "Unknown App" }),
errors({ stack: true }),
timestamp(),
json(),
),
transports:[
new transports.File({filename:'error.log',level:'info'})
]
};
}
private getTransportConsoleOptions(): transports.ConsoleTransportOptions {
return {
debugStdout: false
};
}
private addTransportConsole(
options?: transports.ConsoleTransportOptions
): void {
this.logger.add(new transports.Console(options));
}
}
My test:
import Logger from '../lib/component/logger/appLogger/winstonLogger'
let logger:Logger
logger = new Logger()
test('Winston Logger Test',() => {
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
logger.info('hello',this)
}
greet() {
console.log(globalThis.Document)
logger.info('in greet')
return "Hello, " + this.greeting;
}
}
let greeter = new Greeter("world");
greeter.greet()
})
//{"level":"info","message":"hello","metadata":{},"label":"Unknown App","timestamp":"2019-11-22T09:30:07.256Z"}
So what i would do here is make them pass the class as a parameter when you create the instance of the logger:
const logger = new Logger<Greeter>(Greeter)
test('Winston Logger Test',() => {
class Greeter {
....
This then means this logger instance is linked to this class now for the whole time it is used. In your logger class you can then in the constructor set the name of that class which i have abstracted away in nameByInstance method.
import "reflect-metadata";
import { injectable } from "inversify";
import { createLogger, format, transports, Logger as Iwinston } from "winston";
const { combine, timestamp, metadata, json, errors, label, printf } = format;
#injectable()
export default class Logger<TInstance> {
public logger: Iwinston;
private selfInfo: string;
private loggerInstanceName: string;
constructor(instance: TInstance) {
this.selfInfo = "winston version >= 3.x";
this.logger = createLogger(this.readOptions());
this.addTransportConsole(this.getTransportConsoleOptions());
this.loggerInstanceName = this.nameByInstance(instance);
console.log(this.loggerInstanceName) // Greeter :)
}
public info(message: string, data?: any): void {
console.log(data);
this.logger.info(message,data);
}
public error(message: string, data?: any): void {
this.logger.error(message, data);
}
public getCoreVersion(): string {
return this.selfInfo;
}
private readOptions(): Object {
return {
format: combine(
label({ label: process.env.APP_NAME || "Unknown App" }),
errors({ stack: true }),
timestamp(),
json(),
),
transports:[
new transports.File({filename:'error.log',level:'info'})
]
};
}
private getTransportConsoleOptions(): transports.ConsoleTransportOptions {
return {
debugStdout: false
};
}
private addTransportConsole(
options?: transports.ConsoleTransportOptions
): void {
this.logger.add(new transports.Console(options));
}
private nameByInstance(type: TInstance): string {
return type.prototype["constructor"]["name"];
}
}
This now means you can just let them do logger.info('hello') and you have context to the class name in the logger instance loggerInstanceName to add that to the message they have passed in within the info method in logger without them having to do it.. nice hey?!

How to use query parameters in Nest.js?

I am a freshman in Nest.js.
And my code as below
#Get('findByFilter/:params')
async findByFilter(#Query() query): Promise<Article[]> {
}
I have used postman to test this router
http://localhost:3000/article/findByFilter/bug?google=1&baidu=2
Actually, I can get the query result { google: '1', baidu: '2' }. But I'm not clear why the url has a string 'bug'?
If I delete that word just like
http://localhost:3000/article/findByFilter?google=1&baidu=2
then the postman will shows statusCode 404.
Actually, I don't need the word bug, how to custom the router to realize my destination just like http://localhost:3000/article/findByFilter?google=1&baidu=2
Here's another question is how to make mutiple router point to one method?
Query parameters
You have to remove :params for it to work as expected:
#Get('findByFilter')
async findByFilter(#Query() query): Promise<Article[]> {
// ...
}
Path parameters
The :param syntax is for path parameters and matches any string on a path:
#Get('products/:id')
getProduct(#Param('id') id) {
matches the routes
localhost:3000/products/1
localhost:3000/products/2abc
// ...
Route wildcards
To match multiple endpoints to the same method you can use route wildcards:
#Get('other|te*st')
will match
localhost:3000/other
localhost:3000/test
localhost:3000/te123st
// ...
If you have you parameter as part or url: /articles/${articleId}/details, you wold use #Param
#Get('/articles/:ARTICLE_ID/details')
async getDetails(
#Param('ARTICLE_ID') articleId: string
)
IF you want to provide query params /article/findByFilter/bug?google=1&baidu=2, you could use
#Get('/article/findByFilter/bug?')
async find(
#Query('google') google: number,
#Query('baidu') baidu: number,
)
We can use #Req()
import { Controller, Get, Req } from '#nestjs/common';
import { Request } from 'express';
(...)
#Get(':framework')
getData(#Req() request: Request): Object {
return {...request.params, ...request.query};
}
/nest?version=7
{
"framework": "nest",
"version": "7"
}
read more
You can use the #Req decorator, and use param object, see :
#Get()
findAll(
#Req() req: Request
): Promise<any[]> {
console.log(req.query);
// another code ....
}
For better explaining I wrote a pagination example with number transformer class:
class QueryDto {
#Type(() => Number)
#IsInt()
public readonly page: number;
#Type(() => Number)
#IsInt()
public readonly take: number;
}
#Injectable()
class QueryTransformPipe implements PipeTransform {
async transform(value: QueryRequestDto, { metatype }: ArgumentMetadata) {
if (!metatype) {
return value;
}
return plainToInstance(metatype, value);
}
}
#Controller()
class YourController {
#Get()
// also you can use it with pipe decorator
// #UsePipes(new QueryTransformPipe())
public async getData(#Query(new QueryTransformPipe()) query?: QueryRequestDto): Promise<any[]> {
// here you get instanceof QueryTransformPipe
// and typeof query.page === 'number' && typeof query.take === 'number'
}
}

Angular5 - Error: Function DocumentReference.set() called with invalid data. Data must be an object, but it was: a custom PlatformModel object

I find allready some posts on google where people solve this problem. but i cant reproduce the solutions on my project.
My Interface:
declare module PlatformInterface {
export interface Design {
primaryColor: string;
backgroundImage: string;
}
export interface Saga {
id: string;
name: string;
short_desc: string;
desc: string;
manga: Manga[];
anime: Anime[];
}
export interface Root {
id: string;
name: string;
design: Design[];
saga: Saga[];
}
}
My Model:
export class PlatformModel implements PlatformInterface.Root {
id: string;
name: string;
design = [];
saga = [];
constructor(obj?: any) {
this.id = obj.name.toLowerCase().replace(' ', '-');
this.name = obj.name;
this.design = obj.design;
this.saga = obj.saga;
}
}
My Service:
#Injectable()
export class PlatformService {
public list$: Observable<PlatformModel[]>;
private _platform: AngularFirestoreCollection<PlatformModel>;
constructor(db: AngularFirestore) {
this._platform = db.collection<PlatformModel>('platforms');
this.list$ = this._platform.valueChanges();
}
/** Get Platform by id */
get(id: string): Observable<PlatformModel> {
return this._platform.doc<PlatformModel>(id).valueChanges();
}
/** Add / Update Platform */
set(id: string, platforms: PlatformModel) {
return fromPromise(this._platform.doc(id).set(platforms));
}
/** Remove Platform */
remove(id: string) {
return fromPromise(this._platform.doc(id).delete());
}
}
My function in Component.ts
constructor(public _platformService: PlatformService) {
}
addPlatform(name: string) {
if (name !== '') {
const platform = new PlatformModel({
name: name,
design: [],
saga: []
});
this._platformService.set(platform.id, platform).subscribe();
}
}
The Angular Compiler dont Throw any error, But when i try to fire the addPlatform Function i get in Browser this error:
ERROR Error: Function DocumentReference.set() called with invalid data. Data must be an object, but it was: a custom PlatformModel object
The Errors Says that the Data must be an object, but it is allready an object or not? i mean i define in the service it with:
public list$: Observable<PlatformModel[]>;
[] Makes it to an object or not?
I've found some clarification here Firestore: Add Custom Object to db
while firebase could send the data inside your object to the database, when the data comss back it cannot instantiate it back into an instance of your class. Therefore classes are disallowed
my workaround for custom class was
this.db.collection(`${this.basePath}/`).doc(custom_class.$key)
.set(Object.assign({}, JSON.parse(JSON.stringify(custom_class))))
.then( ret => {
log.debug('file added', ret);
}).catch( err => {
log.error(err);
});
so I guess in your case it would be
/** Add / Update Platform */
set(id: string, platforms: PlatformModel) {
return fromPromise(this._platform.doc(id).set(Object.assign({},JSON.parse(JSON.stringify(platforms))));
}
For adding a Map into Firestore document you'll have to use:
Object.assign({}, JSON.parse(JSON.stringify(YOUR_MAP)))

Trying to use stomp.js in angular2

I am trying to implement a Stomp Websocket client using stomp.js.
I am using angular2 with typescript and webpack and am really new to all of those technologies.
My angular2 project was built on this seed:
https://github.com/angular/angular2-seed
As a guide for implementing the stomp.js client I used https://github.com/sjmf/ng2-stompjs-demo
The error I am currently getting is the following:
?d41d:73 EXCEPTION: TypeError: Cannot read property 'client' of undefined in [null]
The error is happening in this method:
public configure() : void {
// Check for errors
if (this.state.getValue() != STOMPState.CLOSED) {
throw Error("Already running!");
}
let scheme : string = 'ws';
if( AppSettings.IS_SSL ) {
scheme = 'wss';
}
this.client = Stomp.client(
scheme + '://'
+ AppSettings.HOST + ':'
+ AppSettings.WEBSOCK_PORT
+ AppSettings.WEBSOCK_ENDPOINT
);
this.client.heartbeat.incoming = AppSettings.HEARTBEAT;
}
So Stomp seems to be undefined.
I am importing:
import {Stomp} from "stompjs";
I have installed stomp.js with npm like this
npm install --save stompjs
And my stompjs module looks like this:
declare module "stompjs" {
export interface Client {
heartbeat: any;
debug(...args: string[]);
connect(...args: any[]);
disconnect(disconnectCallback: () => any, headers?: any);
send(destination: string, headers?:any, body?: string);
subscribe(destination: string, callback?: (message: Message) => any, body?: string);
unsubscribe();
begin(transaction: string);
commit(transaction: string);
abort(transaction: string);
ack(messageID: string, subscription: string, headers?: any);
nack(messageID: string, subscription: string, headers?: any);
}
export interface Message {
command: string;
headers: any;
body: string;
ack(headers?: any);
nack(headers?: any);
}
export interface Frame {
constructor(command: string, headers?: any, body?: string);
toString(): string;
sizeOfUTF8(s: string);
unmarshall(datas: any);
marshall(command: string, headers?, body?);
}
export interface Stomp {
client: Client;
Frame: Frame;
over(ws: WebSocket);
}
}
I think I am missing the connection between my module and the actual library, but I don't really know how to that and I can't figure it out from the github demo either.
Thanks in advance for any help!
Did you try to export a variable too alongside the interface?
export var Stomp: Stomp;
Using Stomp.js directly in Angular2 (or Angular4) is definitely non trivial.
For a proper Angular2 (and Angular4) type StompService using Observables please refer to https://github.com/stomp-js/ng2-stompjs.
There are sample apps for Angular2 and Agular4 as well.

Categories