How to get Nestjs configService instance in main.ts before instantiating app - nestjs

My app instance depends on configuration : serviceName in that case
const serviceName = 'authentication-service'
const servicePrefix = `api/${serviceName}`;
const swaggerPrefix = 'swagger';
...
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter({
requestIdLogLabel: serviceName,
logger: true,
...
}),
{
// logger: ['log']
logger: process.env.DEV === '1' ? ['log', 'debug', 'error', 'verbose', 'warn'] : ['error', 'warn'],
});
NestJs documentation uses app instance to get the configService singleton :
const configService = app.get(ConfigService);
const port = configService.get('PORT');
Is there any way to get the configService instance before instantiating my app?

Nope. You could call dotenv's config() method yourself and populate process.env if you really need to, but that's about it. Technically, if you really wannted you could create a NestFactory.createApplicationContext() call and get the ConfigService from there, close the context and start up the application with the standard NestFactory.create, but that will end up doing the DI resolution twice, which would be a pretty slow startup

I had a similar need (for the Mikro-ORM configuration file). What I did is to create a new instance of ConfigService:
import {ConfigService} from '#nestjs/config'
const configService = new ConfigService();
Then I could use configService to get the port number:
configService.get<number>('PSQL_PORT', 5432)

What you need to do is to create a dummy app instance before the actual app.
const configApp = await NestFactory.create(AppModule);
let configService = configApp.get(ConfigService);
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter({
requestIdLogLabel: serviceName,
logger: true,
...
}),
{
// logger: ['log']
logger: process.env.DEV === '1' ? ['log', 'debug', 'error', 'verbose', 'warn'] : ['error', 'warn'],
});
//...

I am using custom configuration files as defined in https://docs.nestjs.com/techniques/configuration#custom-configuration-files
and with that I can use just use the following to get access to the config already in a js object.
async function bootstrap() {
const conf = configuration();
...
I initiall tried the solution from tukusejssirs which also worked but it would not map the env entries to js object yet so I came up with this one.
Comments appreciated as it feels a bit weird...

Related

Best practise for getting access to the app instance

I'm creating a NestJS app with Firebase cloud functions. I have to use both onRequest and onCreate (Firestore Events) from Firebase in the NestJS application. It's quite straightforward how to address the onRequest events. However, I'm not sure if I'm doing it right when having to do both at the same time. In order for me to pass the onCreate event changes and context to the service layer, I need to get access to the AppService class. To do that I need access to the app instance that has been created. However, I feel like I'm creating two instances of the app (refer to the code). I would like to know if my current my implementation is best to practice or if there is any way it can be improved. Please note I'm a frontend developer so this work is outside my comfort zone. I would like to know the best way to do this, especially if I have to work with more events such as onUpate, onDelete etc.
import { NestFactory } from '#nestjs/core';
import { ExpressAdapter } from '#nestjs/platform-express';
import { AppModule } from './app.module';
import * as express from 'express';
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
import { Express } from 'express-serve-static-core';
import { AppService } from './app.service';
const server = express();
export const createNestServer = async (expressInstance: Express) => {
//FIRST APP INSTANCE
const app = await NestFactory.create(
AppModule,
new ExpressAdapter(expressInstance),
);
admin.initializeApp();
return app.init();
};
createNestServer(server)
.then((v) => console.log('Nest Ready'))
.catch((err) => console.error('Nest broken', err));
//exporting all onRequest events
export const api = functions.https.onRequest(server);
//exporting the onUserCreate event
exports.onUserCreate = functions.firestore
.document('users/{docId}')
.onWrite(async (change, context) => {
console.log('Changes are tracked');
//SECOND APP INSTANCE
const app = await NestFactory.create(AppModule);
return app.get(AppService).onCreate(change, context);
});
I assume you have something like a UsersController (and others) that handles retrieving, creating, updating the users as part of the NestJS application. Just pass the Express instance (wrapped by the NestJS adapter) to the Cloud Function HTTPS handler (onRequest). NestJS will take care of setting up the routes.
export const api = functions.https.onRequest(server)
#Controller('users')
export class UsersController {
constructor(private readonly usersService: UserService) {}
#Get(':userId')
async getUser(#Param('userId') id: String) {
// retrieve user
}
#Post()
async create(#Body() createUserDto: CreateUserDto) {
// create a new user
}
...
}
#Module({
controllers: [UsersController],
providers: [AppService, UsersService],
exports: [AppService, UsersService]
})
export class AppModule {}
The AppService is a provider registered in the AppModule. You could extract it from the INestApplication instance.
const server = express();
let appService: AppService;
export const createNestServer = async (expressInstance: Express) => {
const app = await NestFactory.create(
AppModule,
new ExpressAdapter(expressInstance),
);
admin.initializeApp();
appService = app.get(AppService);
return app.init();
};
Then you could use it within the onWrite firestore trigger.
Seems like you are trying to do 2 differents tasks with the same NestJS app. Just let the NestJS (or ExpressJS) app handle the HTTPS requests. And for the onCreate, onWrite ... triggers implement a different solution. Perhaps don't rely on NestJS to handle these types of triggers.
NestJS personally feels like overkill, you could just use ExpressJS. You wouldn't have to register the AppService as a provider and jump through hoops.
The following approach has allowed me to solve the multiple instatiation issue of the app instance. Hopefully this is a good and acceptable way to do this. Feedback is more than welcome.
const server = express();
export const createNestServer = async (expressInstance: Express) => {
//FIRST APP INSTANCE
const app = await NestFactory.create(
AppModule,
new ExpressAdapter(expressInstance),
);
admin.initializeApp();
return app.init();
};
const main = createNestServer(server);
export const api = functions.https.onRequest(server);
exports.onUserWrite = functions.firestore
.document('users/{docId}')
.onWrite((change, context) =>
main.then((app) => {
return app.get(AppService).onCreate(change, context);
}),
);

