Viktoria435 commited on
Commit
22df730
·
0 Parent(s):

Initialize NestJS project with basic structure and configurations

Browse files

- Added .gitignore to exclude build artifacts and environment files.
- Created .prettierrc for code formatting preferences.
- Set up ESLint configuration for TypeScript with Prettier integration.
- Configured Nest CLI with nest-cli.json.
- Established package.json and package-lock.json for project dependencies.
- Created initial README.md for project documentation.
- Defined TypeScript configurations in tsconfig.json and tsconfig.build.json.
- Implemented basic application structure with AppModule, AppController, and AppService.
- Developed Book, Visitor, and Worker modules with respective controllers and services.
- Added DTOs for data validation and Swagger setup for API documentation.
- Implemented utility functions for file management, encryption, and date handling.
- Created initial data files for books, visitors, and workers.
- Added exception filters and response interceptors for error handling and response formatting.

This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitignore +56 -0
  2. .prettierrc +4 -0
  3. README.md +98 -0
  4. eslint.config.mjs +35 -0
  5. nest-cli.json +8 -0
  6. package-lock.json +0 -0
  7. package.json +75 -0
  8. src/app.controller.spec.ts +22 -0
  9. src/app.controller.ts +7 -0
  10. src/app.module.ts +13 -0
  11. src/app.service.ts +4 -0
  12. src/book/book.controller.ts +110 -0
  13. src/book/book.module.ts +14 -0
  14. src/book/book.service.ts +123 -0
  15. src/book/dto/book.dto.ts +77 -0
  16. src/book/dto/borrow-book.dto.ts +39 -0
  17. src/book/dto/create-book.dto.ts +68 -0
  18. src/book/dto/return-book.dto.ts +39 -0
  19. src/book/dto/update-book.dto.ts +4 -0
  20. src/common/Link.ts +15 -0
  21. src/common/LinkManager.ts +33 -0
  22. src/common/book-link-manager.ts +9 -0
  23. src/common/key-manager.ts +51 -0
  24. src/common/visitor-link-manager.ts +33 -0
  25. src/common/worker-link-manager.ts +27 -0
  26. src/config/swagger.config.ts +9 -0
  27. src/data/books.txt +8 -0
  28. src/data/encrypted/.encryption_key +1 -0
  29. src/data/visitors.txt +1 -0
  30. src/data/workers.txt +1 -0
  31. src/filters/http-exception.filter.ts +58 -0
  32. src/interceptors/response.interceptor.ts +33 -0
  33. src/main.ts +26 -0
  34. src/types/book.types.ts +53 -0
  35. src/types/worker.types.ts +25 -0
  36. src/utils/crypto.utils.ts +23 -0
  37. src/utils/date.utils.ts +26 -0
  38. src/utils/download.utils.ts +16 -0
  39. src/utils/file-manager.ts +53 -0
  40. src/utils/file.utils.ts +88 -0
  41. src/utils/lzss.util.ts +165 -0
  42. src/utils/offset-encryption.util.ts +55 -0
  43. src/utils/response-wrapper.utils.ts +11 -0
  44. src/utils/swagger.utils.ts +10 -0
  45. src/visitor/dto/create-visitor.dto.ts +20 -0
  46. src/visitor/dto/update-visitor.dto.ts +20 -0
  47. src/visitor/dto/visitor.dto.ts +48 -0
  48. src/visitor/visitor.controller.ts +52 -0
  49. src/visitor/visitor.module.ts +13 -0
  50. src/visitor/visitor.service.ts +138 -0
