Spaces:
Sleeping
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.
- .gitignore +56 -0
- .prettierrc +4 -0
- README.md +98 -0
- eslint.config.mjs +35 -0
- nest-cli.json +8 -0
- package-lock.json +0 -0
- package.json +75 -0
- src/app.controller.spec.ts +22 -0
- src/app.controller.ts +7 -0
- src/app.module.ts +13 -0
- src/app.service.ts +4 -0
- src/book/book.controller.ts +110 -0
- src/book/book.module.ts +14 -0
- src/book/book.service.ts +123 -0
- src/book/dto/book.dto.ts +77 -0
- src/book/dto/borrow-book.dto.ts +39 -0
- src/book/dto/create-book.dto.ts +68 -0
- src/book/dto/return-book.dto.ts +39 -0
- src/book/dto/update-book.dto.ts +4 -0
- src/common/Link.ts +15 -0
- src/common/LinkManager.ts +33 -0
- src/common/book-link-manager.ts +9 -0
- src/common/key-manager.ts +51 -0
- src/common/visitor-link-manager.ts +33 -0
- src/common/worker-link-manager.ts +27 -0
- src/config/swagger.config.ts +9 -0
- src/data/books.txt +8 -0
- src/data/encrypted/.encryption_key +1 -0
- src/data/visitors.txt +1 -0
- src/data/workers.txt +1 -0
- src/filters/http-exception.filter.ts +58 -0
- src/interceptors/response.interceptor.ts +33 -0
- src/main.ts +26 -0
- src/types/book.types.ts +53 -0
- src/types/worker.types.ts +25 -0
- src/utils/crypto.utils.ts +23 -0
- src/utils/date.utils.ts +26 -0
- src/utils/download.utils.ts +16 -0
- src/utils/file-manager.ts +53 -0
- src/utils/file.utils.ts +88 -0
- src/utils/lzss.util.ts +165 -0
- src/utils/offset-encryption.util.ts +55 -0
- src/utils/response-wrapper.utils.ts +11 -0
- src/utils/swagger.utils.ts +10 -0
- src/visitor/dto/create-visitor.dto.ts +20 -0
- src/visitor/dto/update-visitor.dto.ts +20 -0
- src/visitor/dto/visitor.dto.ts +48 -0
- src/visitor/visitor.controller.ts +52 -0
- src/visitor/visitor.module.ts +13 -0
- src/visitor/visitor.service.ts +138 -0
|
@@ -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
|
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"singleQuote": true,
|
| 3 |
+
"trailingComma": "all"
|
| 4 |
+
}
|
|
@@ -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 |
+
<!--[](https://opencollective.com/nest#backer)
|
| 22 |
+
[](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).
|
|
@@ -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 |
+
);
|
|
@@ -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 |
+
}
|
|
The diff for this file is too large to render.
See raw diff
|
|
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
});
|
|
@@ -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 |
+
}
|
|
@@ -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 {}
|
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Injectable } from '@nestjs/common';
|
| 2 |
+
|
| 3 |
+
@Injectable()
|
| 4 |
+
export class AppService {}
|
|
@@ -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 |
+
}
|
|
@@ -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 {}
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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) {}
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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();
|
|
@@ -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 |
+
}
|
|
@@ -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();
|
|
@@ -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();
|
|
@@ -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 |
+
}
|
|
@@ -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
|
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
Xh(}jDoYEOU^/U*$
|
|
@@ -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=[]
|
|
@@ -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"]
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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();
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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 {}
|
|
@@ -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 |
+
}
|