Initial commit from Specify template

This commit is contained in:
2026-01-29 08:34:11 -03:00
commit fe2c861007
74 changed files with 22234 additions and 0 deletions

17
backend/.env.example Normal file
View File

@@ -0,0 +1,17 @@
# Server
PORT=4000
NODE_ENV=development
# Database
DB_HOST=localhost
DB_PORT=5432
DB_USERNAME=thumbpreview
DB_PASSWORD=thumbpreview123
DB_DATABASE=thumbpreview
# YouTube API
YOUTUBE_API_KEY=YOUR_YOUTUBE_API_KEY_HERE
# Upload
UPLOAD_DIR=./uploads
MAX_FILE_SIZE=5242880

4
backend/.prettierrc Normal file
View File

@@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}

98
backend/README.md Normal file
View File

@@ -0,0 +1,98 @@
<p align="center">
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="120" alt="Nest Logo" /></a>
</p>
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
[circleci-url]: https://circleci.com/gh/nestjs/nest
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
<p align="center">
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg" alt="Donate us"/></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow" alt="Follow us on Twitter"></a>
</p>
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](https://opencollective.com/nest#sponsor)-->
## Description
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
## Project setup
```bash
$ npm install
```
## Compile and run the project
```bash
# development
$ npm run start
# watch mode
$ npm run start:dev
# production mode
$ npm run start:prod
```
## Run tests
```bash
# unit tests
$ npm run test
# e2e tests
$ npm run test:e2e
# test coverage
$ npm run test:cov
```
## Deployment
When you're ready to deploy your NestJS application to production, there are some key steps you can take to ensure it runs as efficiently as possible. Check out the [deployment documentation](https://docs.nestjs.com/deployment) for more information.
If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps:
```bash
$ npm install -g mau
$ mau deploy
```
With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure.
## Resources
Check out a few resources that may come in handy when working with NestJS:
- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework.
- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy).
- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/).
- Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks.
- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com).
- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com).
- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs).
- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com).
## Support
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
## Stay in touch
- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec)
- Website - [https://nestjs.com](https://nestjs.com/)
- Twitter - [@nestframework](https://twitter.com/nestframework)
## License
Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).

35
backend/eslint.config.mjs Normal file
View File

@@ -0,0 +1,35 @@
// @ts-check
import eslint from '@eslint/js';
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
import globals from 'globals';
import tseslint from 'typescript-eslint';
export default tseslint.config(
{
ignores: ['eslint.config.mjs'],
},
eslint.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
eslintPluginPrettierRecommended,
{
languageOptions: {
globals: {
...globals.node,
...globals.jest,
},
ecmaVersion: 5,
sourceType: 'module',
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
},
{
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-floating-promises': 'warn',
'@typescript-eslint/no-unsafe-argument': 'warn'
},
},
);

8
backend/nest-cli.json Normal file
View File

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

11627
backend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

83
backend/package.json Normal file
View File

@@ -0,0 +1,83 @@
{
"name": "backend",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@nestjs/axios": "^4.0.1",
"@nestjs/common": "^11.0.1",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.1",
"@nestjs/platform-express": "^11.0.1",
"@nestjs/typeorm": "^11.0.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.3",
"multer": "^2.0.2",
"pg": "^8.17.2",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"sharp": "^0.34.5",
"typeorm": "^0.3.28"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.18.0",
"@nestjs/cli": "^11.0.0",
"@nestjs/schematics": "^11.0.0",
"@nestjs/testing": "^11.0.1",
"@swc/cli": "^0.6.0",
"@swc/core": "^1.10.7",
"@types/express": "^5.0.0",
"@types/jest": "^29.5.14",
"@types/multer": "^2.0.0",
"@types/node": "^22.10.7",
"@types/supertest": "^6.0.2",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.2",
"globals": "^16.0.0",
"jest": "^29.7.0",
"prettier": "^3.4.2",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
"ts-jest": "^29.2.5",
"ts-loader": "^9.5.2",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.7.3",
"typescript-eslint": "^8.20.0"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}

30
backend/src/app.module.ts Normal file
View File

@@ -0,0 +1,30 @@
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ThumbnailsModule } from './modules/thumbnails/thumbnails.module';
import { YouTubeModule } from './modules/youtube/youtube.module';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
}),
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
type: 'postgres',
host: configService.get('DB_HOST', 'localhost'),
port: configService.get('DB_PORT', 5432),
username: configService.get('DB_USERNAME', 'thumbpreview'),
password: configService.get('DB_PASSWORD', 'thumbpreview123'),
database: configService.get('DB_DATABASE', 'thumbpreview'),
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: configService.get('NODE_ENV') !== 'production',
}),
inject: [ConfigService],
}),
ThumbnailsModule,
YouTubeModule,
],
})
export class AppModule {}

