feat: add nestjs fastify backend scaffold

This commit is contained in:
Tiago Yamamoto 2025-12-17 14:50:19 -03:00
parent 470f8463b1
commit d1ff1eaa1c
32 changed files with 4345 additions and 0 deletions

6
backend-nest/.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
node_modules
.env
coverage
dist
.DS_Store
.prisma

22
backend-nest/Dockerfile Normal file
View file

@ -0,0 +1,22 @@
FROM node:iron-slim AS base
RUN corepack enable
WORKDIR /app
FROM base AS deps
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
FROM deps AS build
COPY . .
RUN pnpm prisma:generate
RUN pnpm build
FROM base AS production
ENV NODE_ENV=production
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --prod --frozen-lockfile
COPY --from=build /app/dist ./dist
COPY --from=build /app/prisma ./prisma
EXPOSE 3000
CMD ["node", "dist/main.js"]

View file

@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}

51
backend-nest/package.json Normal file
View file

@ -0,0 +1,51 @@
{
"name": "backend-nest",
"version": "0.1.0",
"description": "NestJS backend using Fastify, Prisma, and JWT auth",
"packageManager": "pnpm@8.15.6",
"private": true,
"scripts": {
"start": "node dist/main.js",
"start:dev": "ts-node-dev --respawn --transpile-only src/main.ts",
"start:debug": "ts-node-dev --respawn --inspect --transpile-only src/main.ts",
"build": "tsc -p tsconfig.build.json",
"format": "prettier --write \"src/**/*.ts\"",
"lint": "eslint . --ext .ts",
"prisma:generate": "prisma generate"
},
"dependencies": {
"@fastify/cookie": "^9.3.1",
"@fastify/swagger": "^8.12.0",
"@fastify/swagger-ui": "^1.9.2",
"@nestjs/common": "^10.3.6",
"@nestjs/config": "^3.2.2",
"@nestjs/core": "^10.3.6",
"@nestjs/jwt": "^10.2.0",
"@nestjs/passport": "^10.0.3",
"@nestjs/platform-fastify": "^10.3.6",
"@nestjs/swagger": "^7.4.2",
"@prisma/client": "^5.16.2",
"bcrypt": "^5.1.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"fastify": "^4.28.1",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"prisma": "^5.16.2",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1"
},
"devDependencies": {
"@types/bcrypt": "^5.0.2",
"@types/node": "^20.14.9",
"@types/passport-jwt": "^4.0.1",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-prettier": "^5.1.3",
"prettier": "^3.3.2",
"ts-node": "^10.9.2",
"ts-node-dev": "^2.0.0",
"typescript": "^5.4.5"
}
}

