Перейти к содержанию

Руководство разработчика

Содержание

  1. Начало работы
  2. Структура проекта
  3. Архитектура
  4. Стандарты кодирования
  5. Работа с Git
  6. Тестирование
  7. Отладка
  8. Добавление новых функций

Начало работы

Предварительные требования

  • Node.js 18.x или выше
  • PostgreSQL 14.x или выше
  • Git
  • Редактор кода (рекомендуется VS Code)

Настройка окружения разработки

  1. Клонирование репозитория

    git clone <repository-url>
    cd library_systemISPRAV-main
    

  2. Установка зависимостей

    # Frontend
    npm install
    
    # Backend
    cd server
    npm install
    cd ..
    

  3. Настройка переменных окружения

    cp server/env.example server/.env
    cp env.example .env
    

Отредактируйте .env файлы с настройками для разработки.

  1. Настройка базы данных
    cd server
    npm run migrate
    

Для чистой установки справочники подставляет миграция 030_seed_reference_data.sql.
Дополнительно (идемпотентно): node scripts/seed-reference-data.js.

  1. Запуск в режиме разработки
    # Терминал 1 - Backend
    cd server
    npm run dev
    
    # Терминал 2 - Frontend
    npm run dev
    

Структура проекта

Frontend (src/)

src/
├── components/          # React компоненты
│   ├── dialogs/        # Модальные окна
│   ├── ui/             # Базовые UI компоненты (shadcn/ui)
│   └── ...             # Другие компоненты
├── pages/              # Страницы приложения
├── hooks/              # Custom React hooks
├── lib/                # Утилиты и конфигурация
│   ├── api.ts          # API клиент
│   ├── constants.ts    # Константы
│   └── utils.ts        # Вспомогательные функции
└── types/              # TypeScript типы

Backend (server/)

server/
├── config/             # Конфигурация
│   ├── database.js     # Настройки БД
│   ├── env.js          # Валидация переменных окружения
│   └── ldap.js         # Настройки LDAP
├── models/             # Sequelize модели (Domain Layer)
├── repositories/       # Слой доступа к данным
│   ├── BaseRepository.js
│   ├── OperationQueryRepository.js
│   ├── OperationCommandRepository.js
│   └── ...
├── services/           # Бизнес-логика (Application Layer)
│   ├── operations/
│   └── ...
├── routes/             # API маршруты (Presentation Layer)
├── middleware/         # Express middleware
├── validators/         # Валидация данных
├── dto/                # Data Transfer Objects
├── utils/              # Утилиты
├── migrations/         # SQL миграции
└── scripts/            # Вспомогательные скрипты

Архитектура

Многослойная архитектура

Система следует принципам Layered Architecture:

  1. Presentation Layer (Routes)
  2. Обработка HTTP запросов
  3. Валидация входных данных
  4. Формирование ответов
  5. НЕ содержит бизнес-логику

  6. Application Layer (Services)

  7. Бизнес-логика
  8. Оркестрация операций
  9. Валидация бизнес-правил
  10. Координация между репозиториями

  11. Data Access Layer (Repositories)

  12. Доступ к данным
  13. Запросы к БД
  14. Маппинг данных

  15. Domain Layer (Models)

  16. Модели данных
  17. Валидация на уровне модели

CQRS Pattern

Для операций используется паттерн Command Query Responsibility Segregation:

  • OperationQueryRepository - методы чтения (queries)
  • OperationCommandRepository - методы изменения (commands)
  • OperationRepository - фасад, объединяющий оба репозитория

Outbox Pattern

Для надёжной обработки очередей бронирований используется синхронное продвижение + Outbox Pattern:

  1. В той же транзакции, что и возврат/отмена брони — запись в outbox_events (CopyBecameAvailable)
  2. После commitBookingService.processQueueForBook(bookCopyId) (возврат и отмена)
  3. Lazy expirationcancelExpiredPendingForCopy при бронировании, постановке в очередь и processQueueForBook
  4. Встроенный воркер server/workers/outboxWorker.js или контейнер outbox-worker (резерв)
  5. outboxProcessor.js → повторный processQueueForBook для необработанных событий
  6. После возврата: ReturnServicetriggerOutboxProcessing() для немедленного тика воркера

