I am having trouble creating an endpoint to serve the swagger documentation. The only way I can do it locally is if the path has a double slash at the end localhost:3003/dev/swagger//; If I omit one of the forward slashes, it returns a 404 for the path localhost:3003/swagger without /dev. Once deployed, API Gateway will return {"message": "Forbidden"} for either swagger endpoint (with or without //). How can I get the API Gateway /swagger endpoint to return the swagger UI?, I'm not sure if I have missed some steps.
Below are the main.ts for my NestJS application as well as the serverless.yml and here is a sample repo with minimum setup to duplicate my issue. https://github.com/MRdgz/serverless-nestj-swagger
main.ts
// main.ts
import { INestApplication } from '#nestjs/common';
import { NestFactory } from '#nestjs/core';
import { DocumentBuilder, SwaggerModule } from '#nestjs/swagger';
import { configure as serverlessExpress } from '#vendia/serverless-express';
import { Callback, Context, Handler } from 'aws-lambda';
import { AppModule } from './app.module';
let server: Handler;
function setupSwagger(nestApp: INestApplication): void {
const config = new DocumentBuilder()
.setTitle('Sample API')
.setDescription('Sample API Documentation')
.setVersion('0.0.1')
.addServer('/dev')
.build();
const document = SwaggerModule.createDocument(nestApp, config);
SwaggerModule.setup('/swagger', nestApp, document, {
customSiteTitle: 'Sample',
swaggerOptions: {
docExpansion: 'none',
operationSorter: 'alpha',
tagSorter: 'alpha',
},
});
}
async function bootstrap(): Promise<Handler> {
const app = await NestFactory.create(AppModule);
setupSwagger(app);
await app.init();
const expressApp = app.getHttpAdapter().getInstance();
return serverlessExpress({ app: expressApp });
}
export const handler: Handler = async (
event: any,
context: Context,
callback: Callback,
) => {
event.path = `${event.path}/`;
event.path = event.path.includes('swagger-ui')
? `swagger${event.path}`
: event.path;
server = server ?? (await bootstrap());
return server(event, context, callback);
};
severless.yml
service: sample-api
variablesResolutionMode: 20210326
useDotenv: true
plugins:
- serverless-offline
- serverless-plugin-optimize
# functions will inherit settings from provider properties if available,
provider:
name: aws
runtime: nodejs14.x
lambdaHashingVersion: 20201221
# memorySize: 1024 # default 1024 MB
timeout: 30 # default 6 seconds
# sls deploy --stage {stage} otherwise defaults to dev
stage: ${opt:stage, 'dev'}
functions:
main:
handler: dist/main.handler
name: ${opt:stage, 'dev'}-${self:service}
events:
- http:
method: ANY
path: /{proxy+}
cors: true
custom:
serverless-offline:
httpPort: 3003
optimize:
external: ['swagger-ui-dist']
You can check this GitHub issue, this may give you some ideas, and this is how I end up with:
const document = SwaggerModule.createDocument(app, config);
const forwardedPrefixSwagger = async (
req: any,
_: Response,
next: NextFunction,
) => {
req.originalUrl = (req.headers['x-forwarded-prefix'] || '') + req.url;
next();
};
app.use(
'/api/',
forwardedPrefixSwagger,
swaggerUi.serve,
swaggerUi.setup(document),
);
I think the issue is described here serverless-http/issues/86
What helped in my case
(please note:
it is still a kind of workaround;
I have checked it locally only npm run sls:offline;
It is a simplified example)
serverless.yaml
My custom section:
plugins:
- serverless-plugin-typescript
- serverless-plugin-optimize
- serverless-offline
[...]
custom:
serverless-offline:
noPrependStageInUrl: true <----- this guy!
optimize:
external: ['swagger-ui-dist']
[...]
functions:
main:
handler: src/lambda.handler
events:
- http:
method: ANY
path: /
- http:
method: ANY
path: '/{proxy+}'
lambda.ts
import { AppModule } from './app.module';
import { Callback, Context, Handler } from 'aws-lambda';
import { NestFactory } from '#nestjs/core';
import { DocumentBuilder, SwaggerModule } from '#nestjs/swagger';
import serverlessExpress from '#vendia/serverless-express';
import { INestApplication } from '#nestjs/common';
let cachedServer: Handler;
async function bootstrap(): Promise<Handler> {
const app = await NestFactory.create(AppModule);
setupSwagger(app);
await app.init();
const expressApp = app.getHttpAdapter().getInstance();
return serverlessExpress({ app: expressApp });
}
export const handler: Handler = async (
event: any,
context: Context,
callback: Callback,
) => {
cachedServer = cachedServer ?? (await bootstrap());
return cachedServer(event, context, callback);
};
function setupSwagger(app: INestApplication) {
const options = new DocumentBuilder()
.setTitle('My API')
.setDescription('My application API')
.setVersion('1.0.0')
.addTag('#tag')
.build();
const document = SwaggerModule.createDocument(app, options);
SwaggerModule.setup('api/swagger', app, document);
}
The result is that http://localhost:3000/api/swagger returns the api page,
http://localhost:3000/api is handled by a default
#Controller('api')
#Get()
Related
I'm using fastify-cli for building my server application.
For testing I want to generate some test JWTs. Therefore I want to use the sign method of the fastify-jwt plugin.
If I run the application with fastify start -l info ./src/app.js everything works as expected and I can access the decorators.
But in the testing setup I get an error that the jwt decorator is undefined. It seems that the decorators are not exposed and I just can't find any error. For the tests I use node-tap with this command: tap \"test/**/*.test.js\" --reporter=list
app.js
import { dirname, join } from 'path'
import autoload from '#fastify/autoload'
import { fileURLToPath } from 'url'
import jwt from '#fastify/jwt'
export const options = {
ignoreTrailingSlash: true,
logger: true
}
export default async (fastify, opts) => {
await fastify.register(jwt, {
secret: process.env.JWT_SECRET
})
// autoload plugins and routes
await fastify.register(autoload, {
dir: join(dirname(fileURLToPath(import.meta.url)), 'plugins'),
options: Object.assign({}, opts),
forceESM: true,
})
await fastify.register(autoload, {
dir: join(dirname(fileURLToPath(import.meta.url)), 'routes'),
options: Object.assign({}, opts),
forceESM: true
})
}
helper.js
import { fileURLToPath } from 'url'
import helper from 'fastify-cli/helper.js'
import path from 'path'
// config for testing
export const config = () => {
return {}
}
export const build = async (t) => {
const argv = [
path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'src', 'app.js')
]
const app = await helper.build(argv, config())
t.teardown(app.close.bind(app))
return app
}
root.test.js
import { auth, build } from '../helper.js'
import { test } from 'tap'
test('requests the "/" route', async t => {
t.plan(1)
const app = await build(t)
const token = app.jwt.sign({ ... }) //-> jwt is undefined
const res = await app.inject({
method: 'GET',
url: '/'
})
t.equal(res.statusCode, 200, 'returns a status code of 200')
})
The issue is that your application diagram looks like this:
and when you write const app = await build(t) the app variable points to Root Context, but Your app.js contains the jwt decorator.
To solve it, you need just to wrap you app.js file with the fastify-plugin because it breaks the encapsulation:
import fp from 'fastify-plugin'
export default fp(async (fastify, opts) => { ... })
Note: you can visualize this structure by using fastify-overview (and the fastify-overview-ui plugin together:
Testing libs...always fun. I am using next-i18next within my NextJS project. We are using the useTranslation hook with namespaces.
When I run my test there is a warning:
console.warn
react-i18next:: You will need to pass in an i18next instance by using initReactI18next
> 33 | const { t } = useTranslation(['common', 'account']);
| ^
I have tried the setup from the react-i18next test examples without success. I have tried this suggestion too.
as well as just trying to mock useTranslation without success.
Is there a more straightforward solution to avoid this warning? The test passes FWIW...
test('feature displays error', async () => {
const { findByTestId, findByRole } = render(
<I18nextProvider i18n={i18n}>
<InviteCollectEmails onSubmit={jest.fn()} />
</I18nextProvider>,
{
query: {
orgId: 666,
},
}
);
const submitBtn = await findByRole('button', {
name: 'account:organization.invite.copyLink',
});
fireEvent.click(submitBtn);
await findByTestId('loader');
const alert = await findByRole('alert');
within(alert).getByText('failed attempt');
});
Last, is there a way to have the translated plain text be the outcome, instead of the namespaced: account:account:organization.invite.copyLink?
Use the following snippet before the describe block OR in beforeEach() to mock the needful.
jest.mock("react-i18next", () => ({
useTranslation: () => ({ t: key => key }),
}));
Hope this helps. Peace.
use this for replace render function.
import { render, screen } from '#testing-library/react'
import DarkModeToggleBtn from '../../components/layout/DarkModeToggleBtn'
import { appWithTranslation } from 'next-i18next'
import { NextRouter } from 'next/router'
jest.mock('react-i18next', () => ({
I18nextProvider: jest.fn(),
__esmodule: true,
}))
const createProps = (locale = 'en', router: Partial<NextRouter> = {}) => ({
pageProps: {
_nextI18Next: {
initialLocale: locale,
userConfig: {
i18n: {
defaultLocale: 'en',
locales: ['en', 'fr'],
},
},
},
} as any,
router: {
locale: locale,
route: '/',
...router,
},
} as any)
const Component = appWithTranslation(() => <DarkModeToggleBtn />)
const defaultRenderProps = createProps()
const renderComponent = (props = defaultRenderProps) => render(
<Component {...props} />
)
describe('', () => {
it('', () => {
renderComponent()
expect(screen.getByRole("button")).toHaveTextContent("")
})
})
I used a little bit more sophisticated approach than mocking to ensure all the functions work the same both in testing and production environment.
First, I create a testing environment:
// testing/env.ts
import i18next, { i18n } from "i18next";
import JSDomEnvironment from "jest-environment-jsdom";
import { initReactI18next } from "react-i18next";
declare global {
var i18nInstance: i18n;
}
export default class extends JSDomEnvironment {
async setup() {
await super.setup();
/* The important part start */
const i18nInstance = i18next.createInstance();
await i18nInstance.use(initReactI18next).init({
lng: "cimode",
resources: {},
});
this.global.i18nInstance = i18nInstance;
/* The important part end */
}
}
I add this environment in jest.config.ts:
// jest.config.ts
export default {
// ...
testEnvironment: "testing/env.ts",
};
Sample component:
// component.tsx
import { useTranslation } from "next-i18next";
export const Component = () => {
const { t } = useTranslation();
return <div>{t('foo')}</div>
}
And later on I use it in tests:
// component.test.tsx
import { setI18n } from "react-i18next";
import { create, act, ReactTestRenderer } from "react-test-renderer";
import { Component } from "./component";
it("renders Component", () => {
/* The important part start */
setI18n(global.i18nInstance);
/* The important part end */
let root: ReactTestRenderer;
act(() => {
root = create(<Component />);
});
expect(root.toJSON()).toMatchSnapshot();
});
I figured out how to make the tests work with an instance of i18next using the renderHook function and the useTranslation hook from react-i18next based on the previous answers and some research.
This is the Home component I wanted to test:
import { useTranslation } from 'next-i18next';
const Home = () => {
const { t } = useTranslation("");
return (
<main>
<div>
<h1> {t("welcome", {ns: 'home'})}</h1>
</div>
</main>
)
};
export default Home;
First, we need to create a setup file for jest so we can start an i18n instance and import the translations to the configuration. test/setup.ts
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import homeES from '#/public/locales/es/home.json';
import homeEN from '#/public/locales/en/home.json';
i18n.use(initReactI18next).init({
lng: "es",
resources: {
en: {
home: homeEN,
},
es: {
home: homeES,
}
},
fallbackLng: "es",
debug: false,
});
export default i18n;
Then we add the setup file to our jest.config.js:
setupFilesAfterEnv: ["<rootDir>/test/setup.ts"]
Now we can try our tests using the I18nextProvider and the useTranslation hook:
import '#testing-library/jest-dom/extend-expect';
import { cleanup, render, renderHook } from '#testing-library/react';
import { act } from 'react-dom/test-utils';
import { I18nextProvider, useTranslation } from 'react-i18next';
import Home from '.';
describe("Index page", (): void => {
afterEach(cleanup);
it("should render properly in Spanish", (): void => {
const t = renderHook(() => useTranslation());
const component = render(
<I18nextProvider i18n={t.result.current.i18n}>
<Home / >
</I18nextProvider>
);
expect(component.getByText("Bienvenido a Pocky")).toBeInTheDocument();
});
it("should render properly in English", (): void => {
const t = renderHook(() => useTranslation());
act(() => {
t.result.current.i18n.changeLanguage("en");
});
const component = render(
<I18nextProvider i18n={t.result.current.i18n}>
<Home/>
</I18nextProvider>
);
expect(component.getByText("Welcome to Pocky")).toBeInTheDocument();
});
});
Here we used the I18nextProvider and send the i18n instance using the useTranslation hook. after that the translations were loaded without problems in the Home component.
We can also change the selected language running the changeLanguage() function and test the other translations.
My e2e test is returning TypeError: metadata_1.Public is not a function for a controller that is using the custom decorator #Public()
Some code is omitted for clarity
it(`/GET forks`, async () => {
const fork: ForksModel = {
type: 'Full Copy',
};
await request(app.getHttpServer())
.get('/forks')
.expect(200)
.expect({ fork: expectedForks});
});
#Public()
public async getAccountForks(#Req() req: Request) {
const { account } = req;
const fork = await this.service.getAccountForks(account);
return { fork, account };
}
public.decorator.ts
import { SetMetadata } from "#nestjs/common";
export const Public = () => SetMetadata( "isPublic", true );
I don't know what is happening here, it doesn't complain this when running nest
This is imported
import { Public } from '#app/utils/metadata';
So i just forgot to export my metadata files from the root utils index.ts!
But Nest didn't complain and the decorator was functional on my Guard when testing!
I'm hosting a Nestjs application on AWS Lambda (using the Serverless Framework).
Please note that the implementation is behind AWS API Gateway.
Question: How can I access to event parameter in my Nest controller?
This is how I bootstrap the NestJS server:
import { APIGatewayProxyHandler } from 'aws-lambda';
import { NestFactory } from '#nestjs/core';
import { AppModule } from './app.module';
import { Server } from 'http';
import { ExpressAdapter } from '#nestjs/platform-express';
import * as awsServerlessExpress from 'aws-serverless-express';
import * as express from 'express';
let cachedServer: Server;
const bootstrapServer = async (): Promise<Server> => {
const expressApp = express();
const adapter = new ExpressAdapter(expressApp);
const app = await NestFactory.create(AppModule, adapter);
app.enableCors();
await app.init();
return awsServerlessExpress.createServer(expressApp);
}
export const handler: APIGatewayProxyHandler = async (event, context) => {
if (!cachedServer) {
cachedServer = await bootstrapServer()
}
return awsServerlessExpress.proxy(cachedServer, event, context, 'PROMISE')
.promise;
};
Here is a function in one controller:
#Get()
getUsers(event) { // <-- HOW TO ACCESS event HERE?? This event is undefined.
return {
statusCode: 200,
body: "This function works and returns this JSON as expected."
}
I'm struggling to understand how I can access the event paramenter, which is easily accessible in a "normal" node 12.x Lambda function:
module.exports.hello = async (event) => {
return {
statusCode: 200,
body: 'In a normal Lambda, the event is easily accessible, but in NestJS its (apparently) not.'
};
};
Solution:
Add AwsExpressServerlessMiddleware to your setup during bootstrap:
const awsServerlessExpressMiddleware = require('aws-serverless-express/middleware')
app.use(awsServerlessExpressMiddleware.eventContext())
Note: The app.use should be before app.init()
Now the event and context object can be accessed:
var event = req.apiGateway.event;
var context = req.apiGateway.context;
Credits: This answer on SO
Looking at Netsjs documentation I can see that the general approach is controller level caching utilizing CacheInterceptor,
What I am looking to achieve is Service/DB level caching - use case is mainly for Static DB data that is required by other services, Is there a way to extend the supplied Cache Module to be used from within services ?
I was also looking for a way to do this.
For now I made it work based on some research by injecting the cacheManager instance in the service, and using it directly.
import { CACHE_MANAGER, Inject, Injectable } from '#nestjs/common';
#Injectable()
export class MyService {
constructor(
#Inject(CACHE_MANAGER) protected readonly cacheManager
) {}
public async myMethod() {
const cachedData = await this.cacheManager.get(cacheKey);
if (cachedData) return cachedData;
const data = ''; // Do normal stuff here
this.cacheManager.set(cacheKey, data);
return data;
}
}
But I'm still looking for a way to create a caching decorator that would contain the cache logic, and would use the function arguments as cache key
This is how I do it (some hook):
import {CACHE_MANAGER, CacheModule, Inject, Module} from '#nestjs/common';
import {Cache} from 'cache-manager';
let cache: Cache;
#Module({
imports: [CacheModule.register({ttl: 60 * 60})]
})
export class CacheableModule {
constructor(#Inject(CACHE_MANAGER) private cacheManager: Cache) {
cache = cacheManager;
}
}
export const Cacheable = (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
const originalMethod = descriptor.value;
descriptor.value = async function (...args: any[]) {
const cacheKey = originalMethod.name + "_" + args.join('_')
const cachedData = await cache.get(cacheKey);
if (cachedData) {
originalMethod.apply(this, args).then((data: any) => {
cache.set(cacheKey, data);
})
return cachedData;
}
const result = await originalMethod.apply(this, args);
cache.set(cacheKey, result);
return result;
};
};
and use it
#Cacheable
async getProducts(projectName: string): Promise<{ [id: string]: Product }> {}
You can use one of the cache stores listed here: https://github.com/BryanDonovan/node-cache-manager#store-engines
Then you simply register that store in the cache module (https://docs.nestjs.com/techniques/caching#different-stores):
import * as redisStore from 'cache-manager-redis-store';
import { CacheModule, Module } from '#nestjs/common';
import { AppController } from './app.controller';
#Module({
imports: [
CacheModule.register({
store: redisStore,
host: 'localhost',
port: 6379,
}),
],
controllers: [AppController],
})
export class ApplicationModule {}