View File

@@ -0,0 +1,27 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
} from 'typeorm';
@Entity('thumbnails')
export class Thumbnail {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
filePath: string;
@Column()
originalName: string;
@Column({ nullable: true })
title: string;
@Column({ default: 0 })
position: number;
@CreateDateColumn()
createdAt: Date;
}

View File

@@ -0,0 +1,27 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
Index,
} from 'typeorm';
@Entity('youtube_cache')
export class YouTubeCache {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column()
searchQuery: string;
@Column('jsonb')
results: any;
@CreateDateColumn()
createdAt: Date;
@Index()
@Column()
expiresAt: Date;
}

36
backend/src/main.ts Normal file
View File

@@ -0,0 +1,36 @@
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';
import { join } from 'path';
import { NestExpressApplication } from '@nestjs/platform-express';
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
// Global prefix
app.setGlobalPrefix('api');
// Validation
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
transform: true,
}),
);
// CORS
app.enableCors({
origin: ['http://localhost:3000', 'http://localhost:5173'],
credentials: true,
});
// Static files for uploads
app.useStaticAssets(join(__dirname, '..', 'uploads'), {
prefix: '/uploads/',
});
const port = process.env.PORT ?? 4000;
await app.listen(port);
console.log(`Backend running on http://localhost:${port}`);
}
bootstrap();

View File