3549
backend-nest/pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,72 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
enum UserRole {
USER
ADMIN
}
enum CompanyStatus {
ACTIVE
INACTIVE
SUSPENDED
}
model Company {
id Int @id @default(autoincrement())
name String
status CompanyStatus @default(ACTIVE)
users User[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model User {
id Int @id @default(autoincrement())
email String @unique
password String
name String
role UserRole @default(USER)
companyId Int
company Company @relation(fields: [companyId], references: [id])
refreshToken String?
orders Order[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Product {
id Int @id @default(autoincrement())
name String
sku String @unique
price Decimal @db.Decimal(10, 2)
inventory InventoryItem?
orders Order[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model InventoryItem {
id Int @id @default(autoincrement())
productId Int @unique
product Product @relation(fields: [productId], references: [id])
quantity Int
updatedAt DateTime @updatedAt
}
model Order {
id Int @id @default(autoincrement())
buyerId Int
productId Int
quantity Int
total Decimal @db.Decimal(12, 2)
buyer User @relation(fields: [buyerId], references: [id])
product Product @relation(fields: [productId], references: [id])
createdAt DateTime @default(now())
}

View file

@ -0,0 +1,19 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AuthModule } from './auth/auth.module';
import { InventoryModule } from './inventory/inventory.module';
import { PrismaModule } from './prisma/prisma.module';
import { UsersModule } from './users/users.module';
import { WebhooksModule } from './webhooks/webhooks.module';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
PrismaModule,
AuthModule,
UsersModule,
InventoryModule,
WebhooksModule,
],
})
export class AppModule {}

View file

@ -0,0 +1,48 @@
import { Body, Controller, Get, HttpCode, HttpStatus, Post, Req, Res, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { FastifyReply } from 'fastify';
import { CreateUserDto } from '../users/dto/create-user.dto';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
import { RefreshTokenGuard } from './guards/refresh-token.guard';
import { AuthService } from './auth.service';
import { LoginDto } from './dto/login.dto';
@ApiTags('auth')
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('register')
async register(@Body() dto: CreateUserDto, @Res({ passthrough: true }) reply: FastifyReply) {
return this.authService.register(dto, reply);
}
@Post('login')
@HttpCode(HttpStatus.OK)
async login(@Body() dto: LoginDto, @Res({ passthrough: true }) reply: FastifyReply) {
return this.authService.login(dto, reply);
}
@Post('refresh')
@UseGuards(RefreshTokenGuard)
@HttpCode(HttpStatus.OK)
async refresh(@Req() req: any, @Res({ passthrough: true }) reply: FastifyReply) {
const userId = req.user?.sub;
const refreshToken = req.user?.refreshToken;
return this.authService.refreshTokens(userId, refreshToken, reply);
}
@Post('logout')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
async logout(@Req() req: any, @Res({ passthrough: true }) reply: FastifyReply) {
return this.authService.logout(req.user.sub, reply);
}
@Get('profile')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
async profile(@Req() req: any) {
return req.user;
}
}

View file

@ -0,0 +1,33 @@
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { PrismaModule } from '../prisma/prisma.module';
import { UsersModule } from '../users/users.module';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
import { RefreshTokenGuard } from './guards/refresh-token.guard';
import { JwtStrategy } from './strategies/jwt.strategy';
import { RefreshTokenStrategy } from './strategies/refresh-token.strategy';
@Module({
imports: [
ConfigModule,
PassportModule,
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (configService: ConfigService) => ({
secret: configService.get<string>('JWT_ACCESS_SECRET') || 'access-secret',
signOptions: { expiresIn: configService.get<string>('JWT_ACCESS_EXPIRES', '15m') },
}),
}),
PrismaModule,
UsersModule,
],
providers: [AuthService, JwtStrategy, RefreshTokenStrategy, JwtAuthGuard, RefreshTokenGuard],
controllers: [AuthController],
exports: [JwtAuthGuard, RefreshTokenGuard, AuthService],
})
export class AuthModule {}

View file

