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

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

View File

@@ -1,30 +1,34 @@
import { MessageBody, OnGatewayConnection, OnGatewayDisconnect, SubscribeMessage, WebSocketGateway, WebSocketServer } from "@nestjs/websockets";
import { Server, Socket } from "socket.io";
import {
OnGatewayConnection,
OnGatewayDisconnect,
WebSocketGateway,
WebSocketServer,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
@WebSocketGateway({
cors: true
cors: true,
})
export class AppGetaway implements OnGatewayDisconnect, OnGatewayConnection {
@WebSocketServer() server: Server
@WebSocketServer() server: Server;
handleDisconnect(client: Socket) {
console.log(`${client.id} disconnected`)
}
handleDisconnect(client: Socket) {
console.log(`${client.id} disconnected`);
}
handleConnection(client: Socket, ...args: any[]) {
console.log(`${client.id} connected`)
}
handleConnection(client: Socket /*...args: any[]*/) {
console.log(`${client.id} connected`);
}
onAddCandidate(registrationData: any) {
this.server.emit('candidateRegistered', registrationData);
}
onAddCandidate(registrationData: any) {
this.server.emit('candidateRegistered', registrationData);
}
onUpdateCandidate(updatedData: any) {
this.server.emit('candidateUpdated', updatedData);
}
onUpdateCandidate(updatedData: any) {
this.server.emit('candidateUpdated', updatedData);
}
onDeleteCandidate(deletedId: number) {
this.server.emit('candidateDeleted', deletedId);
}
onDeleteCandidate(deletedId: number) {
this.server.emit('candidateDeleted', deletedId);
}
}

View File

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

View File

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

View File

@@ -3,7 +3,7 @@ import { PrismaClient } from 'generated/prisma';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
async onModuleInit() {
await this.$connect();
}
async onModuleInit() {
await this.$connect();
}
}

View File

@@ -1,34 +1,33 @@
import { Injectable } from "@nestjs/common";
import { PrismaService } from "./prisma.service";
import { AppGetaway } from "src/app.getaway";
import { Injectable } from '@nestjs/common';
import { PrismaService } from './prisma.service';
import { AppGetaway } from 'src/app.getaway';
@Injectable()
export class StatsService {
constructor(
private prisma: PrismaService,
private socketService: AppGetaway
) { }
constructor(
private prisma: PrismaService,
private socketService: AppGetaway,
) {}
async incrementVisits() {
const stats = await this.prisma.stats.update({
where: { id: 1 },
data: { totalVisits: { increment: 1 } },
});
this.socketService.server.emit('statsUpdated', stats);
return stats;
}
async incrementVisits() {
const stats = await this.prisma.stats.update({
where: { id: 1 },
data: { totalVisits: { increment: 1 } },
});
this.socketService.server.emit('statsUpdated', stats);
return stats;
}
async incrementClicks() {
const stats = await this.prisma.stats.update({
where: { id: 1 },
data: { totalClicks: { increment: 1 } },
});
this.socketService.server.emit('statsUpdated', stats);
return stats;
}
async incrementClicks() {
const stats = await this.prisma.stats.update({
where: { id: 1 },
data: { totalClicks: { increment: 1 } },
});
this.socketService.server.emit('statsUpdated', stats);
return stats;
}
async getStats() {
return this.prisma.stats.findUnique({ where: { id: 1 } });
}
async getStats() {
return this.prisma.stats.findUnique({ where: { id: 1 } });
}
}

View File

@@ -3,20 +3,20 @@ import { StatsService } from './services/stats.service';
@Controller('stats')
export class StatsController {
constructor(private readonly statsService: StatsService) { }
constructor(private readonly statsService: StatsService) {}
@Get()
getStats() {
return this.statsService.getStats();
}
@Get()
getStats() {
return this.statsService.getStats();
}
@Post('visit')
incrementVisit() {
return this.statsService.incrementVisits();
}
@Post('visit')
incrementVisit() {
return this.statsService.incrementVisits();
}
@Post('click')
incrementClick() {
return this.statsService.incrementClicks();
}
@Post('click')
incrementClick() {
return this.statsService.incrementClicks();
}
}

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"
}
}