This commit is contained in:
2025-08-27 21:24:13 +03:00
parent beb033bb6f
commit ef12ba29d1
10 changed files with 140 additions and 174 deletions

View File

@@ -12,12 +12,7 @@
"start:dev": "nest start --watch", "start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch", "start:debug": "nest start --debug --watch",
"start:prod": "node dist/main", "start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "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": { "dependencies": {
"@nestjs/common": "^11.0.1", "@nestjs/common": "^11.0.1",
@@ -40,10 +35,8 @@
"@eslint/js": "^9.18.0", "@eslint/js": "^9.18.0",
"@nestjs/cli": "^11.0.0", "@nestjs/cli": "^11.0.0",
"@nestjs/schematics": "^11.0.0", "@nestjs/schematics": "^11.0.0",
"@nestjs/testing": "^11.0.1",
"@prisma/client": "^6.14.0", "@prisma/client": "^6.14.0",
"@types/express": "^5.0.0", "@types/express": "^5.0.0",
"@types/jest": "^30.0.0",
"@types/multer": "^2.0.0", "@types/multer": "^2.0.0",
"@types/node": "^22.10.7", "@types/node": "^22.10.7",
"@types/supertest": "^6.0.2", "@types/supertest": "^6.0.2",
@@ -51,7 +44,6 @@
"eslint-config-prettier": "^10.0.1", "eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.2", "eslint-plugin-prettier": "^5.2.2",
"globals": "^16.0.0", "globals": "^16.0.0",
"jest": "^30.0.0",
"prettier": "^3.4.2", "prettier": "^3.4.2",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
"supertest": "^7.0.0", "supertest": "^7.0.0",
@@ -61,22 +53,5 @@
"tsconfig-paths": "^4.2.0", "tsconfig-paths": "^4.2.0",
"typescript": "^5.7.3", "typescript": "^5.7.3",
"typescript-eslint": "^8.20.0" "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"
} }
} }

View File