@ -0,0 +1,129 @@
import { BadRequestException, Injectable, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import { FastifyReply } from 'fastify';
import * as bcrypt from 'bcrypt';
import { PrismaService } from '../prisma/prisma.service';
import { UsersService } from '../users/users.service';
import { CreateUserDto } from '../users/dto/create-user.dto';
import { JwtPayload } from './types/jwt-payload.type';
import { LoginDto } from './dto/login.dto';
const REFRESH_COOKIE = 'refresh_token';
@Injectable()
export class AuthService {
constructor(
private readonly prisma: PrismaService,
private readonly jwtService: JwtService,
private readonly configService: ConfigService,
private readonly usersService: UsersService,
) {}
async register(dto: CreateUserDto, reply: FastifyReply) {
const existingUser = await this.usersService.findByEmail(dto.email);
if (existingUser) {
throw new BadRequestException('Email already registered');
}
const hashedPassword = await bcrypt.hash(dto.password, 10);
const user = await this.usersService.createWithCompany(dto, hashedPassword);
const tokens = await this.issueTokens(user);
await this.updateRefreshToken(user.id, tokens.refreshToken);
this.attachRefreshTokenCookie(reply, tokens.refreshToken);
return { accessToken: tokens.accessToken };
}
async login(dto: LoginDto, reply: FastifyReply) {
const user = await this.usersService.findByEmailWithCompany(dto.email);
if (!user) {
throw new UnauthorizedException('Invalid credentials');
}
const passwordMatches = await bcrypt.compare(dto.password, user.password);
if (!passwordMatches) {
throw new UnauthorizedException('Invalid credentials');
}
const tokens = await this.issueTokens(user);
await this.updateRefreshToken(user.id, tokens.refreshToken);
this.attachRefreshTokenCookie(reply, tokens.refreshToken);
return { accessToken: tokens.accessToken };
}
async refreshTokens(userId: number, refreshToken: string, reply: FastifyReply) {
const user = await this.prisma.user.findUnique({
where: { id: userId },
include: { company: true },
});
if (!user || !user.refreshToken) {
throw new UnauthorizedException('Refresh token not found');
}
const refreshMatches = await bcrypt.compare(refreshToken, user.refreshToken);
if (!refreshMatches) {
throw new UnauthorizedException('Refresh token invalid');
}
const tokens = await this.issueTokens(user);
await this.updateRefreshToken(user.id, tokens.refreshToken);
this.attachRefreshTokenCookie(reply, tokens.refreshToken);
return { accessToken: tokens.accessToken };
}
async logout(userId: number, reply: FastifyReply) {
await this.prisma.user.update({
where: { id: userId },
data: { refreshToken: null },
});
reply.clearCookie(REFRESH_COOKIE, { path: '/' });
return { success: true };
}
private async issueTokens(user: any) {
const payload: JwtPayload = {
sub: user.id,
email: user.email,
companyId: user.companyId,
companyStatus: user.company?.status,
role: user.role,
};
const [accessToken, refreshToken] = await Promise.all([
this.jwtService.signAsync(payload, {
secret: this.configService.get<string>('JWT_ACCESS_SECRET') || 'access-secret',
expiresIn: this.configService.get<string>('JWT_ACCESS_EXPIRES', '15m'),
}),
this.jwtService.signAsync(payload, {
secret: this.configService.get<string>('JWT_REFRESH_SECRET') || 'refresh-secret',
expiresIn: this.configService.get<string>('JWT_REFRESH_EXPIRES', '7d'),
}),
]);
return { accessToken, refreshToken };
}
private async updateRefreshToken(userId: number, refreshToken: string) {
const hashedRefresh = await bcrypt.hash(refreshToken, 10);
await this.prisma.user.update({
where: { id: userId },
data: { refreshToken: hashedRefresh },
});
}
attachRefreshTokenCookie(reply: FastifyReply, token: string) {
reply.setCookie(REFRESH_COOKIE, token, {
httpOnly: true,
sameSite: 'lax',
secure: this.configService.get<string>('NODE_ENV') === 'production',
path: '/',
maxAge: 60 * 60 * 24 * 7, // 7 days
});
}
}

View file

@ -0,0 +1,10 @@
import { IsEmail, IsString, MinLength } from 'class-validator';
export class LoginDto {
@IsEmail()
email!: string;
@IsString()
@MinLength(8)
password!: string;
}

View file

@ -0,0 +1,5 @@
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}

View file

@ -0,0 +1,5 @@
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class RefreshTokenGuard extends AuthGuard('jwt-refresh') {}

View file

@ -0,0 +1,20 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { JwtPayload } from '../types/jwt-payload.type';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get<string>('JWT_ACCESS_SECRET') || 'access-secret',
});
}
async validate(payload: JwtPayload) {
return payload;
}
}

View file

