From c6397c53c6b76e78f046f878ebda656395be5241 Mon Sep 17 00:00:00 2001 From: Igor Antonov Date: Wed, 10 Jan 2024 17:31:27 +0300 Subject: [PATCH 01/15] =?UTF-8?q?1.1.=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=20=D0=B8=D0=BD=D1=82=D0=B5=D1=80=D1=84=D0=B5=D0=B9?= =?UTF-8?q?=D1=81=20`Command`.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Чтобы создать CLI-интерфейс в нашем приложении необходимо спланировать детали реализации и архитектуру. Определимся с командами. Функциональность CLI-приложения реализуется за счёт команд. Все команды перечислены в техническом задании к проекту. А что такое команда? Команда выполняет определённое действие и команд несколько. Можно оформить в виде одного модуля, но поддерживать его станет сложно. Да и непонятно чего придерживаться при разработке дополнительных команд: какую структуру соблюдать, как передавать параметры и так далее. Чтобы ответить и учесть эти вопросы, спроектируем интерфейс команды. У каждой команды обязательно должно быть имя и возможность выполниться. Интерфейс `Command` определяет: * Метод `getName`. Он вернёт имя команды. Например, `--help`, `--generate`. Он пригодится при реализации менеджера команд, чтобы отличить одну команду от другой. * Метод `execute`. Метод для выполнения команды. У каждой команды должен быть реализован одноимённый метод. --- src/cli/commands/command.interface.ts | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 src/cli/commands/command.interface.ts diff --git a/src/cli/commands/command.interface.ts b/src/cli/commands/command.interface.ts new file mode 100644 index 0000000..8b6baa7 --- /dev/null +++ b/src/cli/commands/command.interface.ts @@ -0,0 +1,4 @@ +export interface Command { + getName(): string; + execute(...parameters: string[]): void; +} From db0b5fd85b47b897b3e66069eef7d59c42fc366d Mon Sep 17 00:00:00 2001 From: Igor Antonov Date: Wed, 10 Jan 2024 17:42:48 +0300 Subject: [PATCH 02/15] =?UTF-8?q?1.2.=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=20=D0=BA=D0=BE=D0=BC=D0=B0=D0=BD=D0=B4=D1=83=20`Ve?= =?UTF-8?q?rsionCommand`.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Создадим новый модуль `version.command.ts` и опишем класс, который имплементирует ранее созданный интерфейс (`Command`). Команда `--version` должна считывать из `package.json` версию приложения. То есть значение свойства `version`. Для чтения файла воспользуемся встроенным модулем `readFileSync`. Импортируем из модуля `node:fs` функцию `readFileSync` и прочитаем с её помощью файл `package.json`. Не забываем указывать кодировку. Перед чтением файла воспользуемся функцией `resolve` из встроенного модуля `node:path`. Эта функция преобразует относительный путь в абсолютный. --- src/cli/commands/version.command.ts | 50 +++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 src/cli/commands/version.command.ts diff --git a/src/cli/commands/version.command.ts b/src/cli/commands/version.command.ts new file mode 100644 index 0000000..b6aeace --- /dev/null +++ b/src/cli/commands/version.command.ts @@ -0,0 +1,50 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { Command } from './command.interface.js'; + +type PackageJSONConfig = { + version: string; +} + +function isPackageJSONConfig(value: unknown): value is PackageJSONConfig { + return ( + typeof value === 'object' && + value !== null && + !Array.isArray(value) && + Object.hasOwn(value, 'version') + ); +} + +export class VersionCommand implements Command { + constructor( + private readonly filePath: string = './package.json' + ) {} + + private readVersion(): string { + const jsonContent = readFileSync(resolve(this.filePath), 'utf-8'); + const importedContent: unknown = JSON.parse(jsonContent); + + if (! isPackageJSONConfig(importedContent)) { + throw new Error('Failed to parse json content.'); + } + + return importedContent.version; + } + + public getName(): string { + return '--version'; + } + + public async execute(..._parameters: string[]): Promise { + try { + const version = this.readVersion(); + console.info(version); + } catch (error: unknown) { + console.error(`Failed to read version from ${this.filePath}`); + + if (error instanceof Error) { + console.error(error.message); + } + } + } +} From 702850aab60c54f43d88af6e21c79167c37381ae Mon Sep 17 00:00:00 2001 From: Igor Antonov Date: Wed, 10 Jan 2024 17:43:11 +0300 Subject: [PATCH 03/15] =?UTF-8?q?1.3.=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=20=D0=BA=D0=BE=D0=BC=D0=B0=D0=BD=D0=B4=D1=83=20`He?= =?UTF-8?q?lpCommand`.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Создадим ещё одну команду — `--help`. Она возвращает информацию о доступных командах. Эта команда используется в качестве команды по умолчанию. То есть когда пользователь ничего не ввёл. --- src/cli/commands/help.command.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/cli/commands/help.command.ts diff --git a/src/cli/commands/help.command.ts b/src/cli/commands/help.command.ts new file mode 100644 index 0000000..42ccc91 --- /dev/null +++ b/src/cli/commands/help.command.ts @@ -0,0 +1,20 @@ +import { Command } from './command.interface.js'; + +export class HelpCommand implements Command { + public getName(): string { + return '--help'; + } + + public async execute(..._parameters: string[]): Promise { + console.info(` + Программа для подготовки данных для REST API сервера. + Пример: + cli.js -- [--arguments] + Команды: + --version: # выводит номер версии + --help: # печатает этот текст + --import : # импортирует данные из TSV + --generate # генерирует произвольное количество тестовых данных + `); + } +} From 564c5eb5111c987aec675976c42846d2429a6ace Mon Sep 17 00:00:00 2001 From: Igor Antonov Date: Wed, 10 Jan 2024 17:45:23 +0300 Subject: [PATCH 04/15] =?UTF-8?q?1.4.=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=20=D0=BA=D0=BB=D0=B0=D1=81=D1=81=20`CLIApplication?= =?UTF-8?q?`=20=D0=B4=D0=BB=D1=8F=20CLI-=D0=BF=D1=80=D0=B8=D0=BB=D0=BE?= =?UTF-8?q?=D0=B6=D0=B5=D0=BD=D0=B8=D1=8F.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Мы реализовали две команды, теперь научимся ими управлять. Для этого потребуется отдельный менеджер. Реализуем его в виде класса приложения `CLIApplication`. Он будет решать несколько задач: регистрировать список команд (ранее созданные классы), разбирать пользовательский ввод и запускать нужную команду. Начнём с регистрации списка команд. Создадим метод `registerCommands` и опишем в нём функцию для регистрации команд. На вход метод будет принимать массив с командами. Внутри предусмотрим проверку: убедимся, что невозможно зарегистрировать две команды с одинаковым именем. --- src/cli/cli-application.ts | 16 ++++++++++++++++ src/cli/index.ts | 1 + 2 files changed, 17 insertions(+) create mode 100644 src/cli/cli-application.ts create mode 100644 src/cli/index.ts diff --git a/src/cli/cli-application.ts b/src/cli/cli-application.ts new file mode 100644 index 0000000..248ed18 --- /dev/null +++ b/src/cli/cli-application.ts @@ -0,0 +1,16 @@ +import { Command } from './commands/command.interface.js'; + +type CommandCollection = Record; + +export class CLIApplication { + private commands: CommandCollection = {}; + + public registerCommands(commandList: Command[]): void { + commandList.forEach((command) => { + if (Object.hasOwn(this.commands, command.getName())) { + throw new Error(`Command ${command.getName()} is already registered`); + } + this.commands[command.getName()] = command; + }); + } +} diff --git a/src/cli/index.ts b/src/cli/index.ts new file mode 100644 index 0000000..8544350 --- /dev/null +++ b/src/cli/index.ts @@ -0,0 +1 @@ +export * from './cli-application.js'; From 28df72a466d46604bbaddf51e71365b4e077af59 Mon Sep 17 00:00:00 2001 From: Igor Antonov Date: Wed, 10 Jan 2024 17:51:53 +0300 Subject: [PATCH 05/15] =?UTF-8?q?1.5.=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=20=D0=BA=D0=BB=D0=B0=D1=81=D1=81=20`CommandParser`?= =?UTF-8?q?.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Научимся разбирать пользовательский ввод. Например, если пользователь вводит `--help` необходимо разобрать эту строку в более удобную структуру, чтобы потом было проще искать одну из доступных команд. Особенно это актуально, когда команда будет получать параметры. Создадим новый класс `CommandParser`. В нём реализуем статический метод `parse`. --- src/cli/command-parser.ts | 19 +++++++++++++++++++ src/cli/index.ts | 1 + 2 files changed, 20 insertions(+) create mode 100644 src/cli/command-parser.ts diff --git a/src/cli/command-parser.ts b/src/cli/command-parser.ts new file mode 100644 index 0000000..e4da774 --- /dev/null +++ b/src/cli/command-parser.ts @@ -0,0 +1,19 @@ +type ParsedCommand = Record + +export class CommandParser { + static parse(cliArguments: string[]): ParsedCommand { + const parsedCommand: ParsedCommand = {}; + let currentCommand = ''; + + for (const argument of cliArguments) { + if (argument.startsWith('--')) { + parsedCommand[argument] = []; + currentCommand = argument; + } else if (currentCommand && argument) { + parsedCommand[currentCommand].push(argument); + } + } + + return parsedCommand; + } +} diff --git a/src/cli/index.ts b/src/cli/index.ts index 8544350..36cd834 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -1 +1,2 @@ export * from './cli-application.js'; +export * from './command-parser.js'; From 981259fe78833c911a1b204c39b238b6f25fbeb5 Mon Sep 17 00:00:00 2001 From: Igor Antonov Date: Wed, 10 Jan 2024 17:52:45 +0300 Subject: [PATCH 06/15] =?UTF-8?q?1.6.=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=20=D0=BC=D0=B5=D0=BD=D0=B5=D0=B4=D0=B6=D0=B5=D1=80?= =?UTF-8?q?=D1=83=20=D0=BC=D0=B5=D1=82=D0=BE=D0=B4=20`processCommand`=20?= =?UTF-8?q?=D0=B2=20`CLIApplication`.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Команды регистрируются, пользовательский ввод парсится (разбирается). Остаётся научить приложение исполнять команды. Создадим отдельный публичный метод `processCommand`. Аргументом он получает пользовательский ввод и задействует ранее созданные методы. Обратите внимание, мы предусмотрели команду по умолчанию. Ей становится `--help`. --- src/cli/cli-application.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/cli/cli-application.ts b/src/cli/cli-application.ts index 248ed18..bdc09cc 100644 --- a/src/cli/cli-application.ts +++ b/src/cli/cli-application.ts @@ -1,10 +1,15 @@ import { Command } from './commands/command.interface.js'; +import { CommandParser } from './command-parser.js'; type CommandCollection = Record; export class CLIApplication { private commands: CommandCollection = {}; + constructor( + private readonly defaultCommand: string = '--help' + ) {} + public registerCommands(commandList: Command[]): void { commandList.forEach((command) => { if (Object.hasOwn(this.commands, command.getName())) { @@ -13,4 +18,23 @@ export class CLIApplication { this.commands[command.getName()] = command; }); } + + public getCommand(commandName: string): Command { + return this.commands[commandName] ?? this.getDefaultCommand(); + } + + public getDefaultCommand(): Command | never { + if (! this.commands[this.defaultCommand]) { + throw new Error(`The default command (${this.defaultCommand}) is not registered.`); + } + return this.commands[this.defaultCommand]; + } + + public processCommand(argv: string[]): void { + const parsedCommand = CommandParser.parse(argv); + const [commandName] = Object.keys(parsedCommand); + const command = this.getCommand(commandName); + const commandArguments = parsedCommand[commandName] ?? []; + command.execute(...commandArguments); + } } From acb12ec87c73d5f13b1b64bb8af14e5a3fa56b1b Mon Sep 17 00:00:00 2001 From: Igor Antonov Date: Wed, 10 Jan 2024 17:58:26 +0300 Subject: [PATCH 07/15] =?UTF-8?q?1.7.=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=20=D1=82=D0=BE=D1=87=D0=BA=D1=83=20=D0=B2=D1=85?= =?UTF-8?q?=D0=BE=D0=B4=D0=B0=20=D0=B4=D0=BB=D1=8F=20CLI-=D0=BF=D1=80?= =?UTF-8?q?=D0=B8=D0=BB=D0=BE=D0=B6=D0=B5=D0=BD=D0=B8=D1=8F=20(`main.cli.t?= =?UTF-8?q?s`)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Соберём всё вместе: создадим отдельную точку входа для CLI-приложения. В ней опишем единственную функцию `bootstrap` в которой выполним инициализацию приложения. Попробуйте протестировать приложение с помощью сценария `ts`. Например: `npm run ts ./src/main.cli.ts -- --version`. Обратите внимание на два дополнительных дефиса. Мы передаём параметры для `main.cli.ts`, а не для `npm`. --- src/cli/index.ts | 2 ++ src/main.cli.ts | 13 +++++++++++++ 2 files changed, 15 insertions(+) create mode 100644 src/main.cli.ts diff --git a/src/cli/index.ts b/src/cli/index.ts index 36cd834..2b1a105 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -1,2 +1,4 @@ export * from './cli-application.js'; export * from './command-parser.js'; +export * from './commands/help.command.js'; +export * from './commands/version.command.js'; diff --git a/src/main.cli.ts b/src/main.cli.ts new file mode 100644 index 0000000..035de4c --- /dev/null +++ b/src/main.cli.ts @@ -0,0 +1,13 @@ +import { CLIApplication, HelpCommand, VersionCommand } from './cli/index.js'; + +function bootstrap() { + const cliApplication = new CLIApplication(); + cliApplication.registerCommands([ + new HelpCommand(), + new VersionCommand() + ]); + + cliApplication.processCommand(process.argv); +} + +bootstrap(); From 90ecc893685508f2f83bcc84bf86e5ee59c58886 Mon Sep 17 00:00:00 2001 From: Igor Antonov Date: Wed, 10 Jan 2024 17:58:50 +0300 Subject: [PATCH 08/15] =?UTF-8?q?1.8.=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=20=D1=84=D0=B0=D0=B9=D0=BB=20=D1=81=20=D1=82=D0=B5?= =?UTF-8?q?=D1=81=D1=82=D0=BE=D0=B2=D1=8B=D0=BC=D0=B8=20=D0=B4=D0=B0=D0=BD?= =?UTF-8?q?=D0=BD=D1=8B=D0=BC=D0=B8.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Попробуем реализовать более сложную команду — `--import`. Эта команда должна импортировать тестовые данные из tsv-файла. tsv-файл — это обычный текстовый файл. Значения в нём разделены символами табуляции. Чтобы разобрать такую строку на отдельные составляющие, необходимо разрезать её по символу `\t`. --- mocks/mock-data.tsv | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 mocks/mock-data.tsv diff --git a/mocks/mock-data.tsv b/mocks/mock-data.tsv new file mode 100644 index 0000000..00d2dfb --- /dev/null +++ b/mocks/mock-data.tsv @@ -0,0 +1,4 @@ +Кристина Виновен или нет? Какая разница. Исход один. Сверхспособности не помогут. 2022-04-06T08:45:40.283Z institute-book.jpg Sell 1080 Книги;Журналы Пьер Безухов test@pisem.local awesome-avatar.jpg +Зелёная миля Как похудеть и не сойти с ума? А как вернуть вес обратно? Необычная история противостояния. 2022-04-11T08:45:40.284Z book.jpg Buy 888 Посуда;Разное;Диски Роланд Дискейн president@dka.local photo.jpg +Тёмная башня Прими звонок и превратись в зомби. Рассказ о зомбировании через средства связи. 2022-04-05T08:45:40.284Z book.jpg Buy 780 Книги;Журналы;Посуда;Разное;Диски Олег Сидоров president@dka.local photo.jpg +Долгая прогулка Идти или остановиться? Вопрос без правильного ответа. Рассказ о долгой и смертельной прогулки. 2022-04-06T08:45:40.284Z book.jpg Sell 1610 Книги Иван Иванов president@dka.local awesome-avatar.jpg From 70db56a9f4a8169f1b9dedeeb197877ccadb8315 Mon Sep 17 00:00:00 2001 From: Igor Antonov Date: Wed, 10 Jan 2024 17:59:09 +0300 Subject: [PATCH 09/15] =?UTF-8?q?1.9.=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BC=20=D0=BE=D1=81=D0=BD=D0=BE=D0=B2=D1=83=20=D0=BA?= =?UTF-8?q?=D0=BE=D0=BC=D0=B0=D0=BD=D0=B4=D1=8B=20`ImportCommand`=20WIP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Подготовим основу для команды `--import`. Опишем новый класс и сделаем заготовку для метода `execute`. WIP: Линтер ругается на неиспользуемые переменные. --- src/cli/commands/import.command.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/cli/commands/import.command.ts diff --git a/src/cli/commands/import.command.ts b/src/cli/commands/import.command.ts new file mode 100644 index 0000000..2ad1c4c --- /dev/null +++ b/src/cli/commands/import.command.ts @@ -0,0 +1,11 @@ +import { Command } from './command.interface.js'; + +export class ImportCommand implements Command { + public getName(): string { + return '--import'; + } + + public execute(...parameters: string[]): void { + // Чтение файла + } +} From ebc335c29bc4c4875ebf110ab061311a194467a0 Mon Sep 17 00:00:00 2001 From: Igor Antonov Date: Wed, 10 Jan 2024 18:00:13 +0300 Subject: [PATCH 10/15] =?UTF-8?q?1.10.=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=20=D1=82=D0=B8=D0=BF=D1=8B=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D0=B8=D0=BC=D0=BF=D0=BE=D1=80=D1=82=D0=B8=D1=80=D1=83=D0=B5?= =?UTF-8?q?=D0=BC=D1=8B=D1=85=20=D1=81=D1=83=D1=89=D0=BD=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D0=B5=D0=B9.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Перед тем, как написать код для считывания данных, подумаем о структурах и типах. Мы считываем данные определённой «формы». Подготовим описание типов. Создадим директорию `shared/types` и опишем в ней общие типы. Для удобства каждый тип опишем в отдельном модуле. Нам потребуется несколько типов: - Объявление (`Offer`); - Категория (`Category`); - Пользователь (`User`); - Тип объявления (`OfferType`). При описании типов важно опираться на техническое задание, чтобы ничего не пропустить. --- src/shared/types/category.type.ts | 3 +++ src/shared/types/index.ts | 4 ++++ src/shared/types/offer-type.enum.ts | 4 ++++ src/shared/types/offer.type.ts | 14 ++++++++++++++ src/shared/types/user.type.ts | 6 ++++++ 5 files changed, 31 insertions(+) create mode 100644 src/shared/types/category.type.ts create mode 100644 src/shared/types/index.ts create mode 100644 src/shared/types/offer-type.enum.ts create mode 100644 src/shared/types/offer.type.ts create mode 100644 src/shared/types/user.type.ts diff --git a/src/shared/types/category.type.ts b/src/shared/types/category.type.ts new file mode 100644 index 0000000..568dc00 --- /dev/null +++ b/src/shared/types/category.type.ts @@ -0,0 +1,3 @@ +export type Category = { + name: string; +} diff --git a/src/shared/types/index.ts b/src/shared/types/index.ts new file mode 100644 index 0000000..f669f58 --- /dev/null +++ b/src/shared/types/index.ts @@ -0,0 +1,4 @@ +export * from './category.type.js'; +export * from './offer-type.enum.js'; +export * from './user.type.js'; +export * from './offer.type.js'; diff --git a/src/shared/types/offer-type.enum.ts b/src/shared/types/offer-type.enum.ts new file mode 100644 index 0000000..8460f95 --- /dev/null +++ b/src/shared/types/offer-type.enum.ts @@ -0,0 +1,4 @@ +export enum OfferType { + Buy = 'Buy', + Sell = 'Sell', +} diff --git a/src/shared/types/offer.type.ts b/src/shared/types/offer.type.ts new file mode 100644 index 0000000..ed207dd --- /dev/null +++ b/src/shared/types/offer.type.ts @@ -0,0 +1,14 @@ +import { OfferType } from './offer-type.enum.js'; +import { Category } from './category.type.js'; +import { User } from './user.type.js'; + +export type Offer = { + title: string; + description: string; + postDate: Date; + image: string; + type: OfferType + price: number; + categories: Category[]; + user: User; +} diff --git a/src/shared/types/user.type.ts b/src/shared/types/user.type.ts new file mode 100644 index 0000000..711dbe7 --- /dev/null +++ b/src/shared/types/user.type.ts @@ -0,0 +1,6 @@ +export type User = { + email: string; + avatarPath: string; + firstname: string; + lastname: string; +} From 41364fb49f63f94ea04609ca0a421288e0e4a48c Mon Sep 17 00:00:00 2001 From: Igor Antonov Date: Wed, 10 Jan 2024 18:00:44 +0300 Subject: [PATCH 11/15] =?UTF-8?q?1.11.=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=20=D0=B8=D0=BD=D1=82=D0=B5=D1=80=D1=84=D0=B5=D0=B9?= =?UTF-8?q?=D1=81=20`FileReader`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Займёмся импортом данных из файлов. Мы без проблем можем написать этот код внутри команды `ImportCommand`, но вместо этого создадим отдельный класс. Почему: код импорта может меняться и усложняться. Поэтому лучше разместить его отдельно. К тому же команда импорта может начать работать с файлами разных форматов, не только TSV. Определим минимальный интерфейс. Класс, отвечающий за чтение данных должен уметь их читать. Определим метод `read`. --- src/shared/libs/file-reader/file-reader.interface.ts | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 src/shared/libs/file-reader/file-reader.interface.ts diff --git a/src/shared/libs/file-reader/file-reader.interface.ts b/src/shared/libs/file-reader/file-reader.interface.ts new file mode 100644 index 0000000..59f8279 --- /dev/null +++ b/src/shared/libs/file-reader/file-reader.interface.ts @@ -0,0 +1,3 @@ +export interface FileReader { + read(): void; +} From 9c600ef95bd8d47f9f2d7039afd0f879111f24fc Mon Sep 17 00:00:00 2001 From: Igor Antonov Date: Wed, 10 Jan 2024 18:01:37 +0300 Subject: [PATCH 12/15] =?UTF-8?q?1.12.=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=20=D0=BA=D0=BB=D0=B0=D1=81=D1=81=20`TSVFileReader`?= =?UTF-8?q?.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Создадим класс `TSVFileReader`. В нём реализуем чтение и разбор tsv-файла. Воспользуемся той же практикой — синхронной функцией `readFileSync`. Она прочитает файл целиком. Такой подход хорош, когда файлы небольшие, но с большими файлами мы разберёмся позже. Мы считываем файл целиком и разбиваем его на строки. В этом нам поможет управляющий символ `\n`. Затем каждую строку разбиваем на отдельные части. --- src/shared/libs/file-reader/index.ts | 2 + .../libs/file-reader/tsv-file-reader.ts | 37 +++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 src/shared/libs/file-reader/index.ts create mode 100644 src/shared/libs/file-reader/tsv-file-reader.ts diff --git a/src/shared/libs/file-reader/index.ts b/src/shared/libs/file-reader/index.ts new file mode 100644 index 0000000..4e9fa00 --- /dev/null +++ b/src/shared/libs/file-reader/index.ts @@ -0,0 +1,2 @@ +export * from './file-reader.interface.js'; +export * from './tsv-file-reader.js'; diff --git a/src/shared/libs/file-reader/tsv-file-reader.ts b/src/shared/libs/file-reader/tsv-file-reader.ts new file mode 100644 index 0000000..934d945 --- /dev/null +++ b/src/shared/libs/file-reader/tsv-file-reader.ts @@ -0,0 +1,37 @@ +import { FileReader } from './file-reader.interface.js'; +import { readFileSync } from 'node:fs'; +import { Offer, OfferType } from '../../types/index.js'; + +export class TSVFileReader implements FileReader { + private rawData = ''; + + constructor( + private readonly filename: string + ) {} + + public read(): void { + this.rawData = readFileSync(this.filename, { encoding: 'utf-8' }); + } + + public toArray(): Offer[] { + if (!this.rawData) { + throw new Error('File was not read'); + } + + return this.rawData + .split('\n') + .filter((row) => row.trim().length > 0) + .map((line) => line.split('\t')) + .map(([title, description, createdDate, image, type, price, categories, firstname, lastname, email, avatarPath]) => ({ + title, + description, + postDate: new Date(createdDate), + image, + type: OfferType[type as 'Buy' | 'Sell'], + categories: categories.split(';') + .map((name) => ({name})), + price: Number.parseInt(price, 10), + user: { email, firstname, lastname, avatarPath }, + })); + } +} From 58eb42caa9d94889ee81e7bfe4d5f73e507c9a31 Mon Sep 17 00:00:00 2001 From: Igor Antonov Date: Wed, 10 Jan 2024 18:02:27 +0300 Subject: [PATCH 13/15] =?UTF-8?q?1.13.=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=20=D0=B2=20=D0=BA=D0=BE=D0=BC=D0=B0=D0=BD=D0=B4?= =?UTF-8?q?=D1=83=20`--import`=20=D1=87=D1=82=D0=B5=D0=BD=D0=B8=D0=B5=20?= =?UTF-8?q?=D1=84=D0=B0=D0=B9=D0=BB=D0=B0.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Вернёмся к команде `ImportCommand` и добавим код для чтения из файла. В методе `execute` добавим инициализацию класса `TSVFileReader`. Результат временно выведем в консоль. --- src/cli/commands/import.command.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/cli/commands/import.command.ts b/src/cli/commands/import.command.ts index 2ad1c4c..3beccc9 100644 --- a/src/cli/commands/import.command.ts +++ b/src/cli/commands/import.command.ts @@ -1,4 +1,5 @@ import { Command } from './command.interface.js'; +import { TSVFileReader } from '../../shared/libs/file-reader/index.js'; export class ImportCommand implements Command { public getName(): string { @@ -6,6 +7,20 @@ export class ImportCommand implements Command { } public execute(...parameters: string[]): void { - // Чтение файла + const [filename] = parameters; + const fileReader = new TSVFileReader(filename.trim()); + + try { + fileReader.read(); + console.log(fileReader.toArray()); + } catch (err) { + + if (!(err instanceof Error)) { + throw err; + } + + console.error(`Can't import data from file: ${filename}`); + console.error(`Details: ${err.message}`); + } } } From e7564b97e6acdedfea73b85710f1113b9020c103 Mon Sep 17 00:00:00 2001 From: Igor Antonov Date: Wed, 10 Jan 2024 18:04:05 +0300 Subject: [PATCH 14/15] =?UTF-8?q?1.14.=20=D0=97=D0=B0=D1=80=D0=B5=D0=B3?= =?UTF-8?q?=D0=B8=D1=81=D1=82=D1=80=D0=B8=D1=80=D1=83=D0=B5=D0=BC=20=D0=BA?= =?UTF-8?q?=D0=BE=D0=BC=D0=B0=D0=BD=D0=B4=D1=83=20`ImportCommand`.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Чтобы команда `--import` заработала, зарегистрируем её в `CLIApplication`, а затем попробуем протестировать: ``` npm run ts ./src/main.cli.ts -- --import ``` --- src/cli/index.ts | 1 + src/main.cli.ts | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/cli/index.ts b/src/cli/index.ts index 2b1a105..f28f95f 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -2,3 +2,4 @@ export * from './cli-application.js'; export * from './command-parser.js'; export * from './commands/help.command.js'; export * from './commands/version.command.js'; +export * from './commands/import.command.js'; diff --git a/src/main.cli.ts b/src/main.cli.ts index 035de4c..8b73171 100644 --- a/src/main.cli.ts +++ b/src/main.cli.ts @@ -1,10 +1,11 @@ -import { CLIApplication, HelpCommand, VersionCommand } from './cli/index.js'; +import { CLIApplication, HelpCommand, ImportCommand, VersionCommand } from './cli/index.js'; function bootstrap() { const cliApplication = new CLIApplication(); cliApplication.registerCommands([ new HelpCommand(), - new VersionCommand() + new VersionCommand(), + new ImportCommand(), ]); cliApplication.processCommand(process.argv); From 7217b63ff92bc30f6c6292ad97b0375d21ba4f7b Mon Sep 17 00:00:00 2001 From: Igor Antonov Date: Wed, 10 Jan 2024 18:04:58 +0300 Subject: [PATCH 15/15] =?UTF-8?q?1.15.=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=20`shebang`=20=D1=81=D1=86=D0=B5=D0=BD=D0=B0=D1=80?= =?UTF-8?q?=D0=B8=D1=8F=20cli.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Остался последний штрих. Команды работают, но для запуска приходится использовать `ts-node` или `node`. С консольными приложениями всё обычно происходит не так. Мы их запускаем в терминале. Чтобы наше приложение удалось запустить в терминале, нам необходимо прописать `sheband`. sheband — последовательность из символов `#` и `!`. После этого символа можно определить путь к приложению, которое умеет исполнять данный скрипт. Укажем в качестве такого приложения путь к Node.js. Попробуем собрать проект (`npm run build`) и запустить: `./dist/main.cli.js --help`. Ничего не выходит. Терминал выдаст ошибку «permission denied». Чтобы исправить ошибку — необходимо сделать файл запускаемым. Для этого добавим файлу дополнительный атрибут с помощью команды `chmod`. Выполните: `chmod u+x ./dist/main.cli.js`. После этого попробуйте запустить приложение в терминале. --- src/main.cli.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main.cli.ts b/src/main.cli.ts index 8b73171..a737264 100644 --- a/src/main.cli.ts +++ b/src/main.cli.ts @@ -1,3 +1,4 @@ +#!/usr/bin/env node import { CLIApplication, HelpCommand, ImportCommand, VersionCommand } from './cli/index.js'; function bootstrap() {