NestJs using a service from another Module in a Custom Repository - javascript

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).

Related

Nest js custom logger doubled console.logs

have a problem with implementing custom logger by extending default logger in NestJS. I followed documentation https://docs.nestjs.com/techniques/logger#using-the-logger-for-application-logging.
import { Injectable, Logger, Scope } from '#nestjs/common';
#Injectable({ scope: Scope.TRANSIENT })
export class LoggerService extends Logger {
votes = 0;
debug(message: string, trace?: string) {
super.debug(message, trace);
}
error(message: string, trace?: string) {
super.error(message, trace);
}
log(message: string, trace?: string) {
this.votes = this.votes + 1;
console.log(this.votes, 'log', message);
super.log(message, trace);
}
verbose(message: string, trace?: string) {
super.verbose(message, trace);
}
warn(message: string, trace?: string) {
super.warn(message, trace);
}
}
and in main.ts
import { NestFactory } from '#nestjs/core';
import { AppModule } from './app.module';
import { AllExceptionsFilter } from './shared/exception-filters/all-exception-filters';
import { LoggerService } from './shared/modules/logger/logger.service';
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
logger: false,
});
app.useLogger(new LoggerService());
app.useGlobalFilters(new AllExceptionsFilter());
await app.listen(3000);
}
bootstrap();
And in my controller
Controller('user')
export class UserController {
constructor(private readonly userService: UserService, private logger: LoggerService) {}
#Get('/logger-example')
getLoggerExample() {
this.logger.log('it works ?');
return 'loggerExample';
}
And of course user module imports logger module.
I have two problems with that logger:
First one:
When i hit user/logger-example, i got two times log from Logger. It looks like that logger from main.ts exactly app.useLogger(new LoggerService()); is executed along with injected instance, in future i want to put some async logger like pino or something and i'm afraid that log will be send two times. I assume something is wrong with my configuration, because it doesn't look like some common problem. What is more from default Logger i got properly one message.
The second one:
If i also inject that service to different module, for example payments. And made the same endpoint as for user, votes field on Logger class is always 0. Is it proper behaviour for transient scope of injection ? Because as i understood it should be injected per module. So in one module that counter shouldn't be reseted.
Is it properly configured ? Or it is normal behaviour ? I'm really afraid about two loggers executed in one this.logger.log.
I found, that all elements passed into super.log are forced to be another log line. So duplicated logs are from two arguments at super.log. Please forward only message.
log(message: string) {
this.votes = this.votes + 1;
console.log(this.votes, 'log', message);
super.log(message);
}

NestJs: Make sure your class is decorated with an appropriate decorator

I am using graphql-request as a GraphQL client to query a headless CMS to fetch stuff, modify and return to the original request/query. headless cms is hosted separately fyi.
I have the following code :
#Query(returns => BlogPost)
async test() {
const endpoint = 'https://contentxx.com/api/content/project-dev/graphql'
const graphQLClient = new GraphQLClient(endpoint, {
headers: {
authorization: 'Bearer xxxxxxx',
},
})
const query = gql`
{
findContentContent(id: "9f5dde89-7f9b-4b9c-8669-1f0425b2b55d") {
id
flatData {
body
slug
subtitle
title
}
}
}`
return await graphQLClient.request(query);
}
BlogPost is a model having the types :
import { Field, ObjectType } from '#nestjs/graphql';
import { BaseModel } from './base.model';
import FlatDateType from '../resolvers/blogPost/types/flatDatatype.type';
#ObjectType()
export class BlogPost extends BaseModel {
#Field({ nullable: true })
id!: string;
#Field((type) => FlatDateType)
flatData: FlatDateType;
}
and FlatDateType has the following code
export default class FlatDateType {
body: string;
slug: string;
subtitle: string;
title: string;
}
it throws the following exception :
Error: Cannot determine a GraphQL output type for the "flatData". Make
sure your class is decorated with an appropriate decorator.
What is missing in here?
How is your graphql server supposed to understand the type of FlatDataType when there's no information about it being passed to the graphql parser? You need to add the graphql decorators to it as well. #ObjectType(), #Field(), etc.
FlatDataType is not defined as #ObjectType(), therefore type-graphql (or #nestjs/graphql) can't take it as an output in GraphQL.

How to use validation in NestJs with HTML rendering?

NestJS uses validation with validation pipes and
#UsePipes(ValidationPipe)
If this fails it throws an exception. This is fine for REST APIs that return JSON.
How would one validate parameters when using HTML rendering and return
{ errors: ['First error'] }
to an hbs template?
You can create an Interceptor that transforms the validation error into an error response:
#Injectable()
export class ErrorsInterceptor implements NestInterceptor {
intercept(
context: ExecutionContext,
call$: Observable<any>,
): Observable<any> {
return call$.pipe(
// Here you can map (or rethrow) errors
catchError(err => ({errors: [err.message]}),
),
);
}
}
You can use it by adding #UseInterceptors(ErrorsInterceptor) to your controller or its methods.
I've been driving myself half mad trying to find a "Nest like" way to do this while still retaining a degree of customisability, and I think I finally have it. Firstly, we want an error that has a reference to the exisiting class-validator errors, so we create a custom error class like so:
import { ValidationError } from 'class-validator';
export class ValidationFailedError extends Error {
validationErrors: ValidationError[];
target: any;
constructor(validationErrors) {
super();
this.validationErrors = validationErrors;
this.target = validationErrors[0].target
}
}
(We also have a reference to the class we tried to validate, so we can return our object as appropriate)
Then, in main.ts, we can set a custom exception factory like so:
app.useGlobalPipes(
new ValidationPipe({
exceptionFactory: (validationErrors: ValidationError[] = []) => {
return new ValidationFailedError(validationErrors);
},
}),
);
Next, we create an ExceptionFilter to catch our custom error like so:
#Catch(ValidationFailedError)
export class ValidationExceptionFilter implements ExceptionFilter {
view: string
objectName: string
constructor(view: string, objectName: string) {
this.view = view;
this.objectName = objectName;
}
async catch(exception: ValidationFailedError, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
response.render(this.view, {
errors: exception.validationErrors,
[this.objectName]: exception.target,
url: request.url,
});
}
}
We also add an initializer, so we can specify what view to render and what the object's name is, so we can set up our filter on a controller method like so:
#Post(':postID')
#UseFilters(new ValidationExceptionFilter('blog-posts/edit', 'blogPost'))
#Redirect('/blog-posts', 301)
async update(
#Param('id') postID: string,
#Body() editBlogPostDto: EditBlogPostDto,
) {
await this.blogPostsService.update(postID, editBlogPostDto);
}
Hope this helps some folks, because I like NestJS, but it does seem like the docuemntation and tutorials are much more set up for JSON APIs than for more traditional full stack CRUD apps.

how to access object properties from TypeScript?

I'm new to Angular and TypeScript and just started working on a project using MEAN stack (MongoDB, Express, Angular, Node.js).
I created this mongoose module :
import * as mongoose from 'mongoose';
var Schema = mongoose.Schema;
const entrepriseSchema = new mongoose.Schema({
name: {type: String, unique: true, required : true},
telephone: Number,
logo: String,
web_site: String,
sites: [
{site_id: {type: Schema.Types.ObjectId, ref: 'Site'}}
]
});
const Entreprise = mongoose.model('Entreprise', entrepriseSchema);
export default Entreprise;
and this is my entreprise.component.ts :
import { Component, OnInit } from '#angular/core';
import { Http } from '#angular/http';
import { FormGroup, FormControl, Validators, FormBuilder } from '#angular/forms';
import { ActivatedRoute } from '#angular/router';
import { EntrepriseService } from '../services/entreprise.service';
import { SiteService } from '../services/site.service';
#Component({
selector: 'app-entreprise',
templateUrl: './entreprise.component.html',
styleUrls: ['./entreprise.component.scss'],
providers: [EntrepriseService, SiteService]
})
export class EntrepriseComponent implements OnInit {
entreprise = {};
sites = [];
id: String;
constructor(private entrepriseService: EntrepriseService,
private siteService: SiteService,
private http: Http,
private route: ActivatedRoute) {
this.id = route.snapshot.params['id'];
}
ngOnInit() {
this.getEntrepriseById(this.id);
//not working
//console.log(this.entreprise.name);
//console.log(this.entreprise.sites);
//this.getSitesIn(this.entreprise.sites);
}
getEntrepriseById(id) {
this.entrepriseService.getEntreprise(id).subscribe(
data => this.entreprise = data,
error => console.log(error)
);
}
getSitesIn(ids) {
this.siteService.getSitesIn(ids).subscribe(
data => this.sites = data,
error => console.log(error)
);
}
}
when I try to display the properties of the returned from entreprise.component.html it works fine and displays all the properties :
<h3>{{entreprise.name}}</h3>
<div *ngFor="let site of entreprise.sites">
{{site.site_id}}
</div>
{{entreprise.logo}}
{{entreprise.web_site}}
but how can I access the same properties on the TypeScript side ?
The commented code in the EntrepriseComponent is what I'm trying to accomplish but it's not working since this.entreprise is type {} .
The Enterprise model/schema that you created in Mongoose in Node.js resides on the server side. If you want the TypeScript code on the UI to recognize the properties in Enterprise, you will have to create a class in your angular codebase.
Create a folder named, say, models at the same level as your services folder. (Optional)
Create two files named site.ts and enterprise.ts in the models folder created in the previous step (You can put these file at a different location if you want) with the following contents:
site.ts
export interface Site {
site_id?: string;
}
enterprise.ts
import { Site } from './site';
export interface Enterprise {
name?: string;
telephone?: string;
logo?: string;
web_site?: string;
sites?: Site[];
}
Now, inside the EntrepriseComponent file, add the following imports
import { Enterprise} from '../models/entreprise';
import { Site } from '../models/site';
And change the first lines inside the EntrepriseComponent file to
export class EntrepriseComponent implements OnInit {
entreprise: Enterprise = {};
sites: Site[] = [];
Now, the enterprise attribute will be of type Enterprise and you will be able to access the properties that we declared in the enterprise.ts file.
Update:
Also, you cannot console.log(this.enterprise.name) immediately after this.getEntrepriseById(this.id); in your ngOnInit() function. This is because the web service you are making to get the enterprise object would not have resolved when you are trying to log it to the console.
If you want to see the enterprise object in the console or you want to run some code that needs to run after the service call has resolved and the this.enterprise object has a value, the best place to do this would be your getEntrepriseById function. Change the getEntrepriseById function to
getEntrepriseById(id) {
this.entrepriseService.getEntreprise(id).subscribe(
data => {
this.enterprise = data;
console.log(this.enterprise.name);
// Any code to run after this.enterprise resolves can go here.
},
error => console.log(error)
);
}

Angular2 Service Returns Undefined

For some reason my services aren't working. I've been lurking SO for two days trying to find similar questions, but they don't fit my problem.
Service.ts:
import { Injectable } from '#angular/core';
import { Http, Response } from '#angular/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/map';
import { CarObject } from './make';
#Injectable()
export class EdmundsService {
private stylesurl = 'REDACTED';
constructor(private http: Http) { }
getCars(): Observable<CarObject[]> {
return this.http.get(this.stylesurl)
.map(this.extractData)
.catch(this.handleError);
}
private extractData(res: Response) {
let body = res.json();
return body.data || { };
}
private handleError (error: Response | any) {
// In a real world app, we might use a remote logging infrastructure
let errMsg: string;
if (error instanceof Response) {
const body = error.json() || '';
const err = body.error || JSON.stringify(body);
errMsg = `${error.status} - ${error.statusText || ''} ${err}`;
} else {
errMsg = error.message ? error.message : error.toString();
}
console.error(errMsg);
return Observable.throw(errMsg);
}
}
These are my 'models':
class Style {
id: number;
name: string;
make: Make;
model: Model;
year: Year;
submodel: Submodel;
trim: string;
states: string[];
engine: Engine;
transmission: Transmission;
options: Options[];
colors: Color[];
drivenWheels: string;
numOfDoors: string;
squishVins: string[];
categories: Categories;
MPG: MPG;
manufacturerOptionCode: string;
}
export class CarObject {
styles: Style[];
stylesCount: number;
}
My component:
import { CarObject } from './make';
import { EdmundsService } from './edmunds-search-result.service';
#Component({REDACTED
providers: [EdmundsService] })
export class EdmundsSearchResultComponent implements OnInit {
cars: CarObject[];
errorMessage: string;
constructor(private _edmundsService: EdmundsService) { }
getCars(): void {
this._edmundsService.getCars()
.subscribe(
cars => this.cars = cars,
error => this.errorMessage = <any>error);
}
ngOnInit(): void {
this.getCars();
}
}
Component HTML:
{{ cars.stylesCount | async }}
Sample API Response: http://pastebin.com/0LyZuPGW
Error Output:
EXCEPTION: Error in ./EdmundsSearchResultComponent class
EdmundsSearchResultComponent - inline template:0:0 caused by:
Cannot read property 'stylesCount' of undefined
CarObject was designed to match the API Response, so it could be okay to remove the array brackets ( [] )
I don't know why this won't display the object data on my template despite closely following the Tour Of Heroes HTTP/Services tutorial.
What I am trying to do is make an HTTP request from variable 'styleurl' (which I see is successfully made by checking the 'Network' tab in chrome dev tools.) Using this API Response, I want my CarObject to 'consume' the json response, and be available to my component/template.
In your component you're reserving your car property, but you don't initialize it, so it remains undefined.
At the time your HTML renders the promise isn't fulfilled yet, your car is still undefined but you try to access a property from it.
A couple solutions:
preset it:
cars: CarObject = new CarObject(); // or <CarObject>{}
use the elvis operator in your template:
{{ cars?.stylesCount }}
use ngIf:
<div *ngIf="cars">{{ cars.styleCount }}</div>
There are probably a couple of more ways to handle this case.
See my update at the bottom regarding your usage of the async pipe. It probably leads to errors as well in the way you're trying to use it.
Besides, i would suggest reading up on TypeScript types as well as general best practices for angular and typescript especially regarding the usage of models, interfaces and such. Also using Observables would be a good idea instead of Promises.
There are some issues in your code, so this is just a hint, but elaborating on them has no place here i think and aren't the source of your problem.
Hope i could help.
Update:
About your usage of the async pipe:
The async pipe subscribes to an Observable or Promise and returns the latest value it has emitted.
You use it on an array of CarObjects which btw shouldn't be an array.
Take a look at the documentation for the async pipe for proper usage.

Categories