How can I use fastify request logger in other classes without having to pass it as a parameter?

I'm new in nodejs, I'm using fastify and I want to be able to use the req.logger in all the classes functions of the flow, this because I have a the request-id on req.logger, the first solution that came to my mind is to pass as a parameter the logger through all the function/classes but I think that would make the code kind of dirty, here is an example of my code:
app.ts
import pino from 'pino';
import fastify from 'fastify';
declare module 'fastify' {
interface FastifyInstance {
// augment fastify instance with the config object types
config: Config;
}
}
function build() {
const app = fastify({
logger: pino({
name: process.env.NAME,
level: process.env.LOG_LEVEL,
}),
disableRequestLogging: true,
requestIdHeader: 'correlation-id',
requestIdLogLabel: 'correlationId',
});
// register plugins
app.register(apiRoutes, fastify => ({
getObjectUseCase: new GetObjectUseCase(
new TestClass()),
}));
return app;
}
export { build };
routes.ts
import { FastifyPluginCallback } from 'fastify';
import { StatusCodes } from 'http-status-codes';
export const apiRoutes: FastifyPluginCallback<RoutesOpts> = async (fastify, options, done) => {
const getObjectUseCase = options.getObjectUseCase;
fastify.get<object>('/v1/api/:id', async (req, reply) => {
const id = req.params.payoutId;
req.logger.info('This is a logger print'); // has the correlation id inside it while printing
const storedObject = await getObjectCase.execute(id);
reply.code(StatusCodes.OK).send(storedObject);
});
}
GetObjectUseCase.ts
export class GetObjectUseCase {
private anotherClass: TestClass;
constructor(anotherClass: TestClass) {
this. anotherClass = anotherClass;
}
async execute(id: string): Promise<StoredObject> {
// I want to use the logger here with have the correlation id on it without having to pass it as an argument on the method, how is it posible?
return this.anotherClass.getById(id);
// also needed to use it inside anotherClass.getById so I will need to pass the logger also in the method
}
}
Hope I have been clear.
Thanks!
This may not be the best or only way to do it, but this has worked for me in the past.
Typically I structure my projects with an app.ts that just instantiates my FastifyInstance and then exports the log from that created instance. This allows me to use the log where ever I want to.
It looks something like this.
app.ts
import fastify from 'fastify';
const app = fastify({ logger: true /* Your logging configuration */});
export default app;
export const logger = app.log; // Allows me to log where ever I want.
server.ts
import app from './app';
... // All your fastify configuration and other stuff.
app.listen({ ... });
Now I can use the logger outside of fastify stuff.
get-object-use-case.ts
import { logger } from './app'; // Import your fastify logger to use in this class.
export class GetObjectUseCase {
private anotherClass: TestClass;
constructor(anotherClass: TestClass) {
this. anotherClass = anotherClass;
}
async execute(id: string): Promise<StoredObject> {
logger.info({/* Whatever you want to log here. */}); // Now you can use the logger here.
return this.anotherClass.getById(id); // You can just import the logger into the TestClass file to get logging enabled there.
}
}
This even allows you to log before your FastifyInstance is started. Check out this codesandbox for a running example.

