How do I serve static files in NestJS using fastify? - nestjs

How does one serve static files in NestJS using fastify? I can't seem to find any recent examples of setting this up properly. I have my main.ts set up like this:
main.ts
// This must be the first thing imported in the app
import 'src/tracing';
import * as winston from 'winston';
import fastifyStatic, { FastifyStaticOptions } from '#fastify/static';
import { NestFactory } from '#nestjs/core';
import {
FastifyAdapter,
NestFastifyApplication,
} from '#nestjs/platform-fastify';
import { path } from 'app-root-path';
import { WinstonModule } from 'nest-winston';
import { doc } from 'prettier';
import { AppModule } from 'src/app.module';
import join = doc.builders.join;
async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter(),
{
logger: WinstonModule.createLogger({
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json(),
),
transports: [new winston.transports.Console()],
}),
rawBody: true,
},
);
await app.register(require('#fastify/static'), {
root: require('app-root-path').resolve('/client'),
prefix: '/client/', // optional: default '/'
});
// eslint-disable-next-line #typescript-eslint/ban-ts-comment
// #ts-ignore
app.get('/another/path', function (req, reply) {
reply.sendFile('index.html');
});
app.enableShutdownHooks(); // terminus needs this to listen for SIGTERM/SIGKILL
await app.listen(3002, '0.0.0.0');
console.log(`Application is running on: ${await app.getUrl()}`);
}
bootstrap();
The static file I'm attempting to serve is client/index.html.
However, when I run my app I get the following error: Nest could not find /another/path element (this provider does not exist in the current context).
I've also tried setting up my app.module.ts Modules like this:
app.module.ts
#Module({
imports: [
...configModules,
...domainModules,
...libraryModules,
ServeStaticModule.forRoot({
rootPath: require('app-root-path').resolve('/client'),
renderPath: '/client/*',
}),
],
controllers: [AppController],
providers: [AppService],
})
This leads to the following error:
/Users/ewu/Desktop/Projects/janus/node_modules/#nestjs/platform-fastify/node_modules/fastify/lib/route.js:286
throw new FST_ERR_DUPLICATED_ROUTE(opts.method, opts.url)
^
FastifyError: Method 'HEAD' already declared for route '/'
at Object.addNewRoute (/Users/ewu/Desktop/Projects/janus/node_modules/#nestjs/platform-fastify/node_modules/fastify/lib/route.js:286:19)
at Object.route (/Users/ewu/Desktop/Projects/janus/node_modules/#nestjs/platform-fastify/node_modules/fastify/lib/route.js:211:19)
at Object.prepareRoute (/Users/ewu/Desktop/Projects/janus/node_modules/#nestjs/platform-fastify/node_modules/fastify/lib/route.js:144:18)
at Object._head [as head] (/Users/ewu/Desktop/Projects/janus/node_modules/#nestjs/platform-fastify/node_modules/fastify/fastify.js:247:34)
at fastifyStatic (/Users/ewu/Desktop/Projects/janus/node_modules/#fastify/static/index.js:370:17)
Here are the relevant packages and their versions:
"#nestjs/serve-static": "^3.0.0",
"fastify-static": "^4.7.0",
"fastify": "^4.8.1",
"#nestjs/platform-fastify": "^9.1.2",
"#fastify/static": "^6.0.0",
I'm using version 9.0.0 of Nest and v16.15.0 of Node.

You most likely have a #Get() under a #Controller() (most likely your AppController) which is binding the GET / route already. Fastify won't let you bind two handlers to the same route. Because of this, you either need to change the #Get() to have some sort of route associated with it, change the ServeStaticModule to have a different served route, or use a global prefix to modify the rest of the server routes (I believe this leaves the server static module unaffected).

Related

Nestjs Jest Unit Test: Basic test failure - Jest configuration

I have a simple Jest test for my Nest JS project.
The Jest looks like:
import { Test, TestingModule } from '#nestjs/testing';
import { IbmVpcController } from './ibm.vpc.controller';
import { IbmVpcServiceMock } from './ibm.vpc.service.mock';
import { ModuleMocker, MockFunctionMetadata } from 'jest-mock';
import { MOCKED_VPC } from '../../repository/ibm/mock.vpc.data';
const moduleMocker = new ModuleMocker(global);
describe('IbmVpcController', () => {
let controller: IbmVpcController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [IbmVpcController],
providers: [IbmVpcServiceMock]
})
.useMocker((token) => {
if (token === IbmVpcServiceMock) {
return {
list: jest.fn().mockResolvedValue(MOCKED_VPC.VPCs),
get: jest.fn().mockResolvedValue(MOCKED_VPC.VPCs[0]),
create: jest.fn().mockResolvedValue(MOCKED_VPC.VPCs[0]),
update: jest.fn().mockResolvedValue(MOCKED_VPC.VPCs[0]),
};
}
if (typeof token === 'function') {
const mockMetadata = moduleMocker.getMetadata(token) as MockFunctionMetadata<any, any>;
const Mock = moduleMocker.generateFromMetadata(mockMetadata);
return new Mock();
}
})
.compile();
controller = module.get<IbmVpcController>(IbmVpcController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});
My jest.config.js looks like:
module.exports = {
verbose: true,
preset: "ts-jest",
testEnvironment: "node",
roots: ["./src"],
transform: { "\\.ts$": ["ts-jest"] },
testRegex: "(/__test__/.*|(\\.|/)(spec))\\.ts?$",
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
transformIgnorePatterns: [
'<rootDir>/node_modules/',
],
globals: {
"ts-jest": {
tsconfig: {
// allow js in typescript
allowJs: true,
},
},
},
};
However it is failing with the following error:
FAIL apps/protocols/src/ibm/vpc/ibm.vpc.controller.spec.ts
● Test suite failed to run
Jest encountered an unexpected token
This usually means that you are trying to import a file which Jest cannot parse, e.g. it's not plain JavaScript.
By default, if Jest sees a Babel config, it will use that to transform your files, ignoring "node_modules".
Here's what you can do:
• If you are trying to use ECMAScript Modules, see https://jestjs.io/docs/en/ecmascript-modules for how to enable it.
• To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in your config.
• If you need a custom transformation specify a "transform" option in your config.
• If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the "moduleNameMapper" config option.
You'll find more details and examples of these config options in the docs:
https://jestjs.io/docs/en/configuration.html
Details:
C:\Users\pradipm\clients\CloudManager\cm_6\occm\client-infra\nest-services\node_modules\axios\index.js:1
({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,global,jest){import axios from './lib/axios.js';
^^^^^^
SyntaxError: Cannot use import statement outside a module
at Runtime.createScriptFromCode (../../node_modules/jest-runtime/build/index.js:1350:14)
at Object.<anonymous> (../../node_modules/retry-axios/src/index.ts:124:1)
Now able to get it what I am missing in my typescript Nest's Jest configuration.
Basically I tried out some more options also:
I tried out specifying the transformIgnorePatterns as only '/node_modules/'.
Tried out excluding the lodash-es', 'axios'
Tried out transformIgnorePattens as '/lib/' (where axois is there)
Added allowJs: true in the tsconfig.app.json compileOptions.
Any help to get trough my first basic test would be helpful.
With axios version 1.1.2 there's a bug with jest. You can resolve it by adding moduleNameMapper: { '^axios$': require.resovle('axios') } to your jest configuration

Why does it throw an error cannot read property of undefined (reading 'length') when running a GraphQL Subscription with Redis?

Overview
Working on a NestJS project with GraphQL using a laptop with Window OS
Experimenting with GraphQL Subscriptions using graphql-redis-subscription#2.5.0 package
Redis is used in a docker container, see the docker-compose.yml below
The problem arose when the subscription postAdded is executed in GraphQL Playground. Instead of hanging to listen for events, it had crashed before I performed createPost mutation.
My code (I only include some important details)
posts.resolver.ts
import { Inject, UseGuards } from '#nestjs/common';
import { Args, Context, Mutation, Resolver, Subscription } from '#nestjs/graphql';
import { RedisPubSub } from 'graphql-redis-subscriptions';
import { GraphqlJwtAuthGuard } from '../auth/guards';
import { RequestWithUser } from '../auth/interfaces';
import { PUB_SUB } from '../pubsub/pubsub.module'
const POST_ADDED_EVENT = 'postAdded';
#Resolver(() => Post)
export class PostsResolver {
constructor(
private postsService: PostsService,
#Inject(PUB_SUB) private pubSub: RedisPubSub,
) {}
// my subscription (issue)
#Subscription(() => Post)
postAdded() {
return this.pubSub.asyncIterator(POST_ADDED_EVENT);
}
// createPost method
#Mutation(() => Post)
#UseGuards(GraphqlJwtAuthGuard)
async createPost(
#Args('input') createPostInput: CreatePostInput,
#Context() context: { req: RequestWithUser },
) {
// just create a new post (assuming it works)
const newPost = await this.postsService.create(
createPostInput,
context.req.user,
);
this.pubSub.publish(POST_ADDED_EVENT, { postAdded: newPost });
return newPost;
}
}
pubsub.module.ts
import { ConfigService } from '#nestjs/config';
import { RedisPubSub } from 'graphql-redis-subscriptions';
import { Global, Module } from '#nestjs/common';
export const PUB_SUB = 'PUB_SUB';
#Global()
#Module({
providers: [
{
provide: PUB_SUB,
useFactory: (configService: ConfigService) =>
new RedisPubSub({
connection: {
host: configService.get('REDIS_HOST'),
port: configService.get('REDIS_PORT'),
},
}),
inject: [ConfigService],
},
],
exports: [PUB_SUB],
})
export class PubSubModule {}
app.module.ts
import { PubSubModule } from './pubsub/pubsub.module';
#Module({
imports: [
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
playground: true,
autoSchemaFile: path.join(process.cwd(), 'src/schema.gql'),
installSubscriptionHandlers: true,
}),
PubSubModule,
ConfigModule.forRoot({
isGlobal: true,
validationSchema: Joi.object({
REDIS_HOST: Joi.string().required(),
REDIS_PORT: Joi.number().required()
}),
}),
],
providers: [AppService, AppResolver],
})
export class AppModule {}
version: '3'
services:
redis:
image: 'redis:alpine'
ports:
- '6379:6379'
redis-commander:
image: rediscommander/redis-commander:latest
environment:
- REDIS_HOSTS=local:redis:6379
ports:
- '8081:8081'
depends_on:
- redis
All the environment variables have already been defined in .env file.
REDIS_HOST="localhost"
REDIS_PORT=6379
When I run yarn start:dev and execute the subscription in GraphQL Playground
subscription {
postAdded {
id
title
paragraphs
}
}
it raises an error like this:
{
"errors": [
{
"message": "Cannot read properties of undefined (reading 'length')",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"postAdded"
]
}
]
}
The terminal that monitors NestJS also raises an error like this:
[Nest] 8080 - 07/21/2022, 9:30:24 AM ERROR [ExceptionsHandler] Cannot read properties of undefined (reading 'length')
TypeError: Cannot read properties of undefined (reading 'length')
at JavascriptRedisParser.execute (C:\Users\HP\nestjs-project\node_modules\redis-parser\lib\parser.js:530:38)
at Object.data (C:\Users\HP\nestjs-project\node_modules\ioredis\built\DataHandler.js:25:20)
at TransformOperationExecutor.transform (C:\Users\HP\nestjs-project\node_modules\src\TransformOperationExecutor.ts:207:39)
at TransformOperationExecutor.transform (C:\Users\HP\nestjs-project\node_modules\src\TransformOperationExecutor.ts:327:31)
at TransformOperationExecutor.transform (C:\Users\HP\nestjs-project\node_modules\src\TransformOperationExecutor.ts:327:31)
at TransformOperationExecutor.transform (C:\Users\HP\nestjs-project\node_modules\src\TransformOperationExecutor.ts:327:31)
at TransformOperationExecutor.transform (C:\Users\HP\nestjs-project\node_modules\src\TransformOperationExecutor.ts:327:31)
at TransformOperationExecutor.transform (C:\Users\HP\nestjs-project\node_modules\src\TransformOperationExecutor.ts:327:31)
at ClassTransformer.instanceToPlain (C:\Users\HP\nestjs-project\node_modules\src\ClassTransformer.ts:25:21)
at Object.classToPlain (C:\Users\HP\nestjs-project\node_modules\src\index.ts:23:27)
I have installed all the necessary dependencies like ioredis, graphql-redis-subscriptions and even graphql-subscriptions but the errors still exist. Redis also seems to be running properly.
I have tried reading the error logs but it did not occur in my source code and doing some research on StackOverFlow but none seems to have solved the problem.
Are you by any chance using a global ClassSerializerInterceptor?? Because I was running into the same problem just today and I solved by removing the interceptor. It happened because the subscription needs to receive an instance of AsyncIterable but the class serializer turns it into a plain object.
Apart from that I would recommend you change the GraphQl config, remove the installSubscriptionHandlers and change the config like this:
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
playground: true,
autoSchemaFile: path.join(process.cwd(), 'src/schema.gql'),
// remove this option:
// installSubscriptionHandlers: true,
// add the following:
subscriptions: {
"graphql-ws": true // or config object
}
}),
You can read more about it in the nestjs docs
I hope this solves your problem.