@ -0,0 +1,25 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { FastifyRequest } from 'fastify';
import { JwtPayload } from '../types/jwt-payload.type';
@Injectable()
export class RefreshTokenStrategy extends PassportStrategy(Strategy, 'jwt-refresh') {
constructor(configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromExtractors([
(request: FastifyRequest) => request?.cookies?.['refresh_token'] || null,
]),
secretOrKey: configService.get<string>('JWT_REFRESH_SECRET') || 'refresh-secret',
passReqToCallback: true,
ignoreExpiration: false,
});
}
async validate(request: FastifyRequest, payload: JwtPayload) {
const refreshToken = request.cookies?.['refresh_token'];
return { ...payload, refreshToken };
}
}

View file

@ -0,0 +1,7 @@
export type JwtPayload = {
sub: number;
email: string;
companyId: number;
companyStatus?: 'ACTIVE' | 'INACTIVE' | 'SUSPENDED';
role: 'USER' | 'ADMIN';
};

View file

@ -0,0 +1,27 @@
import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from '@nestjs/common';
import { PrismaService } from '../../prisma/prisma.service';
@Injectable()
export class ActiveCompanyGuard implements CanActivate {
constructor(private readonly prisma: PrismaService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const user = request.user;
if (!user?.sub) {
throw new ForbiddenException('User context missing');
}
const dbUser = await this.prisma.user.findUnique({
where: { id: user.sub },
include: { company: true },
});
if (!dbUser?.company || dbUser.company.status !== 'ACTIVE') {
throw new ForbiddenException('Company must be active to complete this action');
}
return true;
}
}

View file

@ -0,0 +1,11 @@
import { IsInt, IsPositive } from 'class-validator';
export class PurchaseDto {
@IsInt()
@IsPositive()
productId!: number;
@IsInt()
@IsPositive()
quantity!: number;
}

View file

@ -0,0 +1,24 @@
import { Body, Controller, Get, Post, Req, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { ActiveCompanyGuard } from '../common/guards/active-company.guard';
import { PurchaseDto } from './dto/purchase.dto';
import { InventoryService } from './inventory.service';
@ApiTags('inventory')
@Controller('inventory')
export class InventoryController {
constructor(private readonly inventoryService: InventoryService) {}
@Get()
async list() {
return this.inventoryService.listProducts();
}
@Post('purchase')
@UseGuards(JwtAuthGuard, ActiveCompanyGuard)
@ApiBearerAuth()
async purchase(@Body() dto: PurchaseDto, @Req() req: any) {
return this.inventoryService.purchaseProduct(req.user.sub, dto);
}
}

View file

@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { PrismaModule } from '../prisma/prisma.module';
import { ActiveCompanyGuard } from '../common/guards/active-company.guard';
import { InventoryController } from './inventory.controller';
import { InventoryService } from './inventory.service';
@Module({
imports: [PrismaModule],
controllers: [InventoryController],
providers: [InventoryService, ActiveCompanyGuard],
})
export class InventoryModule {}

View file

@ -0,0 +1,44 @@
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { PurchaseDto } from './dto/purchase.dto';
@Injectable()
export class InventoryService {
constructor(private readonly prisma: PrismaService) {}
listProducts() {
return this.prisma.product.findMany({ include: { inventory: true } });
}
async purchaseProduct(userId: number, dto: PurchaseDto) {
const product = await this.prisma.product.findUnique({
where: { id: dto.productId },
include: { inventory: true },
});
if (!product) {
throw new NotFoundException('Product not found');
}
if (!product.inventory || product.inventory.quantity < dto.quantity) {
throw new BadRequestException('Insufficient stock');
}
await this.prisma.$transaction([
this.prisma.inventoryItem.update({
where: { productId: dto.productId },
data: { quantity: { decrement: dto.quantity } },
}),
this.prisma.order.create({
data: {
buyerId: userId,
productId: dto.productId,
quantity: dto.quantity,
total: product.price.mul(dto.quantity),
},
}),
]);
return { message: 'Purchase created successfully' };
}
}

51
backend-nest/src/main.ts Normal file
View file

@ -0,0 +1,51 @@
import { ValidationPipe } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { NestFactory } from '@nestjs/core';
import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import fastifyCookie, { FastifyCookieOptions } from '@fastify/cookie';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter(),
);
const configService = app.get(ConfigService);
const cookieOptions: FastifyCookieOptions = {
parseOptions: {
sameSite: 'lax',
},
};
await app.register(fastifyCookie as any, cookieOptions);
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
transform: true,
forbidNonWhitelisted: true,
}),
);
const swaggerConfig = new DocumentBuilder()
.setTitle('SaveInMed Backend')
.setDescription('API Gateway for users, inventory and payments')
.setVersion('1.0')
.addBearerAuth({
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
description: 'Access token',
})
.build();
const document = SwaggerModule.createDocument(app, swaggerConfig);
SwaggerModule.setup('docs', app, document);
const port = configService.get<number>('PORT') || 3000;
await app.listen(port, '0.0.0.0');
}
bootstrap();