@@ -1,4 +1,17 @@
import { BadRequestException, Body, Controller, Delete, Get, NotFoundException, Param, ParseIntPipe, Post, Put, UploadedFile, UseInterceptors } from '@nestjs/common'; import {
BadRequestException,
Body,
Controller,
Delete,
Get,
NotFoundException,
Param,
ParseIntPipe,
Post,
Put,
UploadedFile,
UseInterceptors,
} from '@nestjs/common';
import { RegisterDto } from './dto/register.dto'; import { RegisterDto } from './dto/register.dto';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { PrismaService } from './services/prisma.service'; import { PrismaService } from './services/prisma.service';
@@ -11,8 +24,10 @@ import { unlink } from 'fs';
@ApiTags('App') @ApiTags('App')
@Controller('app') @Controller('app')
export class AppController { export class AppController {
constructor(private prisma: PrismaService, private socketService: AppGetaway) { } constructor(
private prisma: PrismaService,
private socketService: AppGetaway,
) {}
@Get('candidates') @Get('candidates')
async getCandidateList() { async getCandidateList() {
@@ -23,12 +38,12 @@ export class AppController {
age: true, age: true,
cityOrRegion: true, cityOrRegion: true,
profileImage: true, profileImage: true,
} },
}) });
data.forEach((candidate) => { data.forEach((candidate) => {
candidate.profileImage = `${process.env.HOST_URL as string}/uploads/${candidate.profileImage}`; candidate.profileImage = `${process.env.HOST_URL as string}/uploads/${candidate.profileImage}`;
}) });
return data; return data;
} }
@@ -42,21 +57,23 @@ export class AppController {
return data; return data;
} }
@Post('register') @Post('register')
@UseInterceptors(FileInterceptor('profileImage', { @UseInterceptors(
FileInterceptor('profileImage', {
storage: diskStorage({ storage: diskStorage({
destination: 'assets/uploads', destination: 'assets/uploads',
filename: (req, file, cb) => { filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9); const uniqueSuffix =
Date.now() + '-' + Math.round(Math.random() * 1e9);
const ext = extname(file.originalname); const ext = extname(file.originalname);
cb(null, `${file.fieldname}-${uniqueSuffix}${ext}`); cb(null, `${file.fieldname}-${uniqueSuffix}${ext}`);
} },
}) }),
})) }),
)
async register( async register(
@UploadedFile() file: Express.Multer.File, @UploadedFile() file: Express.Multer.File,
@Body() dto: RegisterDto @Body() dto: RegisterDto,
) { ) {
const imagePath = file ? file.filename : null; const imagePath = file ? file.filename : null;
const savedCandidate = await this.prisma.candidate.create({ const savedCandidate = await this.prisma.candidate.create({
@@ -72,27 +89,34 @@ export class AppController {
} }
@Put('candidate/:id') @Put('candidate/:id')
@UseInterceptors(FileInterceptor('profileImage', { @UseInterceptors(
FileInterceptor('profileImage', {
storage: diskStorage({ storage: diskStorage({
destination: 'assets/uploads', destination: 'assets/uploads',
filename: (req, file, cb) => { filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9); const uniqueSuffix =
Date.now() + '-' + Math.round(Math.random() * 1e9);
const ext = extname(file.originalname); const ext = extname(file.originalname);
cb(null, `${file.fieldname}-${uniqueSuffix}${ext}`); cb(null, `${file.fieldname}-${uniqueSuffix}${ext}`);
} },
}) }),
})) }),
)
async update( async update(
@Param('id', ParseIntPipe) id: number, @Param('id', ParseIntPipe) id: number,
@UploadedFile() file: Express.Multer.File, @UploadedFile() file: Express.Multer.File,
@Body() dto: RegisterDto @Body() dto: RegisterDto,
) { ) {
const candidate = await this.prisma.candidate.findFirst({ where: { id } }); const candidate = await this.prisma.candidate.findFirst({ where: { id } });
if (!candidate) throw new NotFoundException(`Candidate with id ${id} not found`); if (!candidate)
throw new NotFoundException(`Candidate with id ${id} not found`);
const dayMilliseconds = 86400000; const dayMilliseconds = 86400000;
const expirationDate = new Date(candidate.createdAt.valueOf() + 3 * dayMilliseconds); const expirationDate = new Date(
if (expirationDate < new Date()) throw new BadRequestException(`Cannot edit candidate form after 3 days`); candidate.createdAt.valueOf() + 3 * dayMilliseconds,
);
if (expirationDate < new Date())
throw new BadRequestException(`Cannot edit candidate form after 3 days`);
const profileImagePath = file ? file.filename : candidate.profileImage; const profileImagePath = file ? file.filename : candidate.profileImage;
@@ -108,16 +132,21 @@ export class AppController {
return; return;
} }
@Delete('candidate/:id') @Delete('candidate/:id')
async deleteCandidate(@Param('id', ParseIntPipe) id: number) { async deleteCandidate(@Param('id', ParseIntPipe) id: number) {
const candidate = await this.prisma.candidate.findFirst({ where: { id } }); const candidate = await this.prisma.candidate.findFirst({ where: { id } });
if (!candidate) throw new NotFoundException(`Candidate with id ${id} not found`); if (!candidate)
throw new NotFoundException(`Candidate with id ${id} not found`);
await this.prisma.candidate.delete({ where: { id } }); await this.prisma.candidate.delete({ where: { id } });
if (candidate.profileImage) { if (candidate.profileImage) {
const filePath = join(process.cwd(), 'assets', 'uploads', candidate.profileImage); const filePath = join(
process.cwd(),
'assets',
'uploads',
candidate.profileImage,
);
unlink(filePath, (err) => { unlink(filePath, (err) => {
if (err) { if (err) {
console.error('Failed to delete file:', err); console.error('Failed to delete file:', err);

View File

@@ -1,19 +1,23 @@
import { MessageBody, OnGatewayConnection, OnGatewayDisconnect, SubscribeMessage, WebSocketGateway, WebSocketServer } from "@nestjs/websockets"; import {
import { Server, Socket } from "socket.io"; OnGatewayConnection,
OnGatewayDisconnect,
WebSocketGateway,
WebSocketServer,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
@WebSocketGateway({ @WebSocketGateway({
cors: true cors: true,
}) })
export class AppGetaway implements OnGatewayDisconnect, OnGatewayConnection { export class AppGetaway implements OnGatewayDisconnect, OnGatewayConnection {
@WebSocketServer() server: Server @WebSocketServer() server: Server;
handleDisconnect(client: Socket) { handleDisconnect(client: Socket) {
console.log(`${client.id} disconnected`) console.log(`${client.id} disconnected`);
} }
handleConnection(client: Socket, ...args: any[]) { handleConnection(client: Socket /*...args: any[]*/) {
console.log(`${client.id} connected`) console.log(`${client.id} connected`);
} }
onAddCandidate(registrationData: any) { onAddCandidate(registrationData: any) {

View File

@@ -14,12 +14,13 @@ import { ConfigModule } from '@nestjs/config';
ServeStaticModule.forRoot( ServeStaticModule.forRoot(
{ {
rootPath: join(__dirname, 'assets/client'), rootPath: join(__dirname, 'assets/client'),
renderPath: '/' renderPath: '/',
}, },
{ {
rootPath: join(__dirname, 'assets/uploads'), rootPath: join(__dirname, 'assets/uploads'),
serveRoot: '/uploads', serveRoot: '/uploads',
}), },
),
], ],
controllers: [AppController, StatsController], controllers: [AppController, StatsController],
providers: [PrismaService, StatsService, AppGetaway], providers: [PrismaService, StatsService, AppGetaway],

View File

@@ -15,19 +15,11 @@ async function bootstrap() {
setupSwagger(app); setupSwagger(app);
app.enableCors({ app.enableCors({
origin: [true], origin: [true],
methods: [ methods: ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE', 'OPTIONS'],
'GET',
'HEAD',
'PUT',
'PATCH',
'POST',
'DELETE',
'OPTIONS',
],
allowedHeaders: ['*'], allowedHeaders: ['*'],
optionsSuccessStatus: 204, optionsSuccessStatus: 204,
credentials: true, credentials: true,
preflightContinue: false preflightContinue: false,
}); });
await app.listen(process.env.PORT ?? 3000); await app.listen(process.env.PORT ?? 3000);
} }
@@ -43,5 +35,5 @@ function setupSwagger(app: INestApplication<any>) {
SwaggerModule.setup('api', app, document); SwaggerModule.setup('api', app, document);
} }
// eslint-disable-next-line @typescript-eslint/no-floating-promises
bootstrap(); bootstrap();

View File

@@ -1,13 +1,12 @@
import { Injectable } from "@nestjs/common"; import { Injectable } from '@nestjs/common';
import { PrismaService } from "./prisma.service"; import { PrismaService } from './prisma.service';
import { AppGetaway } from "src/app.getaway"; import { AppGetaway } from 'src/app.getaway';
@Injectable() @Injectable()
export class StatsService { export class StatsService {
constructor( constructor(
private prisma: PrismaService, private prisma: PrismaService,
private socketService: AppGetaway private socketService: AppGetaway,
) {} ) {}
async incrementVisits() { async incrementVisits() {

View File

@@ -1,25 +0,0 @@
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

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