Nest can't resolve dependencies of the SequelizeCoreModule

I'm trying to import SequelizeModule in my app.module.ts but I got the following error:
[Nest] ERROR [ExceptionHandler] Nest can't resolve dependencies of the
SequelizeCoreModule (SequelizeModuleOptions, ?). Please make sure that
the argument ModuleRef at index [1] is available in the
SequelizeCoreModule context.
app.module.ts
import { Module } from '#nestjs/common';
import { ModuleRef } from '#nestjs/core';
import { SequelizeModule } from '#nestjs/sequelize';
import { join } from 'path';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TweetsController } from './tweets/tweets.controller';
import { TweetsModule } from './tweets/tweets.module';
import { TweetsService } from './tweets/tweets.service';
#Module({
imports: [
SequelizeModule.forRoot({
dialect: 'sqlite',
autoLoadModels: true,
synchronize: true,
host: join(__dirname, 'database.sqlite'),
}),
TweetsModule,
],
controllers: [AppController, TweetsController],
providers: [AppService, TweetsService],
})
export class AppModule {}
this happens when you have multiple nodejs modules loaded for the same #nestjs/core package. See them by running npm ls #nestjs/core. You can solve that by getting ride of those packages somehow and keeping only the one that your app depends on directly. Read the docs: https://docs.nestjs.com/faq/common-errors#cannot-resolve-dependency-error