@@ -0,0 +1,33 @@
import {
Controller,
Post,
Delete,
Param,
UseInterceptors,
UploadedFile,
BadRequestException,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { ThumbnailsService } from './thumbnails.service';
@Controller('thumbnails')
export class ThumbnailsController {
constructor(private readonly thumbnailsService: ThumbnailsService) {}
@Post('upload')
@UseInterceptors(FileInterceptor('file'))
async upload(@UploadedFile() file: Express.Multer.File) {
if (!file) {
throw new BadRequestException('No file uploaded');
}
const thumbnail = await this.thumbnailsService.create(file);
return this.thumbnailsService.formatThumbnailResponse(thumbnail);
}
@Delete(':id')
async remove(@Param('id') id: string) {
await this.thumbnailsService.remove(id);
return { success: true };
}
}

View File

@@ -0,0 +1,38 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { MulterModule } from '@nestjs/platform-express';
import { diskStorage } from 'multer';
import { extname } from 'path';
import { v4 as uuidv4 } from 'uuid';
import { ThumbnailsController } from './thumbnails.controller';
import { ThumbnailsService } from './thumbnails.service';
import { Thumbnail } from '../../entities/thumbnail.entity';
@Module({
imports: [
TypeOrmModule.forFeature([Thumbnail]),
MulterModule.register({
storage: diskStorage({
destination: './uploads',
filename: (req, file, callback) => {
const uniqueName = `${uuidv4()}${extname(file.originalname)}`;
callback(null, uniqueName);
},
}),
fileFilter: (req, file, callback) => {
const allowedMimes = ['image/jpeg', 'image/png', 'image/webp'];
if (allowedMimes.includes(file.mimetype)) {
callback(null, true);
} else {
callback(new Error('Invalid file type'), false);
}
},
limits: {
fileSize: 5 * 1024 * 1024, // 5MB
},
}),
],
controllers: [ThumbnailsController],
providers: [ThumbnailsService],
})
export class ThumbnailsModule {}

View File

@@ -0,0 +1,55 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { unlink } from 'fs/promises';
import { join } from 'path';
import { Thumbnail } from '../../entities/thumbnail.entity';
@Injectable()
export class ThumbnailsService {
constructor(
@InjectRepository(Thumbnail)
private thumbnailRepository: Repository<Thumbnail>,
) {}
async create(file: Express.Multer.File): Promise<Thumbnail> {
const thumbnail = this.thumbnailRepository.create({
filePath: file.filename,
originalName: file.originalname,
title: file.originalname.replace(/\.[^/.]+$/, ''),
});
return this.thumbnailRepository.save(thumbnail);
}
async findOne(id: string): Promise<Thumbnail> {
const thumbnail = await this.thumbnailRepository.findOne({ where: { id } });
if (!thumbnail) {
throw new NotFoundException(`Thumbnail with ID ${id} not found`);
}
return thumbnail;
}
async remove(id: string): Promise<void> {
const thumbnail = await this.findOne(id);
// Delete file from disk
try {
await unlink(join('./uploads', thumbnail.filePath));
} catch (error) {
console.error('Error deleting file:', error);
}
await this.thumbnailRepository.remove(thumbnail);
}
formatThumbnailResponse(thumbnail: Thumbnail) {
return {
id: thumbnail.id,
url: `/uploads/${thumbnail.filePath}`,
originalName: thumbnail.originalName,
title: thumbnail.title,
position: thumbnail.position,
};
}
}

View File

@@ -0,0 +1,24 @@
import { Controller, Get, Query, BadRequestException } from '@nestjs/common';
import { YouTubeService, YouTubeVideoResponse } from './youtube.service';
@Controller('youtube')
export class YouTubeController {
constructor(private readonly youtubeService: YouTubeService) {}
@Get('search')
async search(
@Query('q') query: string,
@Query('maxResults') maxResults?: string,
): Promise<YouTubeVideoResponse[]> {
if (!query || query.trim().length === 0) {
throw new BadRequestException('Search query is required');
}
const results = await this.youtubeService.search(
query.trim(),
maxResults ? parseInt(maxResults, 10) : 10,
);
return results;
}
}

View File

@@ -0,0 +1,16 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { HttpModule } from '@nestjs/axios';
import { YouTubeController } from './youtube.controller';
import { YouTubeService } from './youtube.service';
import { YouTubeCache } from '../../entities/youtube-cache.entity';
@Module({
imports: [
TypeOrmModule.forFeature([YouTubeCache]),
HttpModule,
],
controllers: [YouTubeController],
providers: [YouTubeService],
})
export class YouTubeModule {}

View File

@@ -0,0 +1,201 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, MoreThan } from 'typeorm';
import { HttpService } from '@nestjs/axios';
import { firstValueFrom } from 'rxjs';
import { YouTubeCache } from '../../entities/youtube-cache.entity';
interface YouTubeSearchItem {
id: { videoId: string };
snippet: {
title: string;
channelTitle: string;
publishedAt: string;
thumbnails: {
medium: { url: string };
high: { url: string };
};
};
}
export interface YouTubeVideoResponse {
videoId: string;
title: string;
channelTitle: string;
thumbnailUrl: string;
publishedAt: string;
viewCount?: string;
}
interface YouTubeSearchResponse {
items: YouTubeSearchItem[];
}
interface YouTubeStatsResponse {
items: Array<{
id: string;
statistics: {
viewCount: string;
};
}>;
}
@Injectable()
export class YouTubeService {
private readonly apiKey: string;
private readonly cacheHours = 24;
constructor(
private configService: ConfigService,
private httpService: HttpService,
@InjectRepository(YouTubeCache)
private cacheRepository: Repository<YouTubeCache>,
) {
this.apiKey = this.configService.get<string>('YOUTUBE_API_KEY', '');
}
async search(query: string, maxResults = 10): Promise<YouTubeVideoResponse[]> {
// Check cache first
const cached = await this.getFromCache(query);
if (cached) {
return cached;
}
// If no API key, return mock data for development
if (!this.apiKey || this.apiKey === 'YOUR_YOUTUBE_API_KEY_HERE') {
return this.getMockResults(query, maxResults);
}
// Fetch from YouTube API
const results = await this.fetchFromYouTube(query, maxResults);
// Save to cache
await this.saveToCache(query, results);
return results;
}
private async getFromCache(query: string): Promise<YouTubeVideoResponse[] | null> {
const cached = await this.cacheRepository.findOne({
where: {
searchQuery: query.toLowerCase(),
expiresAt: MoreThan(new Date()),
},
});
return cached ? cached.results : null;
}
private async saveToCache(query: string, results: YouTubeVideoResponse[]): Promise<void> {
const expiresAt = new Date();
expiresAt.setHours(expiresAt.getHours() + this.cacheHours);
const cache = this.cacheRepository.create({
searchQuery: query.toLowerCase(),
results,
expiresAt,
});
await this.cacheRepository.save(cache);
}
private async fetchFromYouTube(query: string, maxResults: number): Promise<YouTubeVideoResponse[]> {
const searchUrl = 'https://www.googleapis.com/youtube/v3/search';
const { data } = await firstValueFrom(
this.httpService.get<YouTubeSearchResponse>(searchUrl, {
params: {
part: 'snippet',
q: query,
type: 'video',
maxResults,
key: this.apiKey,
},
}),
);
const videoIds = data.items.map((item: YouTubeSearchItem) => item.id.videoId).join(',');
// Get view counts
const statsUrl = 'https://www.googleapis.com/youtube/v3/videos';
const { data: statsData } = await firstValueFrom(
this.httpService.get<YouTubeStatsResponse>(statsUrl, {
params: {
part: 'statistics',
id: videoIds,
key: this.apiKey,
},
}),
);
const viewCounts = new Map<string, string>();
statsData.items.forEach((item) => {
viewCounts.set(item.id, item.statistics.viewCount);
});
return data.items.map((item: YouTubeSearchItem) => ({
videoId: item.id.videoId,
title: item.snippet.title,
channelTitle: item.snippet.channelTitle,
thumbnailUrl: item.snippet.thumbnails.high?.url || item.snippet.thumbnails.medium.url,
publishedAt: item.snippet.publishedAt,
viewCount: viewCounts.get(item.id.videoId),
}));
}
private getMockResults(query: string, maxResults: number): YouTubeVideoResponse[] {
const mockVideos: YouTubeVideoResponse[] = [
{
videoId: 'mock1',
title: `${query} - Complete Tutorial for Beginners`,
channelTitle: 'Tech Academy',
thumbnailUrl: 'https://i.ytimg.com/vi/dQw4w9WgXcQ/hqdefault.jpg',
publishedAt: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(),
viewCount: '1250000',
},
{
videoId: 'mock2',
title: `Learn ${query} in 30 Minutes`,
channelTitle: 'Code Master',
thumbnailUrl: 'https://i.ytimg.com/vi/9bZkp7q19f0/hqdefault.jpg',
publishedAt: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(),
viewCount: '890000',
},
{
videoId: 'mock3',
title: `${query} Crash Course 2025`,
channelTitle: 'Dev Tutorial',
thumbnailUrl: 'https://i.ytimg.com/vi/kJQP7kiw5Fk/hqdefault.jpg',
publishedAt: new Date(Date.now() - 14 * 24 * 60 * 60 * 1000).toISOString(),
viewCount: '2100000',
},
{
videoId: 'mock4',
title: `Why ${query} is Amazing`,
channelTitle: 'Tech Reviews',
thumbnailUrl: 'https://i.ytimg.com/vi/RgKAFK5djSk/hqdefault.jpg',
publishedAt: new Date(Date.now() - 60 * 24 * 60 * 60 * 1000).toISOString(),
viewCount: '450000',
},
{
videoId: 'mock5',
title: `${query} Tips and Tricks`,
channelTitle: 'Pro Tips',
thumbnailUrl: 'https://i.ytimg.com/vi/fJ9rUzIMcZQ/hqdefault.jpg',
publishedAt: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString(),
viewCount: '320000',
},
{
videoId: 'mock6',
title: `${query} for Professionals`,
channelTitle: 'Advanced Learning',
thumbnailUrl: 'https://i.ytimg.com/vi/09R8_2nJtjg/hqdefault.jpg',
publishedAt: new Date(Date.now() - 45 * 24 * 60 * 60 * 1000).toISOString(),
viewCount: '780000',
},
];
return mockVideos.slice(0, maxResults);
}
}

View File

@@ -0,0 +1,25 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { App } from 'supertest/types';
import { AppModule } from './../src/app.module';
describe('AppController (e2e)', () => {
let app: INestApplication<App>;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Hello World!');
});
});

View File

@@ -0,0 +1,9 @@
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
}

View File

@@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}

21
backend/tsconfig.json Normal file
View File

@@ -0,0 +1,21 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2023",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": true,
"forceConsistentCasingInFileNames": true,
"noImplicitAny": false,
"strictBindCallApply": false,
"noFallthroughCasesInSwitch": false
}
}

0
backend/uploads/.gitkeep Normal file
View File