Error: metatype is not a constructor when using instance of own HTTPS Server class

Good evening, I am playing around with nest and want to achieve an own HTTPS-Server that can be instantiated everywhere in other projects. Right at the beginning I get the following error-message:
TypeError: metatype is not a constructor
… when I init the following HTTPS-Server:
import { Injectable } from '#nestjs/common';
import { NestFactory } from '#nestjs/core';
import { FastifyAdapter, NestFastifyApplication } from '#nestjs/platform-fastify';
import * as fs from 'fs';
#Injectable()
export class HttpsServer {
constructor() {}
async bootstrap() {
const httpsOptions = {
key: fs.readFileSync('./certs/server.key'),
cert: fs.readFileSync('./certs/server.cert'),
};
const app = await NestFactory.create<NestFastifyApplication>(
new FastifyAdapter({ https: httpsOptions }),
);
await app.listen(443);
}
}
like this:
import { Logger } from '#nestjs/common';
import { HttpsServer } from 'server-lib';
const logger = new Logger();
const app = new HttpsServer();
app.bootstrap().then(() => {
logger.log('Bootstrap complete!');
}).catch((error) => {
logger.log('Bootstrap failed: ', error);
process.exit(1);
});
Thx for help...
All Nest applications needs to have a "RootModule" of some sort. With nest new applications this is a AppModule. This class holds the metadata for the server for how to tie everything together and run. What you could probably do is modify your HttpModule's constructor to do something like
export class HttpsServer {
constructor(private readonly rootModule: Type<any>) {} // Type comes from #nestjs/common
async bootstrap() {
const httpsOptions = {
key: fs.readFileSync('./certs/server.key'),
cert: fs.readFileSync('./certs/server.cert'),
};
const app = await NestFactory.create<NestFastifyApplication>(
this.rootModule,
new FastifyAdapter({ https: httpsOptions }),
);
await app.listen(443);
}
}
So now when you call new HttpServer() you pass in the root module and have everything else already set up. The NestFactory will instantiate the metadata from there properly, and you'll use the FastifyAdapter.
For more information, I suggest you follow the docs overview to get a feeling of how these classes fit together and why they're needed.
You probably have used an incorrect guard.
Check #UseGuards() and use the correct guard for that function.
If you're importing some files from index in the same folder such as all entities as
import * as Entities from './entities
make sure that you have that ./entities in the folder path and just plan '.'
It does not throws error but while building this is not recognized. use the full file path and it will most probably solve the issue.

Right way to get ConfigService when bootstrap microservice

I would like to know if i'm getting ConfigService the right way during bootstrap my microservice.
Is there a way to do it using NestFactory.createMicroservice()?
async function bootstrap() {
const app = await NestFactory.create(CoreModule, {
logger: new MyLogger(),
});
const configService: ConfigService = app.get(ConfigService);
app.connectMicroservice({
transport: Transport.TCP,
options: {
port: configService.PORT,
},
});
await app.startAllMicroservicesAsync();
}
Yes there is a way to do it in the NestFactory, you're already doing it the right way! If you'd like another example, this is my bootstrap function:
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
logger: new MyLogger()
});
const config = app.get<ConfigService>(ConfigService);
const port = config.get('PORT');
configure(app, config);
await app.listen(port);
scribe.info(
`Listening at http://localhost:${port}/${config.get('GLOBAL_PREFIX')}`
);
}
Where MyLogger is a custom implementation of the Nest default logger and configure(app, config) is a lot of application configuration done in a separate file to keep the bootstrap function lean and easy to maintain. Of course, the "listening at ..." also needs to be changed before heading to production, but this app is still in development for me.
The only thing I would suggest to you is changing
const configService: ConfigService = app.get(ConfigService);
to
const configService = app.get<ConfigService>(ConfigService);
using the generics for app.get() to give you the type information.