Nestjs - connect bull-board in a normal controller

I created an app using nest.js and bull.
I added bull-board package to monitor my queues, but in documentation, only one way to add it to the app is mount as middleware:
In main.ts:
app.use('/admin/queues', bullUI);
Is there any way to add bullUI in a normal nest controller, after jwt auth? Like:
#UseGuards(JwtAuthGuard)
#Get("queues")
activate() {
return UI
}
You can use any express middleware like this inside controllers, but maybe some cases cause errors like serving static files with Guard exception and etc.
#UseGuards(JwtAuthGuard)
#Get("queues/*")
activate(#Req() req, #Res() res) {
bullUI(req, res)
}
I've got this working via a middleware consumer, so something like this:
import { router } from 'bull-board';
#Module({
imports: [
NestBullModule.forRoot({ redis }),
],
providers: [],
})
export class BullModule {
configure(consumer: MiddlewareConsumer): void {
consumer
.apply(router)
.forRoutes('/admin/queues');
}
}
I'd like to extend the original answer of #JCF, mainly, because it's working and much easier to understand.
I am using not default bull, with #nestjs/queue, but an improved version of BullMQ from anchan828 repo, with NestJS decorators, but I guess in both cases, the result will be the same.
The queue.module file:
#Module({
imports: [
BullModule.forRoot({
options: {
connection: {
host: redisConfig.host,
port: redisConfig.port,
},
},
}),
/** DI all your queues and Redis connection */
BullModule.registerQueue('yourQueueName'),
],
controllers: [],
providers: [],
})
export class QueueModule {
constructor (
#BullQueueInject('yourQueueName')
private readonly queueOne: Queue,
) {
/** Add queues with adapter, one-by-one */
setQueues([new BullMQAdapter(this.queueOne, { readOnlyMode: false })])
}
configure(consumer: MiddlewareConsumer): void {
consumer
.apply(router)
.forRoutes('/admin/queues');
}
}
Then just add it, to parent AppModule, via import, like that:
I am not sure, that Redis connection is needed here, for parent AppModule
#Module({
imports: [
BullModule.forRoot({
options: {
connection: {
host: redisConfig.host,
port: redisConfig.port,
},
},
}),
QueueModule
],
controllers: [],
providers: [],
})
export class AppModule {}
run the main.js, and visit, localhost:8000/admin/queues