.gitignore ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # compiled output
2
+ /dist
3
+ /node_modules
4
+ /build
5
+
6
+ # Logs
7
+ logs
8
+ *.log
9
+ npm-debug.log*
10
+ pnpm-debug.log*
11
+ yarn-debug.log*
12
+ yarn-error.log*
13
+ lerna-debug.log*
14
+
15
+ # OS
16
+ .DS_Store
17
+
18
+ # Tests
19
+ /coverage
20
+ /.nyc_output
21
+
22
+ # IDEs and editors
23
+ /.idea
24
+ .project
25
+ .classpath
26
+ .c9/
27
+ *.launch
28
+ .settings/
29
+ *.sublime-workspace
30
+
31
+ # IDE - VSCode
32
+ .vscode/*
33
+ !.vscode/settings.json
34
+ !.vscode/tasks.json
35
+ !.vscode/launch.json
36
+ !.vscode/extensions.json
37
+
38
+ # dotenv environment variable files
39
+ .env
40
+ .env.development.local
41
+ .env.test.local
42
+ .env.production.local
43
+ .env.local
44
+
45
+ # temp directory
46
+ .temp
47
+ .tmp
48
+
49
+ # Runtime data
50
+ pids
51
+ *.pid
52
+ *.seed
53
+ *.pid.lock
54
+
55
+ # Diagnostic reports (https://nodejs.org/api/report.html)
56
+ report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
.prettierrc ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ {
2
+ "singleQuote": true,
3
+ "trailingComma": "all"
4
+ }
README.md ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <p align="center">
2
+ <a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="120" alt="Nest Logo" /></a>
3
+ </p>
4
+
5
+ [circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
6
+ [circleci-url]: https://circleci.com/gh/nestjs/nest
7
+
8
+ <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>
9
+ <p align="center">
10
+ <a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
11
+ <a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
12
+ <a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
13
+ <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>
14
+ <a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
15
+ <a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
16
+ <a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
17
+ <a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg" alt="Donate us"/></a>
18
+ <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>
19
+ <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>
20
+ </p>
21
+ <!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
22
+ [![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](https://opencollective.com/nest#sponsor)-->
23
+
24
+ ## Description
25
+
26
+ [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
27
+
28
+ ## Project setup
29
+
30
+ ```bash
31
+ $ npm install
32
+ ```
33
+
34
+ ## Compile and run the project
35
+
36
+ ```bash
37
+ # development
38
+ $ npm run start
39
+
40
+ # watch mode
41
+ $ npm run start:dev
42
+
43
+ # production mode
44
+ $ npm run start:prod
45
+ ```
46
+
47
+ ## Run tests
48
+
49
+ ```bash
50
+ # unit tests
51
+ $ npm run test
52
+
53
+ # e2e tests
54
+ $ npm run test:e2e
55
+
56
+ # test coverage
57
+ $ npm run test:cov
58
+ ```
59
+
60
+ ## Deployment
61
+
62
+ 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.
63
+
64
+ 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:
65
+
66
+ ```bash
67
+ $ npm install -g @nestjs/mau
68
+ $ mau deploy
69
+ ```
70
+
71
+ With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure.
72
+
73
+ ## Resources
74
+
75
+ Check out a few resources that may come in handy when working with NestJS:
76
+
77
+ - Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework.
78
+ - For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy).
79
+ - To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/).
80
+ - Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks.
81
+ - Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com).
82
+ - Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com).
83
+ - To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs).
84
+ - Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com).
85
+
86
+ ## Support
87
+
88
+ 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).
89
+
90
+ ## Stay in touch
91
+
92
+ - Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec)
93
+ - Website - [https://nestjs.com](https://nestjs.com/)
94
+ - Twitter - [@nestframework](https://twitter.com/nestframework)
95
+
96
+ ## License
97
+
98
+ Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).
eslint.config.mjs ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // @ts-check
2
+ import eslint from '@eslint/js';
3
+ import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
4
+ import globals from 'globals';
5
+ import tseslint from 'typescript-eslint';
6
+
7
+ export default tseslint.config(
8
+ {
9
+ ignores: ['eslint.config.mjs'],
10
+ },
11
+ eslint.configs.recommended,
12
+ ...tseslint.configs.recommendedTypeChecked,
13
+ eslintPluginPrettierRecommended,
14
+ {
15
+ languageOptions: {
16
+ globals: {
17
+ ...globals.node,
18
+ ...globals.jest,
19
+ },
20
+ sourceType: 'commonjs',
21
+ parserOptions: {
22
+ projectService: true,
23
+ tsconfigRootDir: import.meta.dirname,
24
+ },
25
+ },
26
+ },
27
+ {
28
+ rules: {
29
+ '@typescript-eslint/no-explicit-any': 'off',
30
+ '@typescript-eslint/no-floating-promises': 'warn',
31
+ '@typescript-eslint/no-unsafe-argument': 'warn',
32
+ "prettier/prettier": ["error", { endOfLine: "auto" }],
33
+ },
34
+ },
35
+ );
nest-cli.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://json.schemastore.org/nest-cli",
3
+ "collection": "@nestjs/schematics",
4
+ "sourceRoot": "src",
5
+ "compilerOptions": {
6
+ "deleteOutDir": true
7
+ }
8
+ }
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "lab4",
3
+ "version": "0.0.1",
4
+ "description": "",
5
+ "author": "",
6
+ "private": true,
7
+ "license": "UNLICENSED",
8
+ "scripts": {
9
+ "build": "nest build",
10
+ "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
11
+ "start": "nest start",
12
+ "start:dev": "nest start --watch",
13
+ "start:debug": "nest start --debug --watch",
14
+ "start:prod": "node dist/main",
15
+ "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
16
+ "test": "jest",
17
+ "test:watch": "jest --watch",
18
+ "test:cov": "jest --coverage",
19
+ "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
20
+ "test:e2e": "jest --config ./test/jest-e2e.json"
21
+ },
22
+ "dependencies": {
23
+ "@nestjs/common": "^11.0.1",
24
+ "@nestjs/core": "^11.0.1",
25
+ "@nestjs/mapped-types": "*",
26
+ "@nestjs/platform-express": "^11.0.1",
27
+ "@nestjs/swagger": "^11.2.3",
28
+ "class-transformer": "^0.5.1",
29
+ "class-validator": "^0.14.3",
30
+ "reflect-metadata": "^0.2.2",
31
+ "rxjs": "^7.8.1"
32
+ },
33
+ "devDependencies": {
34
+ "@eslint/eslintrc": "^3.2.0",
35
+ "@eslint/js": "^9.18.0",
36
+ "@nestjs/cli": "^11.0.0",
37
+ "@nestjs/schematics": "^11.0.0",
38
+ "@nestjs/testing": "^11.0.1",
39
+ "@types/express": "^5.0.0",
40
+ "@types/jest": "^30.0.0",
41
+ "@types/node": "^22.10.7",
42
+ "@types/supertest": "^6.0.2",
43
+ "eslint": "^9.18.0",
44
+ "eslint-config-prettier": "^10.0.1",
45
+ "eslint-plugin-prettier": "^5.2.2",
46
+ "globals": "^16.0.0",
47
+ "jest": "^30.0.0",
48
+ "prettier": "^3.4.2",
49
+ "source-map-support": "^0.5.21",
50
+ "supertest": "^7.0.0",
51
+ "ts-jest": "^29.2.5",
52
+ "ts-loader": "^9.5.2",
53
+ "ts-node": "^10.9.2",
54
+ "tsconfig-paths": "^4.2.0",
55
+ "typescript": "^5.7.3",
56
+ "typescript-eslint": "^8.20.0"
57
+ },
58
+ "jest": {
59
+ "moduleFileExtensions": [
60
+ "js",
61
+ "json",
62
+ "ts"
63
+ ],
64
+ "rootDir": "src",
65
+ "testRegex": ".*\\.spec\\.ts$",
66
+ "transform": {
67
+ "^.+\\.(t|j)s$": "ts-jest"
68
+ },
69
+ "collectCoverageFrom": [
70
+ "**/*.(t|j)s"
71
+ ],
72
+ "coverageDirectory": "../coverage",
73
+ "testEnvironment": "node"
74
+ }
75
+ }
src/app.controller.spec.ts ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Test, TestingModule } from '@nestjs/testing';
2
+ import { AppController } from './app.controller';
3
+ import { AppService } from './app.service';
4
+
5
+ describe('AppController', () => {
6
+ let appController: AppController;
7
+
8
+ beforeEach(async () => {
9
+ const app: TestingModule = await Test.createTestingModule({
10
+ controllers: [AppController],
11
+ providers: [AppService],
12
+ }).compile();
13
+
14
+ appController = app.get<AppController>(AppController);
15
+ });
16
+
17
+ describe('root', () => {
18
+ it('should return "Hello World!"', () => {
19
+ expect(appController.getHello()).toBe('Hello World!');
20
+ });
21
+ });
22
+ });
src/app.controller.ts ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import { Controller } from '@nestjs/common';
2
+ import { AppService } from './app.service';
3
+
4
+ @Controller()
5
+ export class AppController {
6
+ constructor(private readonly appService: AppService) {}
7
+ }
src/app.module.ts ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Module } from '@nestjs/common';
2
+ import { AppController } from './app.controller';
3
+ import { AppService } from './app.service';
4
+ import { BookModule } from './book/book.module';
5
+ import { WorkerModule } from './worker/worker.module';
6
+ import { VisitorModule } from './visitor/visitor.module';
7
+
8
+ @Module({
9
+ imports: [BookModule, WorkerModule, VisitorModule],
10
+ controllers: [AppController],
11
+ providers: [AppService],
12
+ })
13
+ export class AppModule {}
src/app.service.ts ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ import { Injectable } from '@nestjs/common';
2
+
3
+ @Injectable()
4
+ export class AppService {}
src/book/book.controller.ts ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ Controller,
3
+ Get,
4
+ Post,
5
+ Patch,
6
+ Delete,
7
+ Param,
8
+ Body,
9
+ NotFoundException,
10
+ BadRequestException,
11
+ } from '@nestjs/common';
12
+ import { BookService } from './book.service';
13
+ import { VisitorService } from '../visitor/visitor.service';
14
+ import { CreateBookDto } from './dto/create-book.dto';
15
+ import { BorrowBookDto } from './dto/borrow-book.dto';
16
+ import { ReturnBookDto } from './dto/return-book.dto';
17
+ import { DateUtils } from 'src/utils/date.utils';
18
+ import { WorkerService } from 'src/worker/worker.service';
19
+ import { UpdateBookDto } from './dto/update-book.dto';
20
+ import { buildDownloadFile } from 'src/utils/download.utils';
21
+
22
+ @Controller('books')
23
+ export class BookController {
24
+ constructor(
25
+ private readonly bookService: BookService,
26
+ private readonly visitorService: VisitorService,
27
+ private readonly workerService: WorkerService,
28
+ ) {}
29
+
30
+ @Get('all')
31
+ async getAll() {
32
+ return await this.bookService.getAll();
33
+ }
34
+
35
+ @Get(':id/download')
36
+ async download(@Param('id') id: string) {
37
+ const book = await this.bookService.getById(id);
38
+ if (!book) throw new NotFoundException(`Book with id ${id} not found`);
39
+ return buildDownloadFile('books', id, book);
40
+ }
41
+
42
+ @Post('create')
43
+ async create(@Body() dto: CreateBookDto) {
44
+ return await this.bookService.add(dto);
45
+ }
46
+
47
+ @Get(':id')
48
+ async getById(@Param('id') id: string) {
49
+ const book = await this.bookService.getById(id);
50
+ if (!book) throw new NotFoundException(`Book with id ${id} not found`);
51
+ return book;
52
+ }
53
+
54
+ @Patch(':id')
55
+ async update(@Param('id') id: string, @Body() dto: UpdateBookDto) {
56
+ const updated = await this.bookService.update(id, dto);
57
+ if (!updated) throw new NotFoundException(`Book with id ${id} not found`);
58
+ return updated;
59
+ }
60
+
61
+ @Delete(':id')
62
+ async remove(@Param('id') id: string) {
63
+ const deleted = await this.bookService.delete(id);
64
+ if (!deleted) throw new NotFoundException(`Book with id ${id} not found`);
65
+ return { message: `Book with id ${id} deleted successfully` };
66
+ }
67
+
68
+ @Post('borrow')
69
+ async borrow(@Body() dto: BorrowBookDto) {
70
+ const visitor = await this.visitorService.getById(dto.visitorId);
71
+ if (!visitor) throw new NotFoundException(`Visitor not found`);
72
+
73
+ const worker = await this.workerService.getById(dto.workerId);
74
+ if (!worker) throw new NotFoundException(`Worker not found`);
75
+
76
+ if (!DateUtils.isWorkingDay(dto.borrowDate, worker.workDays)) {
77
+ throw new BadRequestException('Library is closed on this day');
78
+ }
79
+
80
+ const borrowedBooks = await this.bookService.borrowBooks(
81
+ dto.bookIds,
82
+ visitor,
83
+ );
84
+ await this.visitorService.addCurrentBooks(visitor.id, borrowedBooks);
85
+ await this.workerService.addIssuedBooks(worker.id, borrowedBooks);
86
+
87
+ return { message: `Successfully borrowed ${borrowedBooks.length} book(s)` };
88
+ }
89
+
90
+ @Post('return')
91
+ async return(@Body() dto: ReturnBookDto) {
92
+ const visitor = await this.visitorService.getById(dto.visitorId);
93
+ if (!visitor) throw new NotFoundException(`Visitor not found`);
94
+
95
+ const worker = await this.workerService.getById(dto.workerId);
96
+ if (!worker) throw new NotFoundException(`Worker not found`);
97
+
98
+ if (!DateUtils.isWorkingDay(dto.returnDate, worker.workDays)) {
99
+ throw new BadRequestException('Library is closed on this day');
100
+ }
101
+
102
+ const returnedBooks = await this.bookService.returnBooks(
103
+ dto.bookIds,
104
+ visitor,
105
+ );
106
+ await this.visitorService.moveToHistory(visitor.id, returnedBooks);
107
+
108
+ return { message: `Successfully returned ${returnedBooks.length} book(s)` };
109
+ }
110
+ }
src/book/book.module.ts ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Module } from '@nestjs/common';
2
+ import { BookController } from './book.controller';
3
+ import { BookService } from './book.service';
4
+ import { BookLinkManager } from 'src/common/book-link-manager';
5
+ import { VisitorModule } from '../visitor/visitor.module';
6
+ import { WorkerModule } from '../worker/worker.module';
7
+
8
+ @Module({
9
+ imports: [VisitorModule, WorkerModule],
10
+ controllers: [BookController],
11
+ providers: [BookService, BookLinkManager],
12
+ exports: [BookService, BookLinkManager],
13
+ })
14
+ export class BookModule {}
src/book/book.service.ts ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ Injectable,
3
+ NotFoundException,
4
+ BadRequestException,
5
+ } from '@nestjs/common';
6
+ import { Book } from './dto/book.dto';
7
+ import { FileManager } from '../utils/file-manager';
8
+ import { parseTxtRow, serializeTxtRow } from '../utils/file.utils';
9
+ import { randomUUID } from 'crypto';
10
+ import { BookStatus } from 'src/types/book.types';
11
+ import { CreateBookDto } from './dto/create-book.dto';
12
+ import { UpdateBookDto } from './dto/update-book.dto';
13
+ import { Visitor } from '../visitor/dto/visitor.dto';
14
+ import { bookLinkManager } from 'src/common/book-link-manager';
15
+
16
+ @Injectable()
17
+ export class BookService {
18
+ private file = new FileManager('src/data/books.txt');
19
+
20
+ async getAll(): Promise<Book[]> {
21
+ const lines = await this.file.readLines();
22
+ return lines.map(parseTxtRow) as unknown as Book[];
23
+ }
24
+
25
+ async getById(id: string): Promise<Book | null> {
26
+ const books = await this.getAll();
27
+ return books.find((b) => b.id === id) || null;
28
+ }
29
+
30
+ async add(dto: CreateBookDto): Promise<Book> {
31
+ const newBook: Book = {
32
+ id: randomUUID(),
33
+ ...dto,
34
+ status: BookStatus.AVAILABLE,
35
+ };
36
+ const lines = await this.file.readLines();
37
+ lines.push(await serializeTxtRow(newBook));
38
+ await this.file.writeLines(lines);
39
+ return newBook;
40
+ }
41
+
42
+ async update(id: string, dto: UpdateBookDto): Promise<Book | null> {
43
+ const books = await this.getAll();
44
+ const index = books.findIndex((b) => b.id === id);
45
+ if (index === -1) return null;
46
+ books[index] = { ...books[index], ...dto };
47
+ await this.file.writeLines(
48
+ await Promise.all(books.map((book) => serializeTxtRow(book))),
49
+ );
50
+ return books[index];
51
+ }
52
+
53
+ async delete(id: string): Promise<boolean> {
54
+ const books = await this.getAll();
55
+ const index = books.findIndex((b) => b.id === id);
56
+ if (index === -1) return false;
57
+ books.splice(index, 1);
58
+ await this.file.writeLines(
59
+ await Promise.all(books.map((book) => serializeTxtRow(book))),
60
+ );
61
+ return true;
62
+ }
63
+
64
+ async borrowBooks(bookIds: string[], visitor: Visitor): Promise<Book[]> {
65
+ const books = await this.getAll();
66
+ const borrowed: Book[] = [];
67
+
68
+ for (const bookId of bookIds) {
69
+ const book = books.find((b) => b.id === bookId);
70
+ if (!book)
71
+ throw new NotFoundException(`Book with id ${bookId} not found`);
72
+ if (book.status === BookStatus.BORROWED) {
73
+ throw new BadRequestException(
74
+ `Book "${book.title}" is already borrowed`,
75
+ );
76
+ }
77
+
78
+ book.status = BookStatus.BORROWED;
79
+ visitor.currentBooks.push(bookLinkManager.toLink(bookId));
80
+
81
+ borrowed.push(book);
82
+ }
83
+
84
+ await this.file.writeLines(
85
+ await Promise.all(books.map((book) => serializeTxtRow(book))),
86
+ );
87
+ return borrowed;
88
+ }
89
+
90
+ async returnBooks(bookIds: string[], visitor: Visitor): Promise<Book[]> {
91
+ const books = await this.getAll();
92
+ const returned: Book[] = [];
93
+
94
+ for (const bookId of bookIds) {
95
+ const book = books.find((b) => b.id === bookId);
96
+ if (!book)
97
+ throw new NotFoundException(`Book with id ${bookId} not found`);
98
+ if (book.status === BookStatus.AVAILABLE) {
99
+ throw new BadRequestException(`Book "${book.title}" is not borrowed`);
100
+ }
101
+
102
+ const currentBookIndex = visitor.currentBooks.findIndex(
103
+ (link) => link.id === bookId,
104
+ );
105
+
106
+ if (currentBookIndex === -1) {
107
+ throw new BadRequestException(
108
+ `Visitor does not have book "${book.title}"`,
109
+ );
110
+ }
111
+
112
+ book.status = BookStatus.AVAILABLE;
113
+ const returnedLink = visitor.currentBooks.splice(currentBookIndex, 1)[0];
114
+ visitor.history.push(returnedLink);
115
+ returned.push(book);
116
+ }
117
+
118
+ await this.file.writeLines(
119
+ await Promise.all(books.map((book) => serializeTxtRow(book))),
120
+ );
121
+ return returned;
122
+ }
123
+ }
src/book/dto/book.dto.ts ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ApiProperty } from '@nestjs/swagger';
2
+ import {
3
+ IsString,
4
+ IsInt,
5
+ MinLength,
6
+ MaxLength,
7
+ Min,
8
+ Max,
9
+ IsNotEmpty,
10
+ IsEnum,
11
+ } from 'class-validator';
12
+ import { BookStatus, Genre } from 'src/types/book.types';
13
+
14
+ export class Book {
15
+ @ApiProperty({
16
+ description: 'Уникальный идентификатор книги',
17
+ example: 'b1f3c2e7-9bd1-4f41-82b3-fc4c938f4a1d',
18
+ })
19
+ @IsString()
20
+ id: string;
21
+
22
+ @ApiProperty({
23
+ description: 'Название книги',
24
+ example: 'Pride and Prejudice',
25
+ })
26
+ @IsString()
27
+ @IsNotEmpty({ message: 'Title is required' })
28
+ @MinLength(2, { message: 'Title must be at least 2 characters' })
29
+ @MaxLength(100, { message: 'Title must be at most 100 characters' })
30
+ title: string;
31
+
32
+ @ApiProperty({
33
+ description: 'Автор книги',
34
+ example: 'Jane Austen',
35
+ })
36
+ @IsString()
37
+ @IsNotEmpty({ message: 'Author is required' })
38
+ @MinLength(2, { message: 'Author must be at least 2 characters' })
39
+ @MaxLength(100, { message: 'Author must be at most 100 characters' })
40
+ author: string;
41
+
42
+ @ApiProperty({
43
+ description: 'Количество страниц',
44
+ example: 279,
45
+ })
46
+ @IsInt({ message: 'Pages must be an integer' })
47
+ @Min(1, { message: 'Pages must be at least 1' })
48
+ @Max(10000, { message: 'Pages must be at most 10000' })
49
+ pages: number;
50
+
51
+ @ApiProperty({
52
+ description: 'Год публикации',
53
+ example: 1813,
54
+ })
55
+ @IsInt({ message: 'Year must be an integer' })
56
+ @Min(1000, { message: 'Year must be at least 1000' })
57
+ @Max(new Date().getFullYear(), {
58
+ message: `Year cannot be greater than ${new Date().getFullYear()}`,
59
+ })
60
+ year: number;
61
+
62
+ @ApiProperty({
63
+ description: 'Жанр книги',
64
+ enum: Genre,
65
+ example: Genre.Romance,
66
+ })
67
+ @IsEnum(Genre, { message: 'Invalid genre' })
68
+ genre: Genre;
69
+
70
+ @ApiProperty({
71
+ description: 'Статус книги',
72
+ enum: BookStatus,
73
+ example: BookStatus.AVAILABLE,
74
+ })
75
+ @IsEnum(BookStatus, { message: 'Invalid book status' })
76
+ status: BookStatus;
77
+ }
src/book/dto/borrow-book.dto.ts ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ApiProperty } from '@nestjs/swagger';
2
+ import {
3
+ IsArray,
4
+ IsString,
5
+ IsDateString,
6
+ ArrayNotEmpty,
7
+ } from 'class-validator';
8
+
9
+ export class BorrowBookDto {
10
+ @ApiProperty({
11
+ description: 'Список ID книг, которые выдаются',
12
+ example: ['book1', 'book2'],
13
+ })
14
+ @IsArray()
15
+ @ArrayNotEmpty({ message: 'bookIds cannot be empty' })
16
+ @IsString({ each: true })
17
+ bookIds: string[];
18
+
19
+ @ApiProperty({
20
+ description: 'ID посетителя',
21
+ example: 'visitor123',
22
+ })
23
+ @IsString()
24
+ visitorId: string;
25
+
26
+ @ApiProperty({
27
+ description: 'ID сотрудника, который выдает книги',
28
+ example: 'employee456',
29
+ })
30
+ @IsString()
31
+ workerId: string;
32
+
33
+ @ApiProperty({
34
+ description: 'Дата выдачи (ISO)',
35
+ example: '2025-11-25',
36
+ })
37
+ @IsDateString()
38
+ borrowDate: string;
39
+ }
src/book/dto/create-book.dto.ts ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ApiProperty } from '@nestjs/swagger';
2
+ import {
3
+ IsString,
4
+ IsInt,
5
+ MinLength,
6
+ MaxLength,
7
+ Min,
8
+ Max,
9
+ IsNotEmpty,
10
+ IsEnum,
11
+ } from 'class-validator';
12
+ import { BookStatus, Genre } from 'src/types/book.types';
13
+
14
+ export class CreateBookDto {
15
+ @ApiProperty({
16
+ description: 'Название книги',
17
+ example: 'Pride and Prejudice',
18
+ })
19
+ @IsString()
20
+ @IsNotEmpty({ message: 'Title is required' })
21
+ @MinLength(2)
22
+ @MaxLength(100)
23
+ title: string;
24
+
25
+ @ApiProperty({
26
+ description: 'Автор книги',
27
+ example: 'Jane Austen',
28
+ })
29
+ @IsString()
30
+ @IsNotEmpty({ message: 'Author is required' })
31
+ @MinLength(2)
32
+ @MaxLength(100)
33
+ author: string;
34
+
35
+ @ApiProperty({
36
+ description: 'Количество страниц',
37
+ example: 279,
38
+ })
39
+ @IsInt()
40
+ @Min(1)
41
+ @Max(10000)
42
+ pages: number;
43
+
44
+ @ApiProperty({
45
+ description: 'Год публикации',
46
+ example: 1813,
47
+ })
48
+ @IsInt()
49
+ @Min(1000)
50
+ @Max(new Date().getFullYear())
51
+ year: number;
52
+
53
+ @ApiProperty({
54
+ description: 'Жанр книги',
55
+ enum: Genre,
56
+ example: Genre.Romance,
57
+ })
58
+ @IsEnum(Genre)
59
+ genre: Genre;
60
+
61
+ @ApiProperty({
62
+ description: 'Статус книги',
63
+ enum: BookStatus,
64
+ example: BookStatus.AVAILABLE,
65
+ })
66
+ @IsEnum(BookStatus, { message: 'Invalid book status' })
67
+ status: BookStatus;
68
+ }
src/book/dto/return-book.dto.ts ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ApiProperty } from '@nestjs/swagger';
2
+ import {
3
+ IsArray,
4
+ IsString,
5
+ IsDateString,
6
+ ArrayNotEmpty,
7
+ } from 'class-validator';
8
+
9
+ export class ReturnBookDto {
10
+ @ApiProperty({
11
+ description: 'Список ID книг, которые возвращаются',
12
+ example: ['book1', 'book2'],
13
+ })
14
+ @IsArray()
15
+ @ArrayNotEmpty({ message: 'bookIds cannot be empty' })
16
+ @IsString({ each: true })
17
+ bookIds: string[];
18
+
19
+ @ApiProperty({
20
+ description: 'ID посетителя',
21
+ example: 'visitor123',
22
+ })
23
+ @IsString()
24
+ visitorId: string;
25
+
26
+ @ApiProperty({
27
+ description: 'ID сотрудника, который принимает книги',
28
+ example: 'employee456',
29
+ })
30
+ @IsString()
31
+ workerId: string;
32
+
33
+ @ApiProperty({
34
+ description: 'Дата возврата (ISO)',
35
+ example: '2025-11-25',
36
+ })
37
+ @IsDateString()
38
+ returnDate: string;
39
+ }
src/book/dto/update-book.dto.ts ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ import { PartialType } from '@nestjs/swagger';
2
+ import { CreateBookDto } from './create-book.dto';
3
+
4
+ export class UpdateBookDto extends PartialType(CreateBookDto) {}
src/common/Link.ts ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export class Link {
2
+ constructor(
3
+ public table: string,
4
+ public id: string,
5
+ ) {}
6
+
7
+ static fromString(str: string): Link {
8
+ const [table, id] = str.split(':');
9
+ return new Link(table, id);
10
+ }
11
+
12
+ toString(): string {
13
+ return `${this.table}:${this.id}`;
14
+ }
15
+ }
src/common/LinkManager.ts ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
2
+ import { FileManager } from '../utils/file-manager';
3
+ import { parseTxtRow } from '../utils/file.utils';
4
+ import { Link } from './Link';
5
+
6
+ export abstract class LinkManager<T> {
7
+ protected abstract fileName: string;
8
+ protected abstract tableName: string;
9
+
10
+ private getFile() {
11
+ return new FileManager(`src/data/${this.fileName}`);
12
+ }
13
+
14
+ toLink(id: string): Link {
15
+ return new Link(this.tableName, id);
16
+ }
17
+
18
+ async resolve(link: Link): Promise<T | null> {
19
+ if (link.table !== this.tableName)
20
+ throw new Error(`Expected link to ${this.tableName}, got ${link.table}`);
21
+
22
+ const file = this.getFile();
23
+ const rows = await file.readLines();
24
+ const items = rows.map(parseTxtRow) as T[];
25
+
26
+ return items.find((i: any) => i.id === link.id) || null;
27
+ }
28
+
29
+ async resolveMany(links: Link[]): Promise<T[]> {
30
+ const resolved = await Promise.all(links.map((l) => this.resolve(l)));
31
+ return resolved.filter((i) => i !== null) as T[];
32
+ }
33
+ }
src/common/book-link-manager.ts ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import { LinkManager } from './LinkManager';
2
+ import { Book } from '../book/dto/book.dto';
3
+
4
+ export class BookLinkManager extends LinkManager<Book> {
5
+ protected fileName = 'books.txt';
6
+ protected tableName = 'books';
7
+ }
8
+
9
+ export const bookLinkManager = new BookLinkManager();
src/common/key-manager.ts ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { promises as fs } from 'fs';
2
+ import path from 'path';
3
+ import {
4
+ decryptWithAlphabet,
5
+ encryptWithAlphabet,
6
+ } from 'src/utils/offset-encryption.util';
7
+
8
+ const dataDir = path.join(process.cwd(), 'src/data/encrypted/');
9
+ const keyFile = path.join(dataDir, '.encryption_key');
10
+
11
+ const MASTER_KEY = process.env.MASTER_KEY || 'RSIOT_SECRET_KEY_VARIAHT1';
12
+
13
+ export async function ensureDataDir(): Promise<void> {
14
+ try {
15
+ await fs.access(dataDir);
16
+ } catch {
17
+ await fs.mkdir(dataDir, { recursive: true });
18
+ }
19
+ }
20
+
21
+ export async function getOrCreateKey(): Promise<string> {
22
+ await ensureDataDir();
23
+
24
+ try {
25
+ const encryptedKey = await fs.readFile(keyFile, 'utf-8');
26
+ return decryptWithAlphabet(encryptedKey, MASTER_KEY);
27
+ } catch (err: unknown) {
28
+ if (err instanceof Error && err.message.includes('ENOENT')) {
29
+ const newKey = generateRandomKey(16);
30
+ await saveKey(newKey);
31
+ return newKey;
32
+ }
33
+ throw err;
34
+ }
35
+ }
36
+
37
+ export async function saveKey(key: string): Promise<void> {
38
+ await ensureDataDir();
39
+ const encryptedKey = encryptWithAlphabet(key, MASTER_KEY);
40
+ await fs.writeFile(keyFile, encryptedKey, 'utf-8');
41
+ }
42
+
43
+ export function generateRandomKey(length: number): string {
44
+ const chars =
45
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*';
46
+ let key = '';
47
+ for (let i = 0; i < length; i++) {
48
+ key += chars.charAt(Math.floor(Math.random() * chars.length));
49
+ }
50
+ return key;
51
+ }
src/common/visitor-link-manager.ts ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { LinkManager } from './LinkManager';
2
+ import { Visitor } from '../visitor/dto/visitor.dto';
3
+ import { bookLinkManager } from './book-link-manager';
4
+ import { Link } from './Link';
5
+
6
+ export interface VisitorWithBooks
7
+ extends Omit<Visitor, 'currentBooks' | 'history'> {
8
+ currentBooks: any[];
9
+ history: any[];
10
+ }
11
+
12
+ export class VisitorLinkManager extends LinkManager<Visitor> {
13
+ protected fileName = 'visitors.txt';
14
+ protected tableName = 'visitors';
15
+
16
+ async enrich(visitor: Visitor): Promise<VisitorWithBooks> {
17
+ const currentBooks = await bookLinkManager.resolveMany(
18
+ visitor.currentBooks as unknown as Link[],
19
+ );
20
+
21
+ const history = await bookLinkManager.resolveMany(
22
+ visitor.history as unknown as Link[],
23
+ );
24
+
25
+ return {
26
+ ...visitor,
27
+ currentBooks,
28
+ history,
29
+ };
30
+ }
31
+ }
32
+
33
+ export const visitorLinkManager = new VisitorLinkManager();
src/common/worker-link-manager.ts ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Worker } from 'src/worker/dto/worker.dto';
2
+ import { bookLinkManager } from './book-link-manager';
3
+ import { LinkManager } from './LinkManager';
4
+ import { Link } from './Link';
5
+ import { Book } from 'src/book/dto/book.dto';
6
+
7
+ export interface WorkerWithBooks extends Omit<Worker, 'issuedBooks'> {
8
+ issuedBooks: Book[];
9
+ }
10
+
11
+ export class WorkerLinkManager extends LinkManager<Worker> {
12
+ protected fileName = 'workers.txt';
13
+ protected tableName = 'workers';
14
+
15
+ async enrich(worker: Worker): Promise<WorkerWithBooks> {
16
+ const issuedBooks = await bookLinkManager.resolveMany(
17
+ worker.issuedBooks as unknown as Link[],
18
+ );
19
+
20
+ return {
21
+ ...worker,
22
+ issuedBooks,
23
+ };
24
+ }
25
+ }
26
+
27
+ export const workerLinkManager = new WorkerLinkManager();
src/config/swagger.config.ts ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import { DocumentBuilder } from '@nestjs/swagger';
2
+
3
+ export function getSwaggerConfig() {
4
+ return new DocumentBuilder()
5
+ .setTitle('Library API')
6
+ .setDescription('Library API Documentation')
7
+ .addBearerAuth()
8
+ .build();
9
+ }
src/data/books.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ id=ccb1b578-8343-45de-a465-007af8af7c28;title=Pride and Prejudice;author=Jane Austen;pages=279;year=1813;genre=Romance;status=Borrowed
2
+ id=9ac18f0d-8bf7-4dfd-aa20-cc9b1b66e610;title=Pride and Prejudice;author=Jane Austen;pages=279;year=1813;genre=Romance;status=Borrowed
3
+ id=84d1af3f-db4e-4331-b94d-e846b0adeb06;title=Pride and Prejudice;author=Jane Austen;pages=279;year=1813;genre=Romance;status=Available
4
+ id=088e4a3d-cb61-4894-a9f0-b62ed9bd4cc1;title=Pride and Prejudice;author=Jane Austen;pages=279;year=1813;genre=Romance;status=Available
5
+ id=aba6d1d3-2c1e-4b14-948d-a370f0699da7;title=Pride and Prejudice;author=Jane Austen;pages=279;year=1813;genre=Romance;status=Available
6
+ id=583f1c35-2f17-4006-9919-ce292cf6803a;title=Pride and Prejudice;author=Jane Austen;pages=279;year=1813;genre=Romance;status=Available
7
+ id=25d250e9-c6ad-44e0-894a-bb5507da6971;title=Pride and Prejudice;author=Jane Austen;pages=279;year=1813;genre=Romance;status=Available
8
+ 7p*&qcw[2X?e1,u^7q*&qcw[2X!e2.u&6p &qcw[2X!d1,v&7p *qcw[2X!d1.v&6q *rcw[2X!d1.u&7p *rdw[2X!d1.u&7p*&qdx[2X!d1.u^7q*&qcx]2X!d1.u^6q &qdx[3X!d1.u^6p**rcw[2Y!d1.u^6p*&rdw[2X?d1.u^6p*&rdw[2Y?e1.u^6p*&qcx[3Y!e2.u^6p*&qcw]3X?e2,u^6p*&qcw[3Y!d2,v^6p*&qcw[2Y?d2,u&6p*&qcw[2Y?d1,u&7p*&qcw[2X!e1,v^7q*&qcw[2X!d2,u&6p &qcw[2X!d2,u^7p**qcw[2X!d1.v&7p*&rcw[2X!d1.u&7p**qdw[2X!d1.u^7p *qdx[2X!d1.u^7q*&qdw]2X!d1.u^6p *qdw]3X!d1.u^6p**rcx[2Y!d1.u^6p**rcw]3X?d1.u^6p*&qdw]3X?e1.u^6p*&qcx]2X!e2.u^6p*&qcw]3X!d2,u^6p*&qcw[3Y!e1,v^6p*&qcw[2Y?d2.v&6p*&qcw[2X?e1.v^7p*&qcw[2X!e2,u^6q*&qcw[2X!d2,u&6q &qcw[2X!d1,v^6p *qcw[2X!d1,v^6p**rcw[2X!d1.u&7q*&rdw[2X!d1.u^7q*&rdx[2X!d1.u^6q &qcw]2X!d1.u^6p *rcx]3X!d1.u^6p *rcx[2Y!d1.u^6p**rcx[2Y?d1.u^6p*&rdx[3X!e1.u^6p*&qdx[3Y!d2.u^6p*&qcx]2X?d2,u^6p*&qcw[3Y?e1,v^6p*&qcw[3X?d1.u&6p*&qcw[2Y?e1.v^7p*&qcw[2X?e1,u^7q*&qcw[2X!e2.u&6p &qcw[2X!d2,u^7p *qcw[2X!d1.v^6p*&rcw[2X!d1.v&6p*&rdw[2X!d1.u&7p *rcx[2X!d1.u^7q*&rcw]2X!d1.u^6p &qcw[3X!d1.u^6p &rcw[2Y!d1.u^6p**rdw[3X?d1.u^6p*&rdw[3X?e1.u^6p*&qdx[3X?d2.u^6p*&qcx]3X?d2,u^6p*&qcw]3X!e1.v^6p*&qcw[3Y!e1.v&6p*&qcw[2Y?d1.v&7p*&qcw[2X?e1.v^7q*&qcw[2X!d2,v^7q &qcw[2X!d2,u^6p *qcw[2X!d1,v&6q**rcw[2X!d1.v&7p &qdw[2X!d1.u&7p &qcx[2X!d1.u^7q**rdx]2X!d1.u^6q *qcx[3X!d1.u^6p**rdx[3Y!d1.u^6p**qcx[3X?d1.u^6p*&rdw[2X?e1.u^6p*&qdx[3Y?d2.u^6p*&qcx]2X?d2,u^6p*&qcw[3X!d1.v^6p*&qcw[3X!d1.v&6p*&qcw[2Y?e1,u&7p*&qcw[2X?e2.u&7q*&qcw[2X!e2,u&6p &qcw[2X!d2,u^7p *qcw[2X!d1,v^7q &rcw[2X!d1.u&7q**rdw[2X!d1.u&7q*&qcx[2X!d1.u^7q*&qcx]2X!d1.u^6q &qdx]3X!d1.u^6p *qcx[3Y!d1.u^6p**rdw[3Y?d1.u^6p*&qdx]3X?e1.u^6p*&qcx]2X?d2.u^6p*&qcw]3X?e2,u^6p*&qcw[3Y?d1,v^6p*&qcw[2Y?e1,v&6p*&qcw[2Y?e2.u&7p*&qcw[2X?e1.v^7q*&qcw[2X!e2.u^6q &qcw[2X!d2,v^6q**qcw[2X!d1.v&7q**rcw[2X!d1.u&7p*&rdw[2X!d1.u^7q &qcx[2X!d1.u^6q &qcx]2X!d1.u^6p *qcx]3X!d1.u^6p**rdw]3Y!d1.u^6p**rcw]3Y?d1.u^6p*&rdw[3X?e1.u^6p*&qdx[3Y?d2.u^6p*&qcx]3X!e1,u^6p*&qcw]3X!e1,v^6p*&qcw[2Y?e2.v&6p*&qcw[2Y!e1.v^7p*&qcw[2X?e1,v&7q*&qcw[2X!e2.v&6q &qcw[2X!d2,u^6p *qcw[2X!d1,v^7q &qcw[2X!e2.v^7p*&qcw]2X!d1.u^6q *qcx]3X!d1.u^6p *rcx[2Y!d1.u^6p**rcw[2Y?d1.u^6p*&rdx[3X!e1.u^6p*&qdx]2Y!e2.u^6p*&qcx]3X!e2,u^6p*&qcw[3Y?e1,v^6p*&qcw[3X!d1.v&6p*&qcw[2Y?e1,v^7p*&qcw[2X?e1.u^7q*&qcw[2X!e2.v^6q &qcw[2X!d2,u&7p**qcw[2X!d1,v^6p**rcw[2X!d1.v&6p**qdw[2X!d1.u&7p *qcx[2X!d1.u^7q*&rcx
src/data/encrypted/.encryption_key ADDED
@@ -0,0 +1 @@
 
 
1
+ Xh(}jDoYEOU^/U*$
src/data/visitors.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ id=10441103-8fc4-4975-8b6b-c26f87dcbc7c;name=John;surname=Doe;registrationDate="2025-12-15T11:50:53.837Z";currentBooks=["books:ccb1b578-8343-45de-a465-007af8af7c28","books:9ac18f0d-8bf7-4dfd-aa20-cc9b1b66e610"];history=[]
src/data/workers.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ id=77c37ac4-101c-47a5-a064-b4927e21fc8c;name=John;surname=Doe;experience=5;workDays=["Monday","Wednesday","Friday"];issuedBooks=["books:ccb1b578-8343-45de-a465-007af8af7c28","books:9ac18f0d-8bf7-4dfd-aa20-cc9b1b66e610"]
src/filters/http-exception.filter.ts ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ ExceptionFilter,
3
+ Catch,
4
+ ArgumentsHost,
5
+ HttpException,
6
+ HttpStatus,
7
+ } from '@nestjs/common';
8
+ import { Response } from 'express';
9
+ import { ApiResponse, ErrorResponse } from 'src/utils/response-wrapper.utils';
10
+
11
+ @Catch()
12
+ export class HttpExceptionFilter implements ExceptionFilter {
13
+ catch(exception: unknown, host: ArgumentsHost) {
14
+ const ctx = host.switchToHttp();
15
+ const response = ctx.getResponse<Response>();
16
+
17
+ let status = HttpStatus.INTERNAL_SERVER_ERROR;
18
+ let message = 'Internal server error';
19
+ let code: string | undefined;
20
+
21
+ // Обработка HttpException (NotFoundException, BadRequestException и т.д.)
22
+ if (exception instanceof HttpException) {
23
+ status = exception.getStatus();
24
+ const exceptionResponse = exception.getResponse();
25
+
26
+ if (typeof exceptionResponse === 'string') {
27
+ message = exceptionResponse;
28
+ } else if (
29
+ typeof exceptionResponse === 'object' &&
30
+ exceptionResponse !== null
31
+ ) {
32
+ const response = exceptionResponse as ErrorResponse;
33
+
34
+ if (Array.isArray(response.message)) {
35
+ message = response.message.join(', ');
36
+ } else {
37
+ message = response.message || message;
38
+ }
39
+
40
+ code = response.code;
41
+ }
42
+ } else if (exception instanceof Error) {
43
+ message = exception.message;
44
+ }
45
+
46
+ const errorResponse: ApiResponse<void> = {
47
+ data: null,
48
+ successful: false,
49
+ error: {
50
+ message,
51
+ statusCode: status,
52
+ code,
53
+ },
54
+ };
55
+
56
+ response.status(status).json(errorResponse);
57
+ }
58
+ }
src/interceptors/response.interceptor.ts ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ Injectable,
3
+ NestInterceptor,
4
+ ExecutionContext,
5
+ CallHandler,
6
+ StreamableFile,
7
+ } from '@nestjs/common';
8
+ import { Response } from 'express';
9
+ import { Observable } from 'rxjs';
10
+ import { map } from 'rxjs/operators';
11
+ import { ApiResponse } from '../utils/response-wrapper.utils';
12
+
13
+ @Injectable()
14
+ export class ResponseInterceptor<T>
15
+ implements NestInterceptor<T, ApiResponse<T> | T>
16
+ {
17
+ intercept(
18
+ context: ExecutionContext,
19
+ next: CallHandler,
20
+ ): Observable<ApiResponse<T> | T> {
21
+ const response = context.switchToHttp().getResponse<Response>();
22
+ return next.handle().pipe(
23
+ map((data: T) => {
24
+ if (data instanceof StreamableFile) return data;
25
+ if (response.headersSent) return data;
26
+ return {
27
+ data,
28
+ successful: true,
29
+ };
30
+ }),
31
+ );
32
+ }
33
+ }
src/main.ts ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NestFactory } from '@nestjs/core';
2
+ import { AppModule } from './app.module';
3
+ import { setupSwagger } from './utils/swagger.utils';
4
+ import { ValidationPipe } from '@nestjs/common';
5
+ import { ResponseInterceptor } from './interceptors/response.interceptor';
6
+ import { HttpExceptionFilter } from './filters/http-exception.filter';
7
+
8
+ async function bootstrap() {
9
+ const app = await NestFactory.create(AppModule);
10
+ setupSwagger(app);
11
+ app.useGlobalPipes(
12
+ new ValidationPipe({
13
+ whitelist: true,
14
+ forbidNonWhitelisted: true,
15
+ transform: true,
16
+ }),
17
+ );
18
+ app.enableCors({
19
+ origin: 'http://localhost:5173',
20
+ credentials: true,
21
+ });
22
+ app.useGlobalInterceptors(new ResponseInterceptor());
23
+ app.useGlobalFilters(new HttpExceptionFilter());
24
+ await app.listen(process.env.PORT ?? 3000);
25
+ }
26
+ bootstrap();
src/types/book.types.ts ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export enum Genre {
2
+ Fiction = 'Fiction',
3
+ NonFiction = 'Non-Fiction',
4
+ Mystery = 'Mystery',
5
+ SciFi = 'Sci-Fi',
6
+ Fantasy = 'Fantasy',
7
+ Biography = 'Biography',
8
+ History = 'History',
9
+ Romance = 'Romance',
10
+ Thriller = 'Thriller',
11
+ Horror = 'Horror',
12
+ Poetry = 'Poetry',
13
+ Drama = 'Drama',
14
+ Comics = 'Comics',
15
+ Other = 'Other',
16
+ }
17
+
18
+ export enum BookStatus {
19
+ AVAILABLE = 'Available',
20
+ BORROWED = 'Borrowed',
21
+ }
22
+
23
+ export interface Book {
24
+ id: string;
25
+ title: string;
26
+ author: string;
27
+ pages: number;
28
+ year: number;
29
+ genre: Genre;
30
+ status: BookStatus;
31
+ }
32
+
33
+ export interface CreateBookRequest {
34
+ title: string;
35
+ author: string;
36
+ pages: number;
37
+ year: number;
38
+ genre: Genre;
39
+ }
40
+
41
+ export interface BorrowBookRequest {
42
+ bookIds: string[];
43
+ visitorId: string;
44
+ employeeId: string;
45
+ borrowDate: string;
46
+ }
47
+
48
+ export interface ReturnBookRequest {
49
+ bookIds: string[];
50
+ visitorId: string;
51
+ employeeId: string;
52
+ returnDate: string;
53
+ }
src/types/worker.types.ts ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export enum DayOfWeek {
2
+ MONDAY = 'Monday',
3
+ TUESDAY = 'Tuesday',
4
+ WEDNESDAY = 'Wednesday',
5
+ THURSDAY = 'Thursday',
6
+ FRIDAY = 'Friday',
7
+ SATURDAY = 'Saturday',
8
+ SUNDAY = 'Sunday',
9
+ }
10
+
11
+ export interface Employee {
12
+ id: string;
13
+ name: string;
14
+ surname: string;
15
+ experience: number;
16
+ workDays: DayOfWeek[];
17
+ issuedBooks?: string[];
18
+ }
19
+
20
+ export interface CreateEmployeeRequest {
21
+ name: string;
22
+ surname: string;
23
+ experience: number;
24
+ workDays: DayOfWeek[];
25
+ }
src/utils/crypto.utils.ts ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { lzssEncode, lzssDecode } from './lzss.util';
2
+ import {
3
+ encryptWithAlphabet,
4
+ decryptWithAlphabet,
5
+ } from './offset-encryption.util';
6
+ import { getOrCreateKey } from 'src/common/key-manager';
7
+
8
+ export async function encryptData(data: string): Promise<string> {
9
+ const compressed = lzssEncode(data);
10
+ const key = await getOrCreateKey();
11
+ const encrypted = encryptWithAlphabet(compressed, key);
12
+
13
+ return encrypted;
14
+ }
15
+
16
+ export async function decryptData(encrypted: string): Promise<string> {
17
+ const key = await getOrCreateKey();
18
+ const decrypted = decryptWithAlphabet(encrypted, key);
19
+ const decompressed = lzssDecode(decrypted);
20
+ console.log('decompressed', decompressed);
21
+
22
+ return decompressed;
23
+ }
src/utils/date.utils.ts ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export class DateUtils {
2
+ private static readonly DAY_NAMES = [
3
+ 'Sunday',
4
+ 'Monday',
5
+ 'Tuesday',
6
+ 'Wednesday',
7
+ 'Thursday',
8
+ 'Friday',
9
+ 'Saturday',
10
+ ] as const;
11
+
12
+ private static readonly DAY_MAP = Object.fromEntries(
13
+ DateUtils.DAY_NAMES.map((day, index) => [index, day]),
14
+ );
15
+
16
+ public static getDayName(date: Date | string): string {
17
+ const dateObj = typeof date === 'string' ? new Date(date) : date;
18
+ const dayOfWeek = dateObj.getDay();
19
+ return this.DAY_MAP[dayOfWeek];
20
+ }
21
+
22
+ public static isWorkingDay(date: Date | string, workDays: string[]): boolean {
23
+ const dayName = this.getDayName(date);
24
+ return workDays.includes(dayName);
25
+ }
26
+ }
src/utils/download.utils.ts ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { StreamableFile } from '@nestjs/common';
2
+
3
+ export function buildDownloadFile(
4
+ table: string,
5
+ id: string,
6
+ payload: unknown,
7
+ ): StreamableFile {
8
+ const content = JSON.stringify(payload, null, 2);
9
+ const buffer = Buffer.from(content, 'utf-8');
10
+ const safeTable = table.replace(/[^a-z0-9_-]/gi, '') || 'item';
11
+ const safeId = id.replace(/[^a-z0-9_-]/gi, '') || 'data';
12
+ return new StreamableFile(buffer, {
13
+ type: 'text/plain',
14
+ disposition: `attachment; filename="${safeTable}-${safeId}.txt"`,
15
+ });
16
+ }
src/utils/file-manager.ts ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as fs from 'fs/promises';
2
+ import * as path from 'path';
3
+ // import { CryptoProvider } from './crypto.utils';
4
+
5
+ export class FileManager {
6
+ constructor(
7
+ private readonly filePath: string,
8
+ // private readonly crypto = new CryptoProvider(),
9
+ ) {}
10
+
11
+ private async ensureFile(): Promise<void> {
12
+ const dir = path.dirname(this.filePath);
13
+
14
+ try {
15
+ await fs.stat(dir);
16
+ } catch {
17
+ await fs.mkdir(dir, { recursive: true });
18
+ }
19
+
20
+ try {
21
+ await fs.stat(this.filePath);
22
+ } catch {
23
+ await fs.writeFile(this.filePath, '', 'utf-8');
24
+ }
25
+ }
26
+
27
+ async readLines(): Promise<string[]> {
28
+ await this.ensureFile();
29
+ const data = await fs.readFile(this.filePath, 'utf-8');
30
+
31
+ if (!data) return [];
32
+
33
+ try {
34
+ // const decrypted = await this.crypto.decrypt(data);
35
+ return data
36
+ .split(/\r?\n/)
37
+ .map((line: string) => line.trim())
38
+ .filter(Boolean);
39
+ } catch {
40
+ return data
41
+ .split(/\r?\n/)
42
+ .map((line: string) => line.trim())
43
+ .filter(Boolean);
44
+ }
45
+ }
46
+
47
+ async writeLines(lines: string[]): Promise<void> {
48
+ await this.ensureFile();
49
+ const plain = lines.join('\n');
50
+ // const encrypted = await this.crypto.encrypt(plain);
51
+ await fs.writeFile(this.filePath, plain, 'utf-8');
52
+ }
53
+ }
src/utils/file.utils.ts ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
2
+ import { Link } from '../common/Link';
3
+ import { decryptData, encryptData } from './crypto.utils';
4
+
5
+ function isLinkString(value: string | undefined) {
6
+ if (!value) return false;
7
+ return /^[a-zA-Z]+:[^:]+$/.test(value);
8
+ }
9
+
10
+ function parseTxtRowInternal(row: string): any {
11
+ if (typeof row !== 'string') {
12
+ throw new Error('parseTxtRow: row is not string: ' + JSON.stringify(row));
13
+ }
14
+
15
+ const parts = row.split(';');
16
+ const obj: any = {};
17
+
18
+ for (const part of parts) {
19
+ const [key, rawValue] = part.split('=');
20
+ const value = rawValue?.trim();
21
+
22
+ if (!key) continue;
23
+
24
+ if (value?.startsWith('[') && value.includes(':')) {
25
+ const arr = JSON.parse(value) as string[];
26
+ obj[key] = arr.map((s: string) =>
27
+ isLinkString(s) ? Link.fromString(s) : s,
28
+ );
29
+ continue;
30
+ }
31
+ if (isLinkString(value)) {
32
+ obj[key] = Link.fromString(value);
33
+ continue;
34
+ }
35
+
36
+ if (value === '[]') {
37
+ obj[key] = [];
38
+ continue;
39
+ }
40
+
41
+ if (value?.startsWith('[') || value?.startsWith('{')) {
42
+ try {
43
+ obj[key] = JSON.parse(value) as unknown;
44
+ continue;
45
+ } catch {
46
+ obj[key] = value;
47
+ }
48
+ }
49
+
50
+ obj[key] = value;
51
+ }
52
+
53
+ return obj;
54
+ }
55
+
56
+ function serializeTxtRowInternal(obj: any): string {
57
+ return Object.entries(obj)
58
+ .map(([key, value]) => {
59
+ if (value instanceof Link) {
60
+ return `${key}=${value.toString()}`;
61
+ }
62
+
63
+ if (Array.isArray(value) && value.every((v) => v instanceof Link)) {
64
+ return `${key}=${JSON.stringify(value.map((v) => v.toString()))}`;
65
+ }
66
+
67
+ if (Array.isArray(value) && value.length === 0) {
68
+ return `${key}=[]`;
69
+ }
70
+
71
+ if (typeof value === 'object') {
72
+ return `${key}=${JSON.stringify(value)}`;
73
+ }
74
+
75
+ return `${key}=${value as string}`;
76
+ })
77
+ .join(';');
78
+ }
79
+
80
+ export async function parseTxtRow(encryptedRow: string): Promise<any> {
81
+ const decrypted = await decryptData(encryptedRow);
82
+ return parseTxtRowInternal(decrypted);
83
+ }
84
+
85
+ export async function serializeTxtRow(obj: any): Promise<string> {
86
+ const serialized = serializeTxtRowInternal(obj);
87
+ return await encryptData(serialized);
88
+ }
src/utils/lzss.util.ts ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface LZSSMatch {
2
+ offset: number;
3
+ length: number;
4
+ }
5
+
6
+ export interface LZSSToken {
7
+ type: 'literal' | 'match';
8
+ value: string | LZSSMatch;
9
+ }
10
+
11
+ // Параметры LZSS
12
+ const WINDOW_SIZE = 4096; // Размер окна поиска (12 бит)
13
+ const LOOKAHEAD_SIZE = 18; // Размер буфера упреждения
14
+ const MIN_MATCH_LENGTH = 3; // Минимальная длина совпадения
15
+
16
+ export function lzssEncode(text: string): string {
17
+ if (!text) return '';
18
+
19
+ const tokens: LZSSToken[] = [];
20
+ let pos = 0;
21
+
22
+ while (pos < text.length) {
23
+ let bestMatch: LZSSMatch | null = null;
24
+ let bestLength = 0;
25
+
26
+ // Определяем начало окна поиска
27
+ const windowStart = Math.max(0, pos - WINDOW_SIZE);
28
+ const lookaheadEnd = Math.min(text.length, pos + LOOKAHEAD_SIZE);
29
+
30
+ // Ищем самое длинное совпадение в окне
31
+ for (let i = windowStart; i < pos; i++) {
32
+ let matchLength = 0;
33
+
34
+ // Считаем длину совпадения
35
+ while (
36
+ pos + matchLength < lookaheadEnd &&
37
+ text[i + matchLength] === text[pos + matchLength]
38
+ ) {
39
+ matchLength++;
40
+ }
41
+
42
+ // Если найдено лучшее совпадение
43
+ if (matchLength >= MIN_MATCH_LENGTH && matchLength > bestLength) {
44
+ bestLength = matchLength;
45
+ bestMatch = {
46
+ offset: pos - i,
47
+ length: matchLength,
48
+ };
49
+ }
50
+ }
51
+
52
+ if (bestMatch) {
53
+ tokens.push({
54
+ type: 'match',
55
+ value: bestMatch,
56
+ });
57
+ pos += bestMatch.length;
58
+ } else {
59
+ tokens.push({
60
+ type: 'literal',
61
+ value: text[pos],
62
+ });
63
+ pos++;
64
+ }
65
+ }
66
+
67
+ // Кодируем токены в бинарную строку
68
+ return encodeTokens(tokens);
69
+ }
70
+
71
+ function encodeTokens(tokens: LZSSToken[]): string {
72
+ let result = '';
73
+
74
+ for (const token of tokens) {
75
+ if (token.type === 'literal') {
76
+ // Флаг 1 означает литерал, затем 8 бит символа
77
+ result += '1';
78
+ const char = token.value as string;
79
+ const charCode = char.charCodeAt(0);
80
+ result += charCode.toString(2).padStart(16, '0'); // 16 бит для Unicode
81
+ } else {
82
+ // Флаг 0 означает совпадение
83
+ result += '0';
84
+ const match = token.value as LZSSMatch;
85
+ // 12 бит для смещения, 6 бит для длины (максимум 18+3=21)
86
+ result += match.offset.toString(2).padStart(12, '0');
87
+ result += (match.length - MIN_MATCH_LENGTH).toString(2).padStart(6, '0');
88
+ }
89
+ }
90
+
91
+ return result;
92
+ }
93
+
94
+ export function lzssDecode(encoded: string): string {
95
+ if (!encoded) return '';
96
+
97
+ let result = '';
98
+ let pos = 0;
99
+
100
+ while (pos < encoded.length) {
101
+ const flag = encoded[pos];
102
+ pos++;
103
+
104
+ if (flag === '1') {
105
+ // Литерал: читаем 16 бит
106
+ if (pos + 16 > encoded.length) {
107
+ console.error('Неполный литерал на позиции', pos);
108
+ break;
109
+ }
110
+ const charCode = parseInt(encoded.substring(pos, pos + 16), 2);
111
+ result += String.fromCharCode(charCode);
112
+ pos += 16;
113
+ } else if (flag === '0') {
114
+ // Совпадение: читаем 12 бит смещения и 6 бит длины
115
+ if (pos + 18 > encoded.length) {
116
+ console.error('Неполное совпадение на позиции', pos);
117
+ break;
118
+ }
119
+
120
+ const offset = parseInt(encoded.substring(pos, pos + 12), 2);
121
+ pos += 12;
122
+ const length =
123
+ parseInt(encoded.substring(pos, pos + 6), 2) + MIN_MATCH_LENGTH;
124
+ pos += 6;
125
+
126
+ // Копируем из уже декодированной части
127
+ const startPos = result.length - offset;
128
+
129
+ if (startPos < 0) {
130
+ console.error(
131
+ `Некорректное смещение: ${offset} на позиции результата ${result.length}`,
132
+ );
133
+ break;
134
+ }
135
+
136
+ for (let i = 0; i < length; i++) {
137
+ result += result[startPos + i];
138
+ }
139
+ } else {
140
+ console.error('Неизвестный флаг:', flag);
141
+ break;
142
+ }
143
+ }
144
+
145
+ return result;
146
+ }
147
+
148
+ // Функция для статистики сжатия
149
+ export function getLZSSStats(
150
+ original: string,
151
+ encoded: string,
152
+ ): {
153
+ originalSize: number;
154
+ compressedSize: number;
155
+ compressionRatio: number;
156
+ } {
157
+ const originalSize = original.length * 16; // 16 бит на символ (UTF-16)
158
+ const compressedSize = encoded.length;
159
+
160
+ return {
161
+ originalSize,
162
+ compressedSize,
163
+ compressionRatio: compressedSize / originalSize,
164
+ };
165
+ }
src/utils/offset-encryption.util.ts ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const CUSTOM_ALPHABET =
2
+ 'abcdefghijklmnopqrstuvwxyz' +
3
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' +
4
+ '0123456789' +
5
+ '.,;:=|!?()[]{}"\'-_/\\@#$%^&* ';
6
+
7
+ const alphabetMap = new Map<string, number>();
8
+
9
+ for (let i = 0; i < CUSTOM_ALPHABET.length; i++) {
10
+ alphabetMap.set(CUSTOM_ALPHABET[i], i + 1);
11
+ }
12
+
13
+ function getPos(char: string): number {
14
+ const pos = alphabetMap.get(char);
15
+ if (!pos) throw new Error(`Character "${char}" is missing in the alphabet`);
16
+ return pos;
17
+ }
18
+
19
+ function getChar(pos: number): string {
20
+ return CUSTOM_ALPHABET[pos - 1];
21
+ }
22
+
23
+ export function encryptWithAlphabet(text: string, key: string): string {
24
+ let result = '';
25
+ const N = CUSTOM_ALPHABET.length;
26
+
27
+ for (let i = 0; i < text.length; i++) {
28
+ const tPos = getPos(text[i]);
29
+ const kPos = getPos(key[i % key.length]);
30
+
31
+ let encPos = tPos + kPos;
32
+ if (encPos > N) encPos -= N;
33
+
34
+ result += getChar(encPos);
35
+ }
36
+
37
+ return result;
38
+ }
39
+
40
+ export function decryptWithAlphabet(encrypted: string, key: string): string {
41
+ let result = '';
42
+ const N = CUSTOM_ALPHABET.length;
43
+
44
+ for (let i = 0; i < encrypted.length; i++) {
45
+ const ePos = getPos(encrypted[i]);
46
+ const kPos = getPos(key[i % key.length]);
47
+
48
+ let decPos = ePos - kPos;
49
+ if (decPos <= 0) decPos += N;
50
+
51
+ result += getChar(decPos);
52
+ }
53
+
54
+ return result;
55
+ }
src/utils/response-wrapper.utils.ts ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface ErrorResponse {
2
+ message: string;
3
+ code?: string;
4
+ statusCode?: number;
5
+ }
6
+
7
+ export interface ApiResponse<T> {
8
+ data?: T | null;
9
+ successful: boolean;
10
+ error?: ErrorResponse | null;
11
+ }
src/utils/swagger.utils.ts ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
2
+ import type { INestApplication } from '@nestjs/common';
3
+ import { SwaggerModule } from '@nestjs/swagger';
4
+ import { getSwaggerConfig } from 'src/config/swagger.config';
5
+
6
+ export function setupSwagger(app: INestApplication) {
7
+ const config = getSwaggerConfig();
8
+ const document = SwaggerModule.createDocument(app, config);
9
+ SwaggerModule.setup('/api/docs', app, document);
10
+ }
src/visitor/dto/create-visitor.dto.ts ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ApiProperty } from '@nestjs/swagger';
2
+ import { IsNotEmpty, IsString, Length } from 'class-validator';
3
+
4
+ export class CreateVisitorDto {
5
+ @ApiProperty({ description: 'Visitor first name', example: 'John' })
6
+ @IsString()
7
+ @IsNotEmpty()
8
+ @Length(2, 50)
9
+ name: string;
10
+
11
+ @ApiProperty({ description: 'Visitor surname', example: 'Doe' })
12
+ @IsString()
13
+ @IsNotEmpty()
14
+ @Length(2, 50)
15
+ surname: string;
16
+
17
+ // @ApiProperty({ description: 'Registration date', example: '2025-11-26' })
18
+ // @IsDateString()
19
+ // registrationDate: string;
20
+ }
src/visitor/dto/update-visitor.dto.ts ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ApiProperty } from '@nestjs/swagger';
2
+ import { IsString, Length } from 'class-validator';
3
+
4
+ export class UpdateVisitorDto {
5
+ @ApiProperty({
6
+ description: 'Visitor first name',
7
+ example: 'John',
8
+ })
9
+ @IsString()
10
+ @Length(2, 50)
11
+ name?: string;
12
+
13
+ @ApiProperty({
14
+ description: 'Visitor surname',
15
+ example: 'Doe',
16
+ })
17
+ @IsString()
18
+ @Length(2, 50)
19
+ surname?: string;
20
+ }
src/visitor/dto/visitor.dto.ts ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ApiProperty } from '@nestjs/swagger';
2
+ import {
3
+ IsString,
4
+ IsNotEmpty,
5
+ Length,
6
+ IsDateString,
7
+ IsArray,
8
+ ValidateNested,
9
+ } from 'class-validator';
10
+ import { Type } from 'class-transformer';
11
+ import { Link } from 'src/common/Link';
12
+
13
+ export class Visitor {
14
+ @ApiProperty({
15
+ description: 'Visitor ID',
16
+ example: '123e4567-e89b-12d3-a456-426614174000',
17
+ })
18
+ @IsString()
19
+ id: string;
20
+
21
+ @ApiProperty({ description: 'Visitor first name', example: 'John' })
22
+ @IsString()
23
+ @IsNotEmpty()
24
+ @Length(2, 50)
25
+ name: string;
26
+
27
+ @ApiProperty({ description: 'Visitor surname', example: 'Doe' })
28
+ @IsString()
29
+ @IsNotEmpty()
30
+ @Length(2, 50)
31
+ surname: string;
32
+
33
+ @ApiProperty({ description: 'Registration date', example: '2025-11-26' })
34
+ @IsDateString()
35
+ registrationDate: Date;
36
+
37
+ @ApiProperty({ description: 'Currently borrowed books', type: [Link] })
38
+ @IsArray()
39
+ @ValidateNested({ each: true })
40
+ @Type(() => Link)
41
+ currentBooks: Link[];
42
+
43
+ @ApiProperty({ description: 'History of returned books', type: [Link] })
44
+ @IsArray()
45
+ @ValidateNested({ each: true })
46
+ @Type(() => Link)
47
+ history: Link[];
48
+ }
src/visitor/visitor.controller.ts ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ Controller,
3
+ Get,
4
+ Post,
5
+ Patch,
6
+ Delete,
7
+ Body,
8
+ Param,
9
+ } from '@nestjs/common';
10
+ import { VisitorService } from './visitor.service';
11
+ import { Visitor } from './dto/visitor.dto';
12
+ import { CreateVisitorDto } from './dto/create-visitor.dto';
13
+ import { UpdateVisitorDto } from './dto/update-visitor.dto';
14
+ import { buildDownloadFile } from 'src/utils/download.utils';
15
+
16
+ @Controller('visitors')
17
+ export class VisitorController {
18
+ constructor(private readonly visitorService: VisitorService) {}
19
+
20
+ @Get('all')
21
+ async getAll(): Promise<Visitor[]> {
22
+ return this.visitorService.getAll();
23
+ }
24
+
25
+ @Get(':id/download')
26
+ async download(@Param('id') id: string) {
27
+ const visitor = await this.visitorService.getById(id);
28
+ return buildDownloadFile('visitors', id, visitor);
29
+ }
30
+
31
+ @Post('create')
32
+ async create(@Body() dto: CreateVisitorDto): Promise<Visitor> {
33
+ return this.visitorService.add(dto);
34
+ }
35
+ @Get(':id')
36
+ async getById(@Param('id') id: string): Promise<Visitor> {
37
+ return this.visitorService.getById(id);
38
+ }
39
+
40
+ @Patch(':id')
41
+ async update(
42
+ @Param('id') id: string,
43
+ @Body() dto: UpdateVisitorDto,
44
+ ): Promise<Visitor> {
45
+ return this.visitorService.update(id, dto);
46
+ }
47
+
48
+ @Delete('delete/:id')
49
+ async remove(@Param('id') id: string): Promise<void> {
50
+ return this.visitorService.delete(id);
51
+ }
52
+ }
src/visitor/visitor.module.ts ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Module, forwardRef } from '@nestjs/common';
2
+ import { VisitorController } from './visitor.controller';
3
+ import { VisitorService } from './visitor.service';
4
+ import { VisitorLinkManager } from 'src/common/visitor-link-manager';
5
+ import { BookModule } from '../book/book.module';
6
+
7
+ @Module({
8
+ imports: [forwardRef(() => BookModule)],
9
+ controllers: [VisitorController],
10
+ providers: [VisitorService, VisitorLinkManager],
11
+ exports: [VisitorService, VisitorLinkManager],
12
+ })
13
+ export class VisitorModule {}
src/visitor/visitor.service.ts ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* eslint-disable prettier/prettier */
2
+ import {
3
+ Injectable,
4
+ NotFoundException,
5
+ BadRequestException,
6
+ } from '@nestjs/common';
7
+ import { FileManager } from 'src/utils/file-manager';
8
+ import { parseTxtRow, serializeTxtRow } from 'src/utils/file.utils';
9
+ import { Visitor } from './dto/visitor.dto';
10
+ import { randomUUID } from 'crypto';
11
+ import { CreateVisitorDto } from './dto/create-visitor.dto';
12
+ import { UpdateVisitorDto } from './dto/update-visitor.dto';
13
+ import { Book } from 'src/book/dto/book.dto';
14
+ import { bookLinkManager } from 'src/common/book-link-manager';
15
+ import { VisitorLinkManager } from 'src/common/visitor-link-manager';
16
+ import { Link } from 'src/common/Link';
17
+
18
+ @Injectable()
19
+ export class VisitorService {
20
+ private file = new FileManager('src/data/visitors.txt');
21
+ private visitorLinkManager: VisitorLinkManager = new VisitorLinkManager();
22
+
23
+ async getAll(): Promise<Visitor[]> {
24
+ const lines = await this.file.readLines();
25
+ return lines.map(parseTxtRow) as unknown as Visitor[];
26
+ }
27
+
28
+ async getById(id: string): Promise<Visitor> {
29
+ const allVisitors = await this.getAll();
30
+ const visitor = allVisitors.find((v) => v.id === id);
31
+ if (!visitor) {
32
+ throw new NotFoundException(`Visitor with id ${id} not found`);
33
+ }
34
+
35
+ return this.visitorLinkManager.enrich(visitor);
36
+ }
37
+
38
+ async add(dto: CreateVisitorDto): Promise<Visitor> {
39
+ const newVisitor: Visitor = {
40
+ id: randomUUID(),
41
+ name: dto.name.trim(),
42
+ surname: dto.surname.trim(),
43
+ registrationDate: new Date(),
44
+ currentBooks: [],
45
+ history: [],
46
+ };
47
+
48
+ const lines = await this.file.readLines();
49
+ lines.push(await serializeTxtRow(newVisitor));
50
+ await this.file.writeLines(lines);
51
+
52
+ return newVisitor;
53
+ }
54
+
55
+ async update(id: string, dto: UpdateVisitorDto): Promise<Visitor> {
56
+ const visitors = await this.getAll();
57
+ const index = visitors.findIndex((v) => v.id === id);
58
+ if (index === -1)
59
+ throw new NotFoundException(`Visitor with id ${id} not found`);
60
+
61
+ visitors[index] = { ...visitors[index], ...dto };
62
+ await this.file.writeLines(
63
+ await Promise.all(visitors.map((visitor) => serializeTxtRow(visitor))),
64
+ );
65
+ return visitors[index];
66
+ }
67
+
68
+ async delete(id: string): Promise<void> {
69
+ const visitors = await this.getAll();
70
+ const index = visitors.findIndex((v) => v.id === id);
71
+ if (index === -1)
72
+ throw new NotFoundException(`Visitor with id ${id} not found`);
73
+
74
+ if (visitors[index].currentBooks.length > 0) {
75
+ throw new BadRequestException(
76
+ 'Cannot delete visitor with unreturned books',
77
+ );
78
+ }
79
+
80
+ visitors.splice(index, 1);
81
+ await this.file.writeLines(
82
+ await Promise.all(visitors.map((visitor) => serializeTxtRow(visitor))),
83
+ );
84
+ }
85
+
86
+ async addCurrentBooks(visitorId: string, books: Book[]): Promise<void> {
87
+ const visitors = await this.getAll();
88
+ const visitorIndex = visitors.findIndex((v) => v.id === visitorId);
89
+
90
+ if (visitorIndex === -1) {
91
+ throw new BadRequestException(`Visitor with id ${visitorId} not found`);
92
+ }
93
+
94
+ visitors[visitorIndex].currentBooks.push(
95
+ ...books.map((b) => bookLinkManager.toLink(b.id)),
96
+ );
97
+ await this.file.writeLines(
98
+ await Promise.all(visitors.map((visitor) => serializeTxtRow(visitor))),
99
+ );
100
+ }
101
+
102
+ async moveToHistory(visitorId: string, books: Book[]): Promise<void> {
103
+ const visitors = await this.getAll();
104
+ const visitorIndex = visitors.findIndex((v) => v.id === visitorId);
105
+
106
+ if (visitorIndex === -1) {
107
+ throw new BadRequestException(`Visitor with id ${visitorId} not found`);
108
+ }
109
+
110
+ const visitor = visitors[visitorIndex];
111
+
112
+ for (const book of books) {
113
+ const currentBookIndex = visitor.currentBooks.findIndex(
114
+ (b) => b.id === book.id,
115
+ );
116
+
117
+ if (currentBookIndex === -1) {
118
+ throw new BadRequestException(
119
+ `Visitor does not have book "${book.title}"`,
120
+ );
121
+ }
122
+
123
+ const [removedBook] = visitor.currentBooks.splice(currentBookIndex, 1);
124
+ visitor.history.push(removedBook);
125
+ }
126
+
127
+ await this.file.writeLines(
128
+ await Promise.all(visitors.map((visitor) => serializeTxtRow(visitor))),
129
+ );
130
+ }
131
+
132
+ async hasBook(visitorId: string, bookId: string): Promise<boolean> {
133
+ const visitor = await this.getById(visitorId);
134
+ if (!visitor) return false;
135
+
136
+ return visitor.currentBooks.some((b: Link) => b.id === bookId);
137
+ }
138
+ }