Переменные окружения:

Переменная Описание
OUTBOX_WORKER_ENABLED true / false (по умолчанию включён в docker-compose.yml)
OUTBOX_WORKER_INTERVAL_MS Интервал тика, мс (по умолчанию 10000)
OUTBOX_WORKER_MAX_EVENTS Лимит событий за раунд
OUTBOX_WORKER_MAX_ROUNDS Раундов за один тик воркера

Скрипты:

node scripts/process-outbox-events.js      # ручной / cron прогон
node scripts/reconcile-outbox-stuck.js     # закрыть устаревшие события
node scripts/cleanup-expired-bookings.js # просроченные брони + outbox

Подробнее: architecture.md, DEPLOYMENT.md.

Валидация и маршруты

  • Backend: Валидация сосредоточена в server/validators/ (express-validator). Файлы: bookValidators.js, userValidators.js, bookingValidators.js, operationValidators.js, referenceValidators.js, authValidators.js, userCategoryValidators.js, qrValidators.js, supplyValidators.js, supplierValidators.js. Реэкспорт из index.js. Маршруты подключают валидаторы и вызывают сервисы (не репозитории напрямую, где это внедрено: suppliers, supply, userCategories, languages, books/import и др.).
  • Frontend: Схемы форм (Zod) в src/lib/validators/: operationSchemas.ts, referenceSchemas.ts, userSchemas.ts, bookSchemas.ts, index.ts. В диалогах и страницах импортируйте схему и используйте zodResolver(schema) без дублирования определений.

Стандарты кодирования

TypeScript (Frontend)

  • Используйте строгую типизацию
  • Избегайте any, используйте конкретные типы
  • Используйте интерфейсы для объектов
  • Документируйте сложные функции
// ✅ Хорошо
interface User {
  id: string;
  name: string;
}

function getUser(id: string): Promise<User> {
  // ...
}

// ❌ Плохо
function getUser(id: any): Promise<any> {
  // ...
}

JavaScript (Backend)

  • Используйте ES6+ синтаксис
  • Следуйте стилю кода проекта
  • Используйте async/await вместо callbacks
  • Обрабатывайте ошибки
// ✅ Хорошо
async function createLoan(data) {
  try {
    const result = await LoanService.createLoan(data);
    return result;
  } catch (error) {
    logger.error('Error creating loan', { error, data });
    throw error;
  }
}

// ❌ Плохо
function createLoan(data, callback) {
  LoanService.createLoan(data, (err, result) => {
    if (err) {
      // ...
    }
    callback(null, result);
  });
}

Именование

  • Переменные и функции: camelCase
  • Классы: PascalCase
  • Константы: UPPER_SNAKE_CASE
  • Файлы: PascalCase.tsx для компонентов, camelCase.ts для утилит

Комментарии

  • Комментируйте сложную бизнес-логику
  • Используйте JSDoc для функций и классов
  • Объясняйте "почему", а не "что"
/**
 * Создает операцию выдачи книги с автоматическим выполнением бронирования
 * @param data - Данные для создания выдачи
 * @returns Созданная операция выдачи
 * @throws {Error} Если книга уже выдана или недоступна
 */
async function createLoan(data: CreateLoanData): Promise<Operation> {
  // ...
}

Работа с Git

Ветвление

  • main - стабильная версия для production
  • develop - ветка разработки
  • feature/* - новые функции
  • fix/* - исправления багов
  • refactor/* - рефакторинг

Коммиты

Используйте понятные сообщения коммитов:

feat: добавлена функция массовой выдачи книг
fix: исправлена ошибка при продлении выдачи
refactor: рефакторинг OperationRepository
docs: обновлена документация API

Pull Requests

  1. Создайте ветку от develop
  2. Внесите изменения
  3. Создайте Pull Request с описанием:
  4. Что изменено
  5. Почему изменено
  6. Как протестировано

Тестирование

CI (GitHub Actions)

Файл: .github/workflows/ci.yml

Job Шаги
Frontend build npm cinpm testnpm run build
Backend tests PostgreSQL 16 → reset-test-db.jsnpm test

Особенности backend job:

  • NODE_ENV=test, CI=true, SKIP_INIT_SQL=true
  • REDIS_URL="" — rate-limit in-memory, без зависания на Redis
  • OUTBOX_WORKER_ENABLED=false
  • Тестовая БД: postgresql://library_user:library_test@localhost:5432/library_test

Тестовая база данных

Скрипты в server/scripts/:

Команда Назначение
npm run reset:test-db DROP SCHEMA public CASCADE + миграции с нуля
npm run setup:test-db Только применить миграции (без DROP)

Важно: в CI и Jest init.sql не применяется — prod-дамп несовместим с построчным парсером (ломаются блоки $$...$$, pg_trgm, триггеры). Вместо этого:

  1. Таблицы создают миграции 001049
  2. Справочники — миграция 030_seed_reference_data.sql (pending, loan, return, conditions и др.)

Переменные тестового окружения: server/__tests__/env.js (загружается до всех тестов через Jest setupFiles).

Unit тесты

cd server
npm run test:unit

Критические сценарии (очередь, outbox, выдача/возврат, QR-лимиты)

cd server
npm run test:critical

Покрытие: booking-queue.test.js (lazy expiration, sync promotion после отмены, idempotency очереди, блокировка при удалении из очереди), critical-operations.test.js, outboxProcessor, loanEligibility.

Интеграционные тесты

Требуется БД library_test (см. TEST_DATABASE_URL в env.js):

cd server
npm run reset:test-db    # рекомендуется перед полным прогоном
npm run test:integration

Все тесты

cd server
npm test

Frontend

npm test        # Vitest
npm run build   # проверка production-сборки

После деплоя frontend: при ошибке «Failed to fetch dynamically imported module» срабатывает авто-перезагрузка (src/lib/chunkLoadRecovery.ts) — устаревший кэш index.html после обновления.

Сессия между вкладками: Layout вызывает ensureSessionFresh(); logout в одной вкладке рассылается через authSync.ts (BroadcastChannel).

Ручное тестирование

  1. Проверьте все сценарии использования
  2. Протестируйте граничные случаи
  3. Проверьте обработку ошибок
  4. Убедитесь в корректности валидации

Отладка

Frontend

React DevTools

Установите расширение React DevTools для браузера.

Console логи

console.log('Debug info', { data });
console.error('Error', error);

Breakpoints

Используйте breakpoints в DevTools браузера.

Backend

Логирование

const logger = require('../utils/logger');

logger.debug('Debug message', { data });
logger.info('Info message', { data });
logger.warn('Warning message', { data });
logger.error('Error message', { error, data });

Debug режим

node --inspect index.js

Подключитесь через Chrome DevTools: chrome://inspect

База данных

-- Просмотр активных подключений
SELECT * FROM pg_stat_activity;

-- Просмотр блокировок
SELECT * FROM pg_locks;

-- Анализ запросов
EXPLAIN ANALYZE SELECT * FROM operations WHERE ...

Добавление новых функций

Шаг 1: Планирование

  1. Определите требования
  2. Спроектируйте API
  3. Определите изменения в БД (если нужны)
  4. Создайте план реализации

Шаг 2: Миграции БД

Если нужны изменения в схеме БД, создайте файл server/migrations/0NN_описание.sql (следующий номер после последней миграции).

Нумерованные миграции применяются через:

cd server
npm run migrate

Справочные данные для чистой установки — в 030_seed_reference_data.sql (или отдельный seed-скрипт с ON CONFLICT DO NOTHING).

Пример:

-- server/migrations/050_add_new_table.sql
CREATE TABLE new_table (
  id SERIAL PRIMARY KEY,
  name VARCHAR(255) NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Шаг 3: Модель (если нужна)

// server/models/NewModel.js
module.exports = (sequelize, DataTypes) => {
  const NewModel = sequelize.define('NewModel', {
    id: {
      type: DataTypes.INTEGER,
      primaryKey: true,
      autoIncrement: true
    },
    name: {
      type: DataTypes.STRING,
      allowNull: false
    }
  }, {
    tableName: 'new_table',
    timestamps: true
  });

  return NewModel;
};

Шаг 4: Репозиторий

// server/repositories/NewRepository.js
const BaseRepository = require('./BaseRepository');
const { NewModel } = require('../models');

class NewRepository extends BaseRepository {
  constructor() {
    super(NewModel);
  }

  async findByName(name) {
    return await this.model.findOne({
      where: { name }
    });
  }
}

module.exports = new NewRepository();

Шаг 5: Сервис

// server/services/NewService.js
const NewRepository = require('../repositories/NewRepository');
const logger = require('../utils/logger');

class NewService {
  static async createNew(data) {
    // Валидация
    if (!data.name) {
      throw new Error('Name is required');
    }

    // Бизнес-логика
    const existing = await NewRepository.findByName(data.name);
    if (existing) {
      throw new Error('Name already exists');
    }

    // Создание
    const result = await NewRepository.create(data);
    logger.info('New item created', { id: result.id });

    return result;
  }

  static async getAll() {
    return await NewRepository.findAll();
  }
}

module.exports = NewService;

Шаг 6: Роут

// server/routes/new.js
const express = require('express');
const { body, validationResult } = require('express-validator');
const { authenticate, authorize } = require('../middleware/auth');
const NewService = require('../services/NewService');
const apiResponse = require('../utils/apiResponse');
const asyncHandler = require('../middleware/asyncHandler');

const router = express.Router();

router.post('/',
  authenticate,
  authorize('admin', 'librarian'),
  body('name').trim().notEmpty(),
  asyncHandler(async (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return apiResponse.unprocessableEntity(res, errors.array());
    }

    const result = await NewService.createNew(req.body);
    return apiResponse.created(res, result);
  })
);

router.get('/',
  authenticate,
  asyncHandler(async (req, res) => {
    const items = await NewService.getAll();
    return apiResponse.success(res, items);
  })
);

module.exports = router;

Шаг 7: Frontend API

// src/lib/api.ts
export const api = {
  // ...
  createNew: (data: CreateNewData) => 
    fetchAPI<NewItem>('/new', { 
      method: 'POST', 
      body: JSON.stringify(data) 
    }),

  getNewItems: () => 
    fetchAPI<NewItem[]>('/new'),
};

Шаг 8: Frontend компонент

// src/pages/NewPage.tsx
import { useState } from 'react';
import { api } from '@/lib/api';
import { toast } from 'sonner';

export default function NewPage() {
  const [items, setItems] = useState([]);
  const [loading, setLoading] = useState(false);

  const handleCreate = async (data) => {
    setLoading(true);
    try {
      const newItem = await api.createNew(data);
      toast.success('Item created');
      setItems([...items, newItem]);
    } catch (error) {
      toast.error(error.message);
    } finally {
      setLoading(false);
    }
  };

  // ...
}

Шаг 9: Тестирование

  1. Протестируйте API через Swagger или Postman
  2. Протестируйте UI компоненты
  3. Проверьте обработку ошибок
  4. Убедитесь в корректности валидации

Шаг 10: Документация

  1. Обновите API документацию
  2. Добавьте примеры использования
  3. Обновите руководство пользователя (если нужно)

Полезные команды

Backend

# Запуск в режиме разработки
npm run dev

# Запуск миграций
npm run migrate

# Первичный prod-дамп (только при миграции со старой БД)
npm run migrate:init

# Тестовая БД
npm run reset:test-db
npm run setup:test-db

# Запуск тестов
npm test

# Линтинг
npm run lint

# Форматирование
npm run format

Frontend

# Запуск в режиме разработки
npm run dev

# Сборка для production
npm run build

# Просмотр production сборки
npm run preview

# Линтинг
npm run lint

# Проверка типов
npm run type-check

Полезные ресурсы

Контакты и поддержка

При возникновении вопросов: 1. Проверьте документацию 2. Поищите в issues на GitHub 3. Создайте новый issue с описанием проблемы