Skip to content
14 changes: 13 additions & 1 deletion Workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ npm install
```env
PORT=3000
SALT=iamsalt
HOST=27.0.0.1
HOST=127.0.0.1
DB_HOST=localhost
DB_USER=admin
DB_PASSWORD=test
Expand All @@ -38,6 +38,7 @@ DB_NAME=six-cities-2
UPLOAD_DIRECTORY=upload
STATIC_DIRECTORY_PATH=static
JWT_SECRET=secret
DEFAULT_USER_PASSWORD=password
```

Назначение переменных:
Expand All @@ -55,6 +56,17 @@ JWT_SECRET=secret

Перед стартом приложения убедитесь, что сервер MongoDB запущен и доступен по параметрам из `.env`. Если база не будет доступна, REST API не сможет завершить инициализацию.

Для запуска проекта с использованием Docker выполните в терминале:

```bash
docker-compose \
--file ./docker-compose.yml \
--env-file ./.env \
--project-name "six-cities" \
up \
-d
```

### 4. Собрать проект

```bash
Expand Down
File renamed without changes.
10 changes: 0 additions & 10 deletions mocks/test-data.tsv

This file was deleted.

45 changes: 42 additions & 3 deletions specification/project.spec.yml
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ paths:
schema:
type: array
items:
$ref: '#/components/schemas/Offer'
$ref: '#/components/schemas/OfferPreview'

post:
tags: [offers]
Expand Down Expand Up @@ -254,7 +254,7 @@ paths:
type: array
maxItems: 3
items:
$ref: '#/components/schemas/Offer'
$ref: '#/components/schemas/OfferPreview'
'400':
description: Неверно выбран город

Expand All @@ -273,7 +273,7 @@ paths:
schema:
type: array
items:
$ref: '#/components/schemas/Offer'
$ref: '#/components/schemas/OfferPreview'
'401':
$ref: '#/components/responses/Unauthorized'

Expand Down Expand Up @@ -631,3 +631,42 @@ components:
type: string
location:
$ref: '#/components/schemas/Coordinates'
OfferPreview:
type: object
required:
- id
- title
- publishDate
- city
- previewImage
- isPremium
- isFavorite
- type
- price
- commentsCount
- rating
properties:
id:
type: string
title:
type: string
publishDate:
type: string
format: date-time
city:
$ref: '#/components/schemas/City'
previewImage:
type: string
isPremium:
type: boolean
isFavorite:
type: boolean
type:
$ref: '#/components/schemas/Housing'
price:
type: integer
commentsCount:
type: integer
rating:
type: number
format: float
21 changes: 21 additions & 0 deletions src/cli/commands/generate.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,29 @@ export class GenerateCommand implements Command {

public async execute(...args: string[]): Promise<void> {
const [count, filepath, url] = args;

if (!count || !filepath || !url) {
console.error(
chalk.redBright(
'Invalid arguments. Usage: npm run cli -- --generate <count> <filepath> <url>',
),
);

return;
}

const offerCount = Number.parseInt(count, 10);

if (Number.isNaN(offerCount) || offerCount <= 0) {
console.error(chalk.redBright('Count must be a positive integer'));
return;
}

if (!filepath.endsWith('.tsv')) {
console.error(chalk.redBright('Output file must have .tsv extension'));
return;
}

try {
await this.load(url);
await this.write(filepath, offerCount);
Expand Down
8 changes: 4 additions & 4 deletions src/cli/commands/help.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ export class HelpCommand implements Command {
Пример:
cli.js --<command> [--arguments]
Команды:
--version: # выводит номер версии
--help: # печатает этот текст
--import <path> <login> <password> <host> <dbname> <salt>: # импортирует данные из TSV
--generate <n> <path> <url> # генерирует произвольное количество тестовых данных
--version: # выводит номер версии
--help: # печатает этот текст
--import <path>: # импортирует данные из TSV
--generate <n> <path> <url>: # генерирует произвольное количество тестовых данных
`);
}
}
97 changes: 68 additions & 29 deletions src/cli/commands/import.command.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import path from 'node:path';
import { Command } from './command.interface.js';
import { TSVFileReader } from '../../shared/libs/file-reader/index.js';
import {
createOffer,
getErrorMessage,
getMongoURI,
ParsedOffer,
} from '../../shared/helpers/index.js';
import chalk from 'chalk';
import {
Expand All @@ -12,6 +14,7 @@ import {
UserService,
} from '../../shared/modules/user/index.js';
import {
CreateOfferDTO,
DefaultOfferService,
OfferModel,
OfferService,
Expand All @@ -21,14 +24,15 @@ import {
MongoDatabaseClient,
} from '../../shared/libs/database-client/index.js';
import { ConsoleLogger, Logger } from '../../shared/libs/logger/index.js';
import { Offer } from '../../shared/types/index.js';
import { CommentModel } from '../../shared/modules/comment/index.js';
import { FavoriteModel } from '../../shared/modules/favorite/index.js';
import {
Config,
RestConfig,
RestSchema,
} from '../../shared/libs/config/index.js';
import { plainToInstance } from 'class-transformer';
import { validateOrReject, ValidationError } from 'class-validator';

export class ImportCommand implements Command {
private userService: UserService;
Expand Down Expand Up @@ -57,13 +61,13 @@ export class ImportCommand implements Command {
return '--import';
}

private async saveOffer(offer: Offer) {
private async saveOffer(offer: ParsedOffer) {
const user = await this.userService.findOrCreate(
{ ...offer.host, password: this.config.get('DEFAULT_USER_PASSWORD') },
this.salt,
);

await this.offerService.create({
const dto = plainToInstance(CreateOfferDTO, {
userId: user.id,
title: offer.title,
description: offer.description,
Expand All @@ -79,53 +83,88 @@ export class ImportCommand implements Command {
price: offer.price,
facilities: offer.facilities,
});

await validateOrReject(dto);

await this.offerService.create(dto);
}

private async onImportedLine(line: string, resolve: () => void) {
const offer = createOffer(line);
await this.saveOffer(offer);
resolve();
private async onImportedLine(
line: string,
resolve: (value: boolean) => void,
) {
try {
const offer = createOffer(line);

await this.saveOffer(offer);

resolve(true);
} catch (error) {
if (Array.isArray(error)) {
console.error(chalk.redBright('Validation failed'));

error.forEach((item: ValidationError) => {
Object.values(item.constraints ?? {}).forEach((message) => {
console.error(message);
});
});
} else {
console.error(getErrorMessage(error));
}

resolve(false);
}
}

private onCompleteImport(count: number) {
console.info(chalk.bgBlueBright(`${count} rows imported`));
this.databaseClient.disconnect();
}

public async execute(
filename: string,
login: string,
password: string,
host: string,
dbname: string,
salt: string,
): Promise<void> {
public async execute(filename?: string): Promise<void> {
if (!filename) {
console.error(
chalk.redBright(
'No file provided. Usage: npm run cli -- --import <path/to/file.tsv>',
),
);
return;
}

if (path.extname(filename).toLowerCase() !== '.tsv') {
console.error(chalk.redBright('Only .tsv files are supported'));
return;
}

this.logger.info('Starting import...');

this.config = new RestConfig(this.logger);

const uri = getMongoURI(
login,
password,
host,
this.config.get('DB_USER'),
this.config.get('DB_PASSWORD'),
this.config.get('DB_HOST'),
this.config.get('DB_PORT'),
dbname,
this.config.get('DB_NAME'),
);
this.salt = salt;

await this.databaseClient.connect(uri);
this.salt = this.config.get('SALT');

const fileReader = new TSVFileReader(filename.trim());
try {
await this.databaseClient.connect(uri);

fileReader.on('line', this.onImportedLine);
fileReader.on('end', this.onCompleteImport);
const fileReader = new TSVFileReader(filename.trim());

fileReader.on('line', this.onImportedLine);
fileReader.on('end', this.onCompleteImport);

try {
await fileReader.read();
} catch (error) {
console.error(
chalk.redBright(`Failed to import data from file: ${filename}`),
);
console.error(`Failed to import data from file: ${filename}`);
console.error(getErrorMessage(error));
} finally {
if (this.databaseClient.isConnectedToDatabase()) {
await this.databaseClient.disconnect();
}
}
}
}
22 changes: 19 additions & 3 deletions src/shared/helpers/mock-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,26 @@ export function generateRandomBoolean() {
return Math.random() < 0.5;
}

export function getRandomItems<T>(items: T[]): T[] {
export function getRandomItems<T>(items: T[], count?: number): T[] {
if (items.length === 0) {
return [];
}

if (count !== undefined) {
if (count > items.length) {
throw new Error(
`Cannot get ${count} items from array with length ${items.length}`,
);
}

const shuffledItems = [...items].sort(() => Math.random() - 0.5);

return shuffledItems.slice(0, count);
}

const startPosition = generateRandomValue(0, items.length - 1);
const endPosition =
startPosition + generateRandomValue(startPosition, items.length);
const endPosition = generateRandomValue(startPosition + 1, items.length);

return items.slice(startPosition, endPosition);
}

Expand Down
8 changes: 6 additions & 2 deletions src/shared/helpers/offer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ import {
UserType,
} from '../types/index.js';

export function createOffer(offerData: string): Offer {
export type ParsedOffer = Omit<Offer, 'publishDate'> & {
publishDate: string;
};

export function createOffer(offerData: string): ParsedOffer {
const [
title,
description,
Expand All @@ -31,7 +35,7 @@ export function createOffer(offerData: string): Offer {
return {
title,
description,
publishDate: new Date(publishDate),
publishDate,
city: City[city as keyof typeof City],
previewImage,
images: images.split(';'),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export interface DatabaseClient {
connect(uri: string): Promise<void>;
disconnect(): Promise<void>;
isConnectedToDatabase(): boolean;
}
Loading
Loading