View file

@ -0,0 +1,9 @@
import { Global, Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';
@Global()
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}

View file

@ -0,0 +1,13 @@
import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
async onModuleInit() {
await this.$connect();
}
async onModuleDestroy() {
await this.$disconnect();
}
}

View file

@ -0,0 +1,18 @@
import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator';
export class CreateUserDto {
@IsNotEmpty()
@IsString()
name!: string;
@IsEmail()
email!: string;
@IsString()
@MinLength(8)
password!: string;
@IsString()
@IsNotEmpty()
companyName!: string;
}

View file

@ -0,0 +1,17 @@
import { Controller, Get, Req, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { UsersService } from './users.service';
@ApiTags('users')
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get('me')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
async me(@Req() req: any) {
return this.usersService.getSafeUser(req.user.sub);
}
}

View file

@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { PrismaModule } from '../prisma/prisma.module';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
@Module({
imports: [PrismaModule],
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}

View file

@ -0,0 +1,49 @@
import { Injectable } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import { CreateUserDto } from './dto/create-user.dto';
@Injectable()
export class UsersService {
constructor(private readonly prisma: PrismaService) {}
async createWithCompany(dto: CreateUserDto, hashedPassword: string) {
return this.prisma.$transaction(async (tx: Prisma.TransactionClient) => {
const company = await tx.company.create({
data: {
name: dto.companyName,
},
});
return tx.user.create({
data: {
name: dto.name,
email: dto.email,
password: hashedPassword,
companyId: company.id,
},
include: { company: true },
});
});
}
async findByEmail(email: string) {
return this.prisma.user.findUnique({ where: { email } });
}
async findByEmailWithCompany(email: string) {
return this.prisma.user.findUnique({ where: { email }, include: { company: true } });
}
async findById(id: number) {
return this.prisma.user.findUnique({ where: { id }, include: { company: true } });
}
async getSafeUser(id: number) {
const user = await this.findById(id);
if (!user) return null;
const { password, refreshToken, ...rest } = user;
return rest;
}
}

View file

@ -0,0 +1,13 @@
import { Body, Controller, HttpCode, Post } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
@ApiTags('webhooks')
@Controller('webhooks')
export class WebhooksController {
@Post('mercado-pago')
@HttpCode(200)
async handleMercadoPago(@Body() payload: any) {
// In a real-world scenario, verify Mercado Pago signatures and process events.
return { received: true, event: payload?.type ?? 'unknown' };
}
}

View file

@ -0,0 +1,7 @@
import { Module } from '@nestjs/common';
import { WebhooksController } from './webhooks.controller';
@Module({
controllers: [WebhooksController],
})
export class WebhooksModule {}

View file

@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"declaration": true,
"noEmit": false,
"incremental": false
},
"include": ["src/**/*"],
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}

View file

@ -0,0 +1,19 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "es2017",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"strict": true,
"skipLibCheck": true,
"moduleResolution": "node",
"esModuleInterop": true
}
}