Mock fs function with jest

First of all, I'm new to es6 and jest.
I have a Logger class for instantiate winston and I would like to test it.
Here my code :
const winston = require('winston');
const fs = require('fs');
const path = require('path');
const config = require('../config.json');
class Logger {
constructor() {
Logger.createLogDir(Logger.logDir);
this.logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
new (winston.transports.Console)({
format: winston.format.combine(
winston.format.colorize({ all: true }),
winston.format.simple(),
),
}),
new (winston.transports.File)({
filename: path.join(Logger.logDir, '/error.log'),
level: 'error',
}),
new (winston.transports.File)({
filename: path.join(Logger.logDir, '/info.log'),
level: 'info',
}),
new (winston.transports.File)({
filename: path.join(Logger.logDir, '/combined.log'),
}),
],
});
}
static get logDir() {
return (config.logDir == null) ? 'log' : config.logDir;
}
static createLogDir(logDir) {
if (!fs.existsSync(logDir)) {
// Create the directory if it does not exist
fs.mkdirSync(logDir);
}
}
}
exports.logger = new Logger().logger;
export default new Logger();
I would like to test my function createLogDir().
I my head, I think it's a good idea to test the state of fs.existsSync.
If fs.existsSync return false, fs.mkdirSync must be called.
So I try to write some jest test :
describe('logDir configuration', () => {
test('default path must be used', () => {
const logger = require('./logger');
jest.mock('fs');
fs.existsSync = jest.fn();
fs.existsSync.mockReturnValue(false);
const mkdirSync = jest.spyOn(logger, 'fs.mkdirSync');
expect(mkdirSync).toHaveBeenCalled();
});
});
However, I've got an error :
● logDir configuration › default path must be used
Cannot spy the fs.mkdirSync property because it is not a function; undefined given instead
18 | fs.existsSync = jest.fn();
19 | fs.existsSync.mockReturnValue(true);
> 20 | const mkdirSync = jest.spyOn(logger, 'fs.mkdirSync');
21 | expect(mkdirSync).toHaveBeenCalled();
22 | });
23 | });
at ModuleMockerClass.spyOn (node_modules/jest-mock/build/index.js:590:15)
at Object.test (src/logger.test.js:20:28)
Can you help me to debug and test my function please ?
Regards.
The error there is because it is looking for a method called fs.mkdirSync on your logger object, which doesn't exist. If you had access to the fs module in your test then you would spy on the mkdirSync method like this:
jest.spyOn(fs, 'mkdirSync');
However, I think you need to take a different approach.
Your createLogDir function is a static method - meaning that it can only be called on the class, and not on an instance of that class (new Logger() is an instance of the class Logger). Therefore, in order to test that function you need to export the class and not an instance of it, i.e.:
module.exports = Logger;
Then you could have the following tests:
const Logger = require('./logger');
const fs = require('fs');
jest.mock('fs') // this auto mocks all methods on fs - so you can treat fs.existsSync and fs.mkdirSync like you would jest.fn()
it('should create a new log directory if one doesn\'t already exist', () => {
// set up existsSync to meet the `if` condition
fs.existsSync.mockReturnValue(false);
// call the function that you want to test
Logger.createLogDir('test-path');
// make your assertion
expect(fs.mkdirSync).toHaveBeenCalled();
});
it('should NOT create a new log directory if one already exists', () => {
// set up existsSync to FAIL the `if` condition
fs.existsSync.mockReturnValue(true);
Logger.createLogDir('test-path');
expect(fs.mkdirSync).not.toHaveBeenCalled();
});
Note: it looks like you're mixing CommonJS and es6 module syntax (export default is es6) - I would try to stick to one or the other

Resources