├── db.json ├── .gitignore ├── db_categories.json ├── modules ├── getCategories.js ├── resetDB.js ├── deleteFinanceData.js ├── getFinanceData.js ├── getTestData.js └── addFinanceData.js ├── package.json ├── index.js ├── LICENSE ├── README.md └── db_test.json /db.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ -------------------------------------------------------------------------------- /db_categories.json: -------------------------------------------------------------------------------- 1 | {"income":["Зарплата","Подарки","Сайт ооо \"пунктир\""],"expenses":["Еда","Транспорт","Развлечения","Образование"]} -------------------------------------------------------------------------------- /modules/getCategories.js: -------------------------------------------------------------------------------- 1 | // Импорт необходимого модуля 2 | import { readFile } from "fs/promises"; 3 | 4 | // Путь к файлу с категориями 5 | const categoriesFilePath = "./db_categories.json"; 6 | 7 | // Функция-обработчик для GET-запроса категорий 8 | export async function getCategories(req, res) { 9 | try { 10 | // Читаем текущие категории из файла 11 | const data = await readFile(categoriesFilePath, "utf8"); 12 | const categories = JSON.parse(data); 13 | 14 | // Возвращаем категории в ответе 15 | res.status(200).json(categories); 16 | } catch (err) { 17 | res.status(500).json({ message: "Ошибка при работе с файлом" }); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api_finance", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "start": "node index", 9 | "dev": "nodemon index", 10 | "lint": "eslint . --fix" 11 | }, 12 | "keywords": [], 13 | "author": "Maksim Leskin ", 14 | "license": "ISC", 15 | "dependencies": { 16 | "cors": "^2.8.5", 17 | "eslint": "^8.49.0", 18 | "eslint-config-prettier": "^9.0.0", 19 | "eslint-plugin-prettier": "^5.0.0", 20 | "express": "^4.18.2", 21 | "nodemon": "^3.0.1", 22 | "prettier": "3.0.1", 23 | "uuid": "^9.0.1" 24 | }, 25 | "engines": { 26 | "node": ">=16.0.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /modules/resetDB.js: -------------------------------------------------------------------------------- 1 | import { writeFile } from "fs/promises"; 2 | 3 | // Путь к файлу данных 4 | const dataFilePath = "./db.json"; 5 | // Путь к файлу с категориями 6 | const categoriesFilePath = "./db_categories.json"; 7 | 8 | // Функция-обработчик для сброса базы данных 9 | export const resetDB = async (req, res) => { 10 | // Стандартное содержимое для db_categories.json 11 | const defaultCategories = { 12 | income: ["Зарплата", "Подарки"], 13 | expenses: ["Еда", "Транспорт", "Развлечения", "Образование"], 14 | }; 15 | 16 | try { 17 | // Очистка файла db.json 18 | await writeFile(dataFilePath, JSON.stringify([]), "utf8"); 19 | 20 | // Восстановление файла db_categories.json 21 | await writeFile( 22 | categoriesFilePath, 23 | JSON.stringify(defaultCategories), 24 | "utf8", 25 | ); 26 | 27 | res.status(200).json({ message: "База данных была успешно сброшена." }); 28 | } catch (err) { 29 | // Обработка возможной ошибки при записи файла 30 | res.status(500).json({ message: "Ошибка при сбросе базы данных." }); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Импорт необходимых модулей 2 | import express from "express"; 3 | import cors from "cors"; 4 | 5 | import { getFinanceData } from "./modules/getFinanceData.js"; 6 | import { addFinanceData } from "./modules/addFinanceData.js"; 7 | import { getCategories } from "./modules/getCategories.js"; 8 | import { resetDB } from "./modules/resetDB.js"; 9 | import { deleteFinanceData } from "./modules/deleteFinanceData.js"; 10 | import { getTestData } from "./modules/getTestData.js"; 11 | 12 | // Инициализация Express приложения 13 | const app = express(); 14 | 15 | app.use(cors()); 16 | 17 | // Middleware для разбора JSON-запросов 18 | app.use(express.json()); 19 | 20 | // Регистрация обработчиков 21 | app.get("/api/finance", getFinanceData); 22 | app.get("/api/test", getTestData); 23 | app.post("/api/finance", addFinanceData); 24 | app.get("/api/categories", getCategories); 25 | app.get("/api/reset", resetDB); 26 | app.delete("/api/finance/:id", deleteFinanceData); 27 | 28 | // Запуск сервера 29 | const port = 3000; 30 | app.listen(port, () => { 31 | console.log(`Сервер запущен на порту ${port}`); 32 | }); 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2023] [Methed] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /modules/deleteFinanceData.js: -------------------------------------------------------------------------------- 1 | import { readFile, writeFile } from "fs/promises"; 2 | 3 | const dataFilePath = "./db.json"; // Путь к файлу данных 4 | 5 | export const deleteFinanceData = async (req, res) => { 6 | const { id } = req.params; // Получаем ID из параметров запроса 7 | 8 | try { 9 | // Чтение текущих данных 10 | const data = await readFile(dataFilePath, "utf8"); 11 | const financeData = JSON.parse(data); 12 | 13 | // Фильтрация данных для удаления соответствующей записи 14 | const newData = financeData.filter((item) => item.id !== id); 15 | 16 | // Если размер массива не изменился, значит, запись не найдена 17 | if (financeData.length === newData.length) { 18 | return res 19 | .status(404) 20 | .json({ message: "Операция с таким ID не найдена." }); 21 | } 22 | 23 | // Запись обновленных данных обратно в файл 24 | await writeFile(dataFilePath, JSON.stringify(newData), "utf8"); 25 | 26 | res.status(200).json({ message: "Финансовая операция удалена." }); 27 | } catch (err) { 28 | // Обработка возможной ошибки при чтении или записи файла 29 | res 30 | .status(500) 31 | .json({ message: "Ошибка при удалении финансовой операции." }); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /modules/getFinanceData.js: -------------------------------------------------------------------------------- 1 | import { readFile } from "fs/promises"; 2 | // Путь к файлу данных 3 | const dataFilePath = "./db.json"; 4 | 5 | const validateDate = (date) => 6 | /^\d{4}-\d{2}-\d{2}$/.test(date) && !isNaN(new Date(date).getTime()); 7 | 8 | // Функция-обработчик для GET-запроса 9 | export const getFinanceData = async (req, res) => { 10 | const { startDate, endDate } = req.query; 11 | 12 | try { 13 | const data = await readFile(dataFilePath, "utf8"); 14 | let financeData = JSON.parse(data); 15 | 16 | if (startDate || endDate) { 17 | const start = startDate || "2000-01-01"; 18 | const end = endDate || new Date().toISOString().split("T")[0]; 19 | 20 | if (!validateDate(start) || !validateDate(end)) { 21 | return res.status(400).json({ 22 | message: "Некорректный формат даты. Используйте формат YYYY-MM-DD.", 23 | }); 24 | } 25 | const startDataTimeStamp = new Date(start).getTime(); 26 | const endDataTimeStamp = new Date(end).getTime(); 27 | const filterFinanceData = financeData.filter(({ date }) => { 28 | const timestampDate = new Date(date).getTime(); 29 | return ( 30 | timestampDate >= startDataTimeStamp && 31 | timestampDate <= endDataTimeStamp 32 | ); 33 | }); 34 | res.json(filterFinanceData); 35 | } else { 36 | res.json(financeData); 37 | } 38 | 39 | // Фильтруем данные в соответствии с датами 40 | } catch (err) { 41 | res.status(500).json({ message: "Ошибка при работе с файлом данных" }); 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /modules/getTestData.js: -------------------------------------------------------------------------------- 1 | import { readFile } from "fs/promises"; 2 | // Путь к файлу данных 3 | const dataFilePath = "./db_test.json"; 4 | 5 | const validateDate = (date) => 6 | /^\d{4}-\d{2}-\d{2}$/.test(date) && !isNaN(new Date(date).getTime()); 7 | 8 | // Функция-обработчик для GET-запроса 9 | export const getTestData = async (req, res) => { 10 | const { startDate, endDate } = req.query; 11 | 12 | try { 13 | const data = await readFile(dataFilePath, "utf8"); 14 | let financeData = JSON.parse(data); 15 | 16 | if (startDate || endDate) { 17 | const start = startDate || "2000-01-01"; 18 | const end = endDate || new Date().toISOString().split("T")[0]; 19 | 20 | if (!validateDate(start) || !validateDate(end)) { 21 | return res.status(400).json({ 22 | message: "Некорректный формат даты. Используйте формат YYYY-MM-DD.", 23 | }); 24 | } 25 | const startDataTimeStamp = new Date(start).getTime(); 26 | const endDataTimeStamp = new Date(end).getTime(); 27 | const filterFinanceData = financeData.filter(({ date }) => { 28 | const timestampDate = new Date(date).getTime(); 29 | return ( 30 | timestampDate >= startDataTimeStamp && 31 | timestampDate <= endDataTimeStamp 32 | ); 33 | }); 34 | res.json(filterFinanceData); 35 | } else { 36 | res.json(financeData); 37 | } 38 | 39 | // Фильтруем данные в соответствии с датами 40 | } catch (err) { 41 | res.status(500).json({ message: "Ошибка при работе с файлом данных" }); 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Личный финансовый менеджер 2 | 3 | ## Описание 4 | 5 | Этот проект представляет собой сервер на Node.js для интенсива "Личный финансовый менеджер". Сервер обрабатывает данные о доходах и расходах, сохраняя их в JSON-файле. Также поддерживается работа с категориями доходов и расходов. 6 | 7 | ## Технологии 8 | 9 | - Node.js 10 | - Express 11 | - uuid для генерации уникальных идентификаторов 12 | - fs/promises для асинхронной работы с файловой системой 13 | 14 | ## Установка и запуск проекта 15 | 16 | 1. Клонировать репозиторий: 17 | 18 | git clone 19 | 20 | 2. Перейти в директорию проекта: 21 | 22 | cd <название-директории> 23 | 24 | 3. Установить зависимости: 25 | 26 | npm install 27 | 28 | 4. Запустить проект: 29 | 30 | - В режиме разработки: 31 | 32 | ``` 33 | npm run dev 34 | ``` 35 | 36 | - В обычном режиме: 37 | 38 | ``` 39 | npm start 40 | ``` 41 | 42 | ## API Endpoints 43 | 44 | - `GET /api/finance`: Получение всех записей о доходах и расходах, передав searchParams startDate и/или endDate можно отфильтровать по дате (формат ГГГГ-ММ-ДД) 45 | - `POST /api/finance`: Добавление новой записи о доходе или расходе 46 | - `GET /api/categories`: Получение списка категорий 47 | - `DELETE /api/finance/:id`: Удаление записи 48 | - `GET /api/reset`: Сброс БД до стартового состояния 49 | - `GET /api/test`: Получение тестовых записей 50 | 51 | ## Формат данных 52 | 53 | Объекты о доходах и расходах имеют следующую структуру: 54 | 55 | ```json 56 | { 57 | "id": "уникальный идентификатор", 58 | "type": "тип записи (income/outcome)", 59 | "amount": "сумма", 60 | "description": "описание операции", 61 | "category": "категория операции" 62 | } 63 | ``` 64 | 65 | ## Пример структуры категорий: 66 | 67 | ```json 68 | { 69 | "income": ["Зарплата", "Подарки"], 70 | "outcome": ["Еда", "Транспорт", "Развлечения", "Образование"] 71 | } 72 | ``` 73 | 74 | ## Авторы 75 | 76 | Ваше имя и контакты 77 | 78 | ## Лицензия 79 | 80 | Этот проект распространяется под лицензией MIT. См. файл LICENSE для подробной информации. 81 | -------------------------------------------------------------------------------- /modules/addFinanceData.js: -------------------------------------------------------------------------------- 1 | import { readFile, writeFile } from "fs/promises"; 2 | import { v4 as uuidv4 } from "uuid"; 3 | 4 | // Путь к файлу данных 5 | const dataFilePath = "./db.json"; 6 | // Путь к файлу с категориями 7 | const categoriesFilePath = "./db_categories.json"; 8 | 9 | function formatDate(date) { 10 | let d = new Date(date), 11 | month = '' + (d.getUTCMonth() + 1), // Месяцы начинаются с 0 12 | day = '' + d.getUTCDate(), 13 | year = d.getUTCFullYear(); 14 | 15 | if (month.length < 2) month = '0' + month; 16 | if (day.length < 2) day = '0' + day; 17 | 18 | return [year, month, day].join('-'); 19 | } 20 | 21 | // Функция-обработчик для POST-запроса 22 | export const addFinanceData = async (req, res) => { 23 | const { type, amount, description, category } = req.body; 24 | 25 | // Предопределенные типы категорий 26 | const allowedTypes = ["income", "expenses"]; 27 | 28 | // Проверяем, является ли тип одним из разрешенных значений 29 | if (!allowedTypes.includes(type)) { 30 | return res.status(400).json({ 31 | message: 32 | "Тип операции не распознан. Допустимые значения: 'income' или 'expenses'.", 33 | }); 34 | } 35 | 36 | // Валидация на наличие полей 37 | // Валидация на наличие остальных обязательных полей 38 | if (amount == null || description == null || category == null) { 39 | return res.status(400).json({ message: "Отсутствуют обязательные поля" }); 40 | } 41 | 42 | const id = uuidv4(); 43 | 44 | const date = new Date().toISOString(); 45 | const formattedDate = formatDate(date); 46 | 47 | const formatCategory = 48 | category[0].toUpperCase() + category.slice(1).toLowerCase(); 49 | 50 | const newItem = { 51 | id, 52 | type, 53 | amount, 54 | description, 55 | category: formatCategory, 56 | date: formattedDate, 57 | }; 58 | 59 | try { 60 | // Добавление новой категории в db_categories.json, если таковая ещё не существует 61 | const categoriesData = await readFile(categoriesFilePath, "utf8"); 62 | const categories = JSON.parse(categoriesData); 63 | 64 | // Проверяем, есть ли категория в соответствующем списке, и добавляем при необходимости 65 | if (!categories[type].includes(formatCategory)) { 66 | categories[type].push(formatCategory); 67 | await writeFile(categoriesFilePath, JSON.stringify(categories), "utf8"); 68 | } 69 | 70 | // Добавление новой финансовой записи в db.json 71 | const data = await readFile(dataFilePath, "utf8"); 72 | const financeData = JSON.parse(data); 73 | financeData.push(newItem); 74 | await writeFile(dataFilePath, JSON.stringify(financeData), "utf8"); 75 | 76 | res.status(201).json(newItem); 77 | } catch (err) { 78 | console.log("err: ", err); 79 | res.status(500).json({ message: "Ошибка при работе с файлом" }); 80 | } 81 | }; 82 | -------------------------------------------------------------------------------- /db_test.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "1", 4 | "type": "income", 5 | "amount": 45000, 6 | "description": "Зарплата за сентябрь", 7 | "category": "Зарплата", 8 | "date": "2023-09-05" 9 | }, 10 | { 11 | "id": "2", 12 | "type": "expenses", 13 | "amount": 3200, 14 | "description": "Аренда жилья", 15 | "category": "Жилье", 16 | "date": "2023-09-10" 17 | }, 18 | { 19 | "id": "3", 20 | "type": "expenses", 21 | "amount": 1200, 22 | "description": "Книги", 23 | "category": "Образование", 24 | "date": "2023-09-12" 25 | }, 26 | { 27 | "id": "4", 28 | "type": "expenses", 29 | "amount": 250, 30 | "description": "Транспорт", 31 | "category": "Транспорт", 32 | "date": "2023-09-14" 33 | }, 34 | { 35 | "id": "5", 36 | "type": "income", 37 | "amount": 7000, 38 | "description": "Фриланс", 39 | "category": "Дополнительный доход", 40 | "date": "2023-09-18" 41 | }, 42 | { 43 | "id": "6", 44 | "type": "expenses", 45 | "amount": 850, 46 | "description": "Продукты", 47 | "category": "Еда", 48 | "date": "2023-09-20" 49 | }, 50 | { 51 | "id": "7", 52 | "type": "expenses", 53 | "amount": 1900, 54 | "description": "Одежда", 55 | "category": "Шоппинг", 56 | "date": "2023-09-22" 57 | }, 58 | { 59 | "id": "8", 60 | "type": "expenses", 61 | "amount": 480, 62 | "description": "Интернет", 63 | "category": "Коммунальные услуги", 64 | "date": "2023-09-26" 65 | }, 66 | { 67 | "id": "9", 68 | "type": "expenses", 69 | "amount": 1500, 70 | "description": "Ресторан", 71 | "category": "Развлечения", 72 | "date": "2023-09-28" 73 | }, 74 | { 75 | "id": "10", 76 | "type": "income", 77 | "amount": 32000, 78 | "description": "Зарплата за октябрь", 79 | "category": "Зарплата", 80 | "date": "2023-10-05" 81 | }, 82 | { 83 | "id": "11", 84 | "type": "expenses", 85 | "amount": 2500, 86 | "description": "Техника", 87 | "category": "Электроника", 88 | "date": "2023-10-07" 89 | }, 90 | { 91 | "id": "12", 92 | "type": "expenses", 93 | "amount": 150, 94 | "description": "Общественный транспорт", 95 | "category": "Транспорт", 96 | "date": "2023-10-11" 97 | }, 98 | { 99 | "id": "13", 100 | "type": "income", 101 | "amount": 20000, 102 | "description": "Бонус", 103 | "category": "Премия", 104 | "date": "2023-10-15" 105 | }, 106 | { 107 | "id": "14", 108 | "type": "expenses", 109 | "amount": 650, 110 | "description": "Лекарства", 111 | "category": "Здоровье", 112 | "date": "2023-10-18" 113 | }, 114 | { 115 | "id": "15", 116 | "type": "income", 117 | "amount": 1500, 118 | "description": "Продажа вещей", 119 | "category": "Дополнительный доход", 120 | "date": "2023-10-20" 121 | }, 122 | { 123 | "id": "16", 124 | "type": "expenses", 125 | "amount": 550, 126 | "description": "Кафе", 127 | "category": "Еда", 128 | "date": "2023-10-23" 129 | }, 130 | { 131 | "id": "17", 132 | "type": "expenses", 133 | "amount": 3000, 134 | "description": "Подарки", 135 | "category": "Подарки", 136 | "date": "2023-10-25" 137 | }, 138 | { 139 | "id": "18", 140 | "type": "expenses", 141 | "amount": 2200, 142 | "description": "Ремонт", 143 | "category": "Дом", 144 | "date": "2023-10-27" 145 | }, 146 | { 147 | "id": "19", 148 | "type": "income", 149 | "amount": 47000, 150 | "description": "Зарплата за ноябрь", 151 | "category": "Зарплата", 152 | "date": "2023-11-05" 153 | }, 154 | { 155 | "id": "20", 156 | "type": "expenses", 157 | "amount": 3300, 158 | "description": "Путешествие", 159 | "category": "Отдых", 160 | "date": "2023-11-07" 161 | } 162 | ] 163 | --------------------------------------------------------------------------------