Firebase Angular 4 Initialize based on node environment

I've built my Angular 4 project using the Angular CLI.
I am deploying my app on Heroku, I've created heroku pipelines for dev and production environment.
I have two firebase database dev and production and I want my angular 2 apps to connect to the firebase database based on heroku config variables
I searched on google and found this answer helpful as #yoni-rabinovitch suggested to send an HTTP request on node server to check for the environment when the app initializes.
I am a beginner in Angular 4 and typescript and all I need to implement is to send an HTTP request and initialize the firebase module based on the response.
app.module.ts
import { environment } from '../environments/environment';
#NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
HttpClientModule,
FormsModule,
ReactiveFormsModule,
BrowserAnimationsModule,
AngularFireModule.initializeApp(environment.firebase)
],
bootstrap: [AppComponent]
})
export class AppModule {}
Any help would be highly appreciated
I had to create an API on my server and set the function below in environments.ts file.
export let environment: any = {
production: false,
firebase: {...}
};
export const setEnvironment = async () => {
try {
const response = await fetch('api/checkEnvironment');
const json = await response.json();
if (json.data?.isPROD) {
// Prod Environment
environment = {
production: true,
firebase: {...}
};
}
} catch (error) {
throw error;
}
};
and call it from main.ts file
import {
environment,
setEnvironment,
} from './environments/environment';
import { enableProdMode } from '#angular/core';
import { platformBrowserDynamic } from '#angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { FIREBASE_OPTIONS } from '#angular/fire';
(async () => {
await setEnvironment();
if (environment.production) {
enableProdMode();
}
platformBrowserDynamic([
{
provide: FIREBASE_OPTIONS,
useValue: environment.firebase,
},
])
.bootstrapModule(AppModule)
.catch((err) => console.error(err));
})();
You can create a function to receive the configuration:
import { environment } from '../environments/environment';
#NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
HttpClientModule,
FormsModule,
ReactiveFormsModule,
BrowserAnimationsModule,
AngularFireModule.initializeApp(AppModule.getFirebaseConfig())
],
bootstrap: [AppComponent]
})
export class AppModule {
static getFirebaseConfig(): FirebaseAppConfig {
// do http request
// return correct correct configuration depending on http request
return environment.firebase;
}
}

Resources