diff --git a/README.md b/README.md index 44e38d3..b100000 100644 --- a/README.md +++ b/README.md @@ -102,10 +102,24 @@ To showcase extended capabilities: ## πŸ“š Documentation +### General Documentation - [Detailed Architecture](./docs/architecture.md) - [SQLite Integration](./docs/sqlite-integration.md) - [Complete Demo Script](./docs/demo-script.md) +### API Documentation +- [API Overview](./api/README.md) - Quick start, configuration, and architecture +- [Database Schema](./api/docs/database.md) - Tables, migrations, and seed data +- [Data Models](./api/docs/models.md) - TypeScript entity interfaces and relationships +- [API Endpoints](./api/docs/endpoints.md) - Complete REST API reference +- [Repository Pattern](./api/docs/repository-pattern.md) - Data access layer architecture +- [Development Guide](./api/docs/development.md) - Setup, testing, and deployment + +### Interactive API Documentation +When the API is running, visit: +- **Swagger UI**: http://localhost:3000/api-docs +- **OpenAPI JSON**: http://localhost:3000/api-docs.json + Database defaults and env vars: - DB file: `api/data/app.db` (override with `DB_FILE=/absolute/path/to/file.db`) - Enable WAL: `DB_ENABLE_WAL=true` (default) diff --git a/api/README.md b/api/README.md new file mode 100644 index 0000000..7289043 --- /dev/null +++ b/api/README.md @@ -0,0 +1,290 @@ +# OctoCAT Supply Chain Management API + +A TypeScript-based REST API built with Express.js and SQLite, following the repository pattern for clean data access and maintainability. + +## πŸ—οΈ Architecture Overview + +This API is part of the OctoCAT Supply Chain Management system, providing RESTful endpoints for managing: +- **Suppliers** - Vendor information and contact details +- **Headquarters** - Company headquarters data +- **Branches** - Branch locations linked to headquarters +- **Products** - Product catalog with pricing and supplier relationships +- **Orders** - Customer orders placed at branches +- **Order Details** - Individual line items in orders +- **Deliveries** - Delivery tracking from suppliers +- **Order Detail Deliveries** - Junction table linking order details to deliveries + +### Technology Stack + +- **Runtime**: Node.js with TypeScript +- **Framework**: Express.js 4.x +- **Database**: SQLite 3.x with WAL mode +- **API Documentation**: Swagger/OpenAPI 3.0 +- **Testing**: Vitest with in-memory database +- **Architecture Pattern**: Repository pattern with dependency injection + +### Directory Structure + +``` +api/ +β”œβ”€β”€ src/ +β”‚ β”œβ”€β”€ db/ # Database configuration and utilities +β”‚ β”‚ β”œβ”€β”€ config.ts # Database configuration +β”‚ β”‚ β”œβ”€β”€ sqlite.ts # SQLite connection management +β”‚ β”‚ β”œβ”€β”€ migrate.ts # Migration runner +β”‚ β”‚ └── seed.ts # Database seeding +β”‚ β”œβ”€β”€ models/ # TypeScript interfaces for entities +β”‚ β”‚ β”œβ”€β”€ branch.ts +β”‚ β”‚ β”œβ”€β”€ delivery.ts +β”‚ β”‚ β”œβ”€β”€ headquarters.ts +β”‚ β”‚ β”œβ”€β”€ order.ts +β”‚ β”‚ β”œβ”€β”€ orderDetail.ts +β”‚ β”‚ β”œβ”€β”€ orderDetailDelivery.ts +β”‚ β”‚ β”œβ”€β”€ product.ts +β”‚ β”‚ └── supplier.ts +β”‚ β”œβ”€β”€ repositories/ # Data access layer +β”‚ β”‚ β”œβ”€β”€ branchesRepo.ts +β”‚ β”‚ β”œβ”€β”€ deliveriesRepo.ts +β”‚ β”‚ β”œβ”€β”€ headquartersRepo.ts +β”‚ β”‚ β”œβ”€β”€ orderDetailsRepo.ts +β”‚ β”‚ β”œβ”€β”€ orderDetailDeliveriesRepo.ts +β”‚ β”‚ β”œβ”€β”€ ordersRepo.ts +β”‚ β”‚ β”œβ”€β”€ productsRepo.ts +β”‚ β”‚ └── suppliersRepo.ts +β”‚ β”œβ”€β”€ routes/ # Express route handlers +β”‚ β”‚ β”œβ”€β”€ branch.ts +β”‚ β”‚ β”œβ”€β”€ delivery.ts +β”‚ β”‚ β”œβ”€β”€ headquarters.ts +β”‚ β”‚ β”œβ”€β”€ order.ts +β”‚ β”‚ β”œβ”€β”€ orderDetail.ts +β”‚ β”‚ β”œβ”€β”€ orderDetailDelivery.ts +β”‚ β”‚ β”œβ”€β”€ product.ts +β”‚ β”‚ └── supplier.ts +β”‚ β”œβ”€β”€ utils/ # Utility functions +β”‚ β”‚ β”œβ”€β”€ errors.ts # Custom error classes +β”‚ β”‚ └── sql.ts # SQL query helpers +β”‚ β”œβ”€β”€ index.ts # Application entry point +β”‚ └── init-db.ts # Database initialization script +β”œβ”€β”€ sql/ +β”‚ β”œβ”€β”€ migrations/ # SQL migration files +β”‚ β”‚ └── 001_init.sql +β”‚ └── seed/ # SQL seed data files +β”‚ β”œβ”€β”€ 001_suppliers.sql +β”‚ β”œβ”€β”€ 002_headquarters.sql +β”‚ β”œβ”€β”€ 003_branches.sql +β”‚ β”œβ”€β”€ 004_products.sql +β”‚ β”œβ”€β”€ 005_orders.sql +β”‚ β”œβ”€β”€ 006_order_details.sql +β”‚ β”œβ”€β”€ 007_deliveries.sql +β”‚ └── 008_order_detail_deliveries.sql +β”œβ”€β”€ docs/ # Additional documentation +β”œβ”€β”€ api-swagger.json # OpenAPI specification +β”œβ”€β”€ ERD.png # Entity Relationship Diagram +└── package.json +``` + +## πŸš€ Quick Start + +### Prerequisites + +- Node.js 18+ and npm +- Basic understanding of TypeScript and Express.js + +### Installation + +From the repository root: + +```bash +# Install all dependencies +npm install + +# Install API dependencies only +npm install --workspace=api +``` + +### Database Initialization + +The database will be automatically initialized when you start the server. To manually initialize: + +```bash +# Initialize database with migrations and seed data +npm run db:init --workspace=api + +# Run migrations only +npm run db:migrate --workspace=api + +# Seed data only +npm run db:seed --workspace=api +``` + +### Running the API + +```bash +# Development mode with hot reload +npm run dev --workspace=api + +# Build TypeScript +npm run build --workspace=api + +# Production mode +npm run start --workspace=api +``` + +The API will start on `http://localhost:3000`. + +### API Documentation + +Once running, visit: +- **Swagger UI**: http://localhost:3000/api-docs +- **OpenAPI JSON**: http://localhost:3000/api-docs.json + +## βš™οΈ Configuration + +### Environment Variables + +Configure the API using environment variables: + +| Variable | Default | Description | +|----------|---------|-------------| +| `PORT` | `3000` | API server port | +| `NODE_ENV` | `development` | Environment (development/production) | +| `DB_FILE` | `./data/app.db` | SQLite database file path (absolute or relative to api/) | +| `DB_ENGINE` | `sqlite` | Database engine (currently only sqlite supported) | +| `DB_ENABLE_WAL` | `true` | Enable Write-Ahead Logging for better concurrency | +| `DB_TIMEOUT` | `30000` | Database connection timeout (milliseconds) | +| `DB_FOREIGN_KEYS` | `true` | Enable foreign key constraints | + +### Example Configuration + +```bash +# .env file or export commands +export PORT=3001 +export DB_FILE=/absolute/path/to/database.db +export DB_ENABLE_WAL=true +export DB_FOREIGN_KEYS=true +``` + +## πŸ“š Additional Documentation + +Detailed documentation is available in the `docs/` directory: + +- **[Database Documentation](./docs/database.md)** - Schema, migrations, and seed data +- **[Models Documentation](./docs/models.md)** - All entity models and their relationships +- **[Endpoints Documentation](./docs/endpoints.md)** - Complete API endpoint reference +- **[Repository Pattern](./docs/repository-pattern.md)** - Data access layer architecture +- **[Development Guide](./docs/development.md)** - Setup, testing, and deployment + +Also see the repository-level documentation: +- [Architecture Overview](../docs/architecture.md) +- [SQLite Integration Guide](../docs/sqlite-integration.md) + +## πŸ§ͺ Testing + +```bash +# Run all tests +npm run test --workspace=api + +# Run tests with coverage +npm run test:coverage --workspace=api + +# Run tests in watch mode +npm run test --workspace=api -- --watch +``` + +Tests use an in-memory SQLite database for fast, isolated execution. + +## πŸ” API Design Principles + +### Repository Pattern + +The API follows the repository pattern to separate business logic from data access: +- **Models** define TypeScript interfaces for entities +- **Repositories** handle all database operations (CRUD + custom queries) +- **Routes** orchestrate requests, call repositories, and return responses + +### Error Handling + +The API uses custom error classes that automatically map to HTTP status codes: +- `NotFoundError` β†’ 404 +- `ValidationError` β†’ 400 +- `ConflictError` β†’ 409 +- `DatabaseError` β†’ 500 + +All errors are handled by Express middleware and return consistent JSON responses. + +### Data Mapping + +- Database columns use `snake_case` (e.g., `product_id`) +- TypeScript models use `camelCase` (e.g., `productId`) +- Automatic mapping is handled by the `objectToCamelCase` utility + +### SQL Injection Protection + +All database queries use parameterized statements to prevent SQL injection attacks. + +## πŸ—οΈ Project Scripts + +From the repository root: + +```bash +# Build API +npm run build --workspace=api + +# Start API (production) +npm run start --workspace=api + +# Development mode +npm run dev --workspace=api + +# Run tests +npm run test --workspace=api + +# Database management +npm run db:init --workspace=api # Initialize with migrations + seed +npm run db:migrate --workspace=api # Run migrations only +npm run db:seed --workspace=api # Seed data only +``` + +## πŸ“ˆ Performance Considerations + +- **Indexes** on foreign keys and frequently queried columns +- **WAL mode** enabled by default for better concurrency +- **Connection pooling** via singleton repository pattern +- **Parameterized queries** for query plan caching + +## πŸ”’ Security + +- Parameterized SQL queries prevent SQL injection +- CORS enabled (configure for production) +- Foreign key constraints enforce referential integrity +- Error messages don't expose sensitive information in production + +## πŸ“ Contributing + +When making changes to the API: + +1. Follow existing code patterns and naming conventions +2. Update Swagger documentation in model/route files +3. Add tests for new repository methods +4. Run migrations for schema changes (never modify existing migrations) +5. Update this README if adding new features or changing configuration + +## πŸ› Troubleshooting + +### Database Locked Error +**Cause**: Long-running transactions or unclosed connections +**Solution**: Ensure all database operations are properly awaited + +### Foreign Key Constraint Error +**Cause**: Trying to reference non-existent records +**Solution**: Verify referenced entities exist before creating relationships + +### Migration Errors +**Cause**: SQL syntax errors or conflicting schema changes +**Solution**: Check migration file syntax and test against current schema + +For more troubleshooting tips, see the [Development Guide](./docs/development.md). + +## πŸ“„ License + +MIT License - See the repository root for details. diff --git a/api/docs/database.md b/api/docs/database.md new file mode 100644 index 0000000..59244db --- /dev/null +++ b/api/docs/database.md @@ -0,0 +1,441 @@ +# Database Documentation + +This document provides comprehensive information about the SQLite database schema, migrations, and seed data for the OctoCAT Supply Chain Management API. + +## Overview + +The API uses SQLite as its persistence layer with the following characteristics: +- **File-based storage**: `api/data/app.db` (configurable via `DB_FILE` environment variable) +- **WAL mode**: Enabled by default for better concurrency +- **Foreign keys**: Enforced to maintain referential integrity +- **Test mode**: In-memory database (`:memory:`) for fast, isolated tests + +## Database Schema + +The database follows the Entity Relationship Diagram (ERD) at `api/ERD.png`. + +### Schema Diagram + +```mermaid +erDiagram + Headquarters ||--o{ Branch: has + Branch ||--o{ Order: placed_at + Order ||--o{ OrderDetail: contains + OrderDetail ||--o{ OrderDetailDelivery: fulfilled_by + OrderDetail }|--|| Product: references + Delivery ||--o{ OrderDetailDelivery: includes + Supplier ||--o{ Delivery: provides + Supplier ||--o{ Product: supplies +``` + +## Tables + +### suppliers + +Stores supplier/vendor information. + +| Column | Type | Constraints | Description | +|--------|------|-------------|-------------| +| `supplier_id` | INTEGER | PRIMARY KEY | Unique identifier | +| `name` | TEXT | NOT NULL | Supplier company name | +| `description` | TEXT | | Company description | +| `contact_person` | TEXT | | Primary contact name | +| `email` | TEXT | | Contact email address | +| `phone` | TEXT | | Contact phone number | + +**Relationships:** +- One-to-many with `products` +- One-to-many with `deliveries` + +### headquarters + +Stores company headquarters information. + +| Column | Type | Constraints | Description | +|--------|------|-------------|-------------| +| `headquarters_id` | INTEGER | PRIMARY KEY | Unique identifier | +| `name` | TEXT | NOT NULL | Headquarters name | +| `description` | TEXT | | Description | +| `address` | TEXT | | Physical address | +| `contact_person` | TEXT | | Primary contact name | +| `email` | TEXT | | Contact email address | +| `phone` | TEXT | | Contact phone number | + +**Relationships:** +- One-to-many with `branches` + +### branches + +Stores branch location information. + +| Column | Type | Constraints | Description | +|--------|------|-------------|-------------| +| `branch_id` | INTEGER | PRIMARY KEY | Unique identifier | +| `headquarters_id` | INTEGER | NOT NULL, FK | Reference to headquarters | +| `name` | TEXT | NOT NULL | Branch name | +| `description` | TEXT | | Description | +| `address` | TEXT | | Physical address | +| `contact_person` | TEXT | | Primary contact name | +| `email` | TEXT | | Contact email address | +| `phone` | TEXT | | Contact phone number | + +**Relationships:** +- Many-to-one with `headquarters` +- One-to-many with `orders` + +**Foreign Keys:** +- `headquarters_id` β†’ `headquarters(headquarters_id)` ON DELETE CASCADE + +**Indexes:** +- `idx_branches_headquarters_id` on `headquarters_id` + +### products + +Stores product catalog information. + +| Column | Type | Constraints | Description | +|--------|------|-------------|-------------| +| `product_id` | INTEGER | PRIMARY KEY | Unique identifier | +| `supplier_id` | INTEGER | NOT NULL, FK | Reference to supplier | +| `name` | TEXT | NOT NULL | Product name | +| `description` | TEXT | | Product description | +| `price` | REAL | NOT NULL | Current price | +| `sku` | TEXT | NOT NULL | Stock Keeping Unit code | +| `unit` | TEXT | NOT NULL | Unit of measurement (e.g., "box", "pallet") | +| `img_name` | TEXT | | Product image filename | +| `discount` | REAL | DEFAULT 0.0 | Discount percentage (0.0-1.0) | + +**Relationships:** +- Many-to-one with `suppliers` +- One-to-many with `order_details` + +**Foreign Keys:** +- `supplier_id` β†’ `suppliers(supplier_id)` ON DELETE CASCADE + +**Indexes:** +- `idx_products_supplier_id` on `supplier_id` +- `idx_products_sku` on `sku` + +### orders + +Stores customer order information. + +| Column | Type | Constraints | Description | +|--------|------|-------------|-------------| +| `order_id` | INTEGER | PRIMARY KEY | Unique identifier | +| `branch_id` | INTEGER | NOT NULL, FK | Branch that placed the order | +| `order_date` | TEXT | NOT NULL | Order date (ISO 8601 format) | +| `name` | TEXT | NOT NULL | Order name/reference | +| `description` | TEXT | | Order description | +| `status` | TEXT | NOT NULL, DEFAULT 'pending' | Order status (pending, processing, completed, cancelled) | + +**Relationships:** +- Many-to-one with `branches` +- One-to-many with `order_details` + +**Foreign Keys:** +- `branch_id` β†’ `branches(branch_id)` ON DELETE CASCADE + +**Indexes:** +- `idx_orders_branch_id` on `branch_id` +- `idx_orders_status` on `status` + +### order_details + +Stores individual line items for orders. + +| Column | Type | Constraints | Description | +|--------|------|-------------|-------------| +| `order_detail_id` | INTEGER | PRIMARY KEY | Unique identifier | +| `order_id` | INTEGER | NOT NULL, FK | Reference to order | +| `product_id` | INTEGER | NOT NULL, FK | Reference to product | +| `quantity` | INTEGER | NOT NULL | Quantity ordered | +| `unit_price` | REAL | NOT NULL | Price per unit at time of order | +| `notes` | TEXT | | Additional notes | + +**Relationships:** +- Many-to-one with `orders` +- Many-to-one with `products` +- One-to-many with `order_detail_deliveries` + +**Foreign Keys:** +- `order_id` β†’ `orders(order_id)` ON DELETE CASCADE +- `product_id` β†’ `products(product_id)` ON DELETE CASCADE + +**Indexes:** +- `idx_order_details_order_id` on `order_id` +- `idx_order_details_product_id` on `product_id` + +### deliveries + +Stores delivery information from suppliers. + +| Column | Type | Constraints | Description | +|--------|------|-------------|-------------| +| `delivery_id` | INTEGER | PRIMARY KEY | Unique identifier | +| `supplier_id` | INTEGER | NOT NULL, FK | Supplier making the delivery | +| `delivery_date` | TEXT | NOT NULL | Delivery date (ISO 8601 format) | +| `name` | TEXT | NOT NULL | Delivery name/reference | +| `description` | TEXT | | Delivery description | +| `status` | TEXT | NOT NULL, DEFAULT 'pending' | Delivery status (pending, in_transit, delivered, cancelled) | + +**Relationships:** +- Many-to-one with `suppliers` +- One-to-many with `order_detail_deliveries` + +**Foreign Keys:** +- `supplier_id` β†’ `suppliers(supplier_id)` ON DELETE CASCADE + +**Indexes:** +- `idx_deliveries_supplier_id` on `supplier_id` +- `idx_deliveries_status` on `status` + +### order_detail_deliveries + +Junction table linking order details to deliveries. + +| Column | Type | Constraints | Description | +|--------|------|-------------|-------------| +| `order_detail_delivery_id` | INTEGER | PRIMARY KEY | Unique identifier | +| `order_detail_id` | INTEGER | NOT NULL, FK | Reference to order detail | +| `delivery_id` | INTEGER | NOT NULL, FK | Reference to delivery | +| `quantity` | INTEGER | NOT NULL | Quantity fulfilled by this delivery | +| `notes` | TEXT | | Additional notes | + +**Relationships:** +- Many-to-one with `order_details` +- Many-to-one with `deliveries` + +**Foreign Keys:** +- `order_detail_id` β†’ `order_details(order_detail_id)` ON DELETE CASCADE +- `delivery_id` β†’ `deliveries(delivery_id)` ON DELETE CASCADE + +**Indexes:** +- `idx_order_detail_deliveries_order_detail_id` on `order_detail_id` +- `idx_order_detail_deliveries_delivery_id` on `delivery_id` + +## Migrations System + +Database schema changes are managed through SQL migration files in `api/sql/migrations/`. + +### Migration Files + +Migrations are executed in sequential order based on filename: +- `001_init.sql` - Initial schema creation + +### Migration Tracking + +The system maintains a `migrations` table to track which migrations have been executed: + +```sql +CREATE TABLE migrations ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + executed_at TEXT NOT NULL +); +``` + +### Running Migrations + +```bash +# Run all pending migrations +npm run db:migrate --workspace=api + +# Or initialize database with migrations + seed data +npm run db:init --workspace=api +``` + +### Creating New Migrations + +To add a new migration: + +1. Create a new file: `api/sql/migrations/00X_description.sql` +2. Add your SQL statements +3. Run migrations to apply changes + +**Important Rules:** +- Never modify existing migration files +- Always increment the migration number +- Test migrations on a copy of the database first +- Ensure migrations are idempotent when possible (use `IF NOT EXISTS` clauses) + +## Seed Data + +Sample data is provided through SQL files in `api/sql/seed/` for demo and testing purposes. + +### Seed Files + +Executed in order: +1. `001_suppliers.sql` - Sample supplier data +2. `002_headquarters.sql` - Sample headquarters data +3. `003_branches.sql` - Sample branch data +4. `004_products.sql` - Sample product catalog +5. `005_orders.sql` - Sample orders +6. `006_order_details.sql` - Sample order line items +7. `007_deliveries.sql` - Sample delivery data +8. `008_order_detail_deliveries.sql` - Sample delivery fulfillments + +### Running Seed Data + +```bash +# Seed the database +npm run db:seed --workspace=api + +# Or initialize with migrations + seed +npm run db:init --workspace=api +``` + +### Seed Data Characteristics + +- **Deterministic**: Produces consistent data each run +- **Referential Integrity**: Maintains proper foreign key relationships +- **Realistic**: Provides meaningful demo data +- **Resettable**: Can be cleared and re-seeded for testing + +## Database Configuration + +Configuration is managed in `api/src/db/config.ts`: + +```typescript +export const DB_CONFIG = { + DB_FILE: process.env.DB_FILE || './data/app.db', + DB_ENGINE: process.env.DB_ENGINE || 'sqlite', + ENABLE_WAL: process.env.DB_ENABLE_WAL !== 'false', + TIMEOUT: parseInt(process.env.DB_TIMEOUT || '30000'), + FOREIGN_KEYS: process.env.DB_FOREIGN_KEYS !== 'false' +}; +``` + +### Configuration Options + +- **DB_FILE**: Database file path (absolute or relative to `api/`) +- **DB_ENGINE**: Database engine (currently only `sqlite`) +- **ENABLE_WAL**: Enable Write-Ahead Logging for better concurrency +- **TIMEOUT**: Connection timeout in milliseconds +- **FOREIGN_KEYS**: Enable foreign key constraint enforcement + +## Performance Optimizations + +### Indexes + +Indexes are created on: +- All foreign key columns for faster JOIN operations +- Frequently queried columns (`status`, `sku`) +- Composite indexes for common query patterns + +### WAL Mode + +Write-Ahead Logging (WAL) mode is enabled by default, providing: +- Better concurrency (readers don't block writers) +- Faster write performance +- More robust crash recovery + +### Query Optimization + +- **Parameterized queries**: Allows query plan caching +- **Selective columns**: Repositories only select needed columns +- **Efficient JOINs**: Indexes support optimal JOIN execution + +## Backup and Recovery + +### Manual Backup + +```bash +# Backup the database file +cp api/data/app.db api/data/app-backup-$(date +%Y%m%d-%H%M%S).db +``` + +### Restore from Backup + +```bash +# Restore from a backup +cp api/data/app-backup-YYYYMMDD-HHMMSS.db api/data/app.db +``` + +### Automated Backups + +For production deployments, implement automated backup strategies: +- Regular scheduled backups +- Off-site storage +- Backup verification + +## Testing Strategy + +### Test Database + +Tests use an in-memory SQLite database (`:memory:`) for: +- **Speed**: No disk I/O overhead +- **Isolation**: Each test suite gets a fresh database +- **Cleanup**: Database is destroyed after tests complete + +### Test Database Setup + +```typescript +import { getDatabase } from '../db/sqlite'; + +// Create in-memory database for testing +const db = await getDatabase(true); // true = test mode +``` + +## Troubleshooting + +### Database Locked Error + +**Symptoms**: `SQLITE_BUSY: database is locked` errors + +**Causes**: +- Long-running transactions +- Unclosed database connections +- Concurrent writes without WAL mode + +**Solutions**: +1. Ensure all database operations are properly awaited +2. Enable WAL mode (default) +3. Increase timeout setting +4. Check for unclosed connections in error handlers + +### Foreign Key Constraint Violations + +**Symptoms**: `FOREIGN KEY constraint failed` errors + +**Causes**: +- Attempting to insert records that reference non-existent parent records +- Deleting parent records without handling dependent records + +**Solutions**: +1. Verify referenced entities exist before creating relationships +2. Use CASCADE delete when appropriate +3. Check the order of data insertion in seed files + +### Migration Errors + +**Symptoms**: Migration fails to execute + +**Causes**: +- SQL syntax errors +- Schema conflicts with existing structure +- Missing migration dependencies + +**Solutions**: +1. Test migration SQL manually using SQLite CLI +2. Verify migration order and dependencies +3. Check for conflicting column/table names +4. Review migration tracking table for execution history + +## Best Practices + +1. **Never modify existing migrations** - Always create new migration files +2. **Use transactions** for multi-statement migrations +3. **Test seed data order** - Ensure foreign key relationships are satisfied +4. **Keep seeds minimal** - Only include data needed for demos/testing +5. **Document schema changes** - Add comments to complex migrations +6. **Verify data integrity** - Run checks after seeding +7. **Use indexes wisely** - Balance query performance with write overhead +8. **Monitor database size** - SQLite files can grow indefinitely + +## Additional Resources + +- [SQLite Documentation](https://www.sqlite.org/docs.html) +- [SQLite WAL Mode](https://www.sqlite.org/wal.html) +- [Repository Pattern](./repository-pattern.md) +- [Development Guide](./development.md) diff --git a/api/docs/development.md b/api/docs/development.md new file mode 100644 index 0000000..c889c76 --- /dev/null +++ b/api/docs/development.md @@ -0,0 +1,684 @@ +# Development Guide + +Complete guide for setting up, developing, testing, and deploying the OctoCAT Supply Chain Management API. + +## Table of Contents + +- [Local Setup](#local-setup) +- [Development Workflow](#development-workflow) +- [Running Tests](#running-tests) +- [Building](#building) +- [Database Management](#database-management) +- [Code Quality](#code-quality) +- [Debugging](#debugging) +- [Deployment](#deployment) +- [Troubleshooting](#troubleshooting) + +--- + +## Local Setup + +### Prerequisites + +- **Node.js**: 18.x or higher +- **npm**: 9.x or higher +- **Git**: For version control +- **VS Code** (recommended): For best development experience + +### Initial Setup + +1. **Clone the repository**: + ```bash + git clone https://github.com/webmaxru/copilot_agent_mode.git + cd copilot_agent_mode + ``` + +2. **Install dependencies**: + ```bash + # Install all workspace dependencies (root + api + frontend) + npm install + + # Or install API dependencies only + npm install --workspace=api + ``` + +3. **Initialize the database**: + ```bash + # Initialize database with migrations and seed data + npm run db:init --workspace=api + ``` + +4. **Start the development server**: + ```bash + # Start API in development mode with hot reload + npm run dev --workspace=api + ``` + +5. **Verify the setup**: + - API: http://localhost:3000 + - Swagger UI: http://localhost:3000/api-docs + - Test endpoint: http://localhost:3000/api/products + +--- + +## Development Workflow + +### Project Structure + +``` +api/ +β”œβ”€β”€ src/ +β”‚ β”œβ”€β”€ db/ # Database configuration and utilities +β”‚ β”œβ”€β”€ models/ # TypeScript entity interfaces +β”‚ β”œβ”€β”€ repositories/ # Data access layer +β”‚ β”œβ”€β”€ routes/ # Express route handlers +β”‚ β”œβ”€β”€ utils/ # Utility functions +β”‚ β”œβ”€β”€ index.ts # Application entry point +β”‚ └── init-db.ts # Database initialization script +β”œβ”€β”€ sql/ +β”‚ β”œβ”€β”€ migrations/ # SQL schema migrations +β”‚ └── seed/ # SQL seed data +β”œβ”€β”€ docs/ # API documentation +└── package.json +``` + +### Development Mode + +The API uses `tsx` for hot-reloading during development: + +```bash +npm run dev --workspace=api +``` + +This automatically: +- Compiles TypeScript on the fly +- Restarts the server on file changes +- Initializes the database if needed + +### Making Code Changes + +1. **Create a new branch**: + ```bash + git checkout -b feature/your-feature-name + ``` + +2. **Make your changes**: + - Follow existing code patterns + - Update Swagger annotations in models/routes + - Add tests for new functionality + +3. **Test your changes**: + ```bash + npm run test --workspace=api + ``` + +4. **Build to verify**: + ```bash + npm run build --workspace=api + ``` + +5. **Commit and push**: + ```bash + git add . + git commit -m "Description of changes" + git push origin feature/your-feature-name + ``` + +### Adding a New Entity + +To add a new entity (e.g., `Category`): + +1. **Create migration**: + - Add `api/sql/migrations/002_add_categories.sql` + + ```sql + CREATE TABLE categories ( + category_id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + description TEXT + ); + ``` + +2. **Create model**: + - Add `api/src/models/category.ts` + + ```typescript + export interface Category { + categoryId: number; + name: string; + description: string; + } + ``` + +3. **Create repository**: + - Add `api/src/repositories/categoriesRepo.ts` + - Implement CRUD methods following existing patterns + +4. **Create routes**: + - Add `api/src/routes/category.ts` + - Register routes in `api/src/index.ts` + +5. **Add tests**: + - Add `api/src/repositories/categoriesRepo.test.ts` + - Add `api/src/routes/category.test.ts` + +6. **Update Swagger**: + - Add JSDoc annotations in model and route files + +7. **Run migrations**: + ```bash + npm run db:migrate --workspace=api + ``` + +--- + +## Running Tests + +### Test Commands + +```bash +# Run all tests +npm run test --workspace=api + +# Run tests with coverage report +npm run test:coverage --workspace=api + +# Run tests in watch mode (for development) +npm run test --workspace=api -- --watch + +# Run specific test file +npm run test --workspace=api -- src/repositories/productsRepo.test.ts +``` + +### Test Structure + +Tests are colocated with source files: +- Unit tests: `*.test.ts` files next to the code they test +- Test framework: Vitest +- Database: In-memory SQLite (`:memory:`) for fast, isolated tests + +### Writing Tests + +#### Repository Tests + +```typescript +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { ProductsRepository } from './productsRepo'; + +describe('ProductsRepository', () => { + let mockDb: any; + let repo: ProductsRepository; + + beforeEach(() => { + mockDb = { + run: vi.fn(), + get: vi.fn(), + all: vi.fn(), + close: vi.fn() + }; + repo = new ProductsRepository(mockDb); + }); + + it('should find all products', async () => { + mockDb.all.mockResolvedValue([ + { product_id: 1, name: 'Product 1', price: 10.0 } + ]); + + const products = await repo.findAll(); + + expect(products).toHaveLength(1); + expect(products[0].productId).toBe(1); + }); +}); +``` + +#### Route Tests + +```typescript +import { describe, it, expect } from 'vitest'; +import request from 'supertest'; +import app from '../index'; + +describe('GET /api/products', () => { + it('should return all products', async () => { + const response = await request(app) + .get('/api/products') + .expect(200); + + expect(Array.isArray(response.body)).toBe(true); + }); +}); +``` + +### Test Coverage + +View coverage report: + +```bash +npm run test:coverage --workspace=api +``` + +Coverage reports are generated in `api/coverage/`: +- `coverage/index.html` - HTML coverage report +- `coverage/lcov.info` - LCOV format for CI tools + +--- + +## Building + +### Build Commands + +```bash +# Build TypeScript to JavaScript +npm run build --workspace=api + +# Build from repository root +npm run build +``` + +### Build Output + +- **Output directory**: `api/dist/` +- **Entry point**: `api/dist/index.js` + +### Build Configuration + +TypeScript configuration is in `api/tsconfig.json`: + +```json +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true + } +} +``` + +### Running Built Code + +```bash +# Build first +npm run build --workspace=api + +# Run the built application +npm run start --workspace=api +``` + +--- + +## Database Management + +### Database Commands + +```bash +# Initialize database (migrations + seed) +npm run db:init --workspace=api + +# Run migrations only +npm run db:migrate --workspace=api + +# Seed data only +npm run db:seed --workspace=api +``` + +### Database Location + +- **Development**: `api/data/app.db` +- **Testing**: `:memory:` (in-memory) +- **Custom location**: Set `DB_FILE` environment variable + +### Database Configuration + +Configure via environment variables: + +```bash +export DB_FILE=/path/to/database.db +export DB_ENABLE_WAL=true +export DB_FOREIGN_KEYS=true +export DB_TIMEOUT=30000 +``` + +### Viewing Database Contents + +Using SQLite CLI: + +```bash +# Open database +sqlite3 api/data/app.db + +# List tables +.tables + +# View schema +.schema products + +# Query data +SELECT * FROM products LIMIT 5; + +# Exit +.quit +``` + +### Database Migrations + +1. **Create a new migration**: + ```bash + # Create file: api/sql/migrations/002_description.sql + ``` + +2. **Add SQL statements**: + ```sql + -- Add new column + ALTER TABLE products ADD COLUMN category TEXT; + + -- Create index + CREATE INDEX idx_products_category ON products(category); + ``` + +3. **Run migration**: + ```bash + npm run db:migrate --workspace=api + ``` + +**Important**: Never modify existing migration files. Always create new ones. + +### Resetting the Database + +```bash +# Delete database file +rm api/data/app.db + +# Reinitialize +npm run db:init --workspace=api +``` + +--- + +## Code Quality + +### Linting + +Currently, no linter is configured. Consider adding ESLint: + +```bash +# Install ESLint (example) +npm install --save-dev --workspace=api \ + eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin +``` + +### Code Formatting + +Consider adding Prettier for consistent formatting: + +```bash +# Install Prettier (example) +npm install --save-dev --workspace=api prettier +``` + +### Type Checking + +Verify TypeScript types: + +```bash +# Check for type errors without building +npx tsc --noEmit --workspace=api +``` + +--- + +## Debugging + +### VS Code Debugging + +1. **Launch configuration** (`.vscode/launch.json`): + ```json + { + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Debug API", + "program": "${workspaceFolder}/api/src/index.ts", + "cwd": "${workspaceFolder}/api", + "runtimeExecutable": "npx", + "runtimeArgs": ["tsx"], + "skipFiles": ["/**"] + } + ] + } + ``` + +2. **Set breakpoints** in source files +3. **Press F5** to start debugging + +### Console Logging + +Add logging to track execution: + +```typescript +console.log('Processing product:', product); +console.error('Error occurred:', error); +``` + +### Database Query Logging + +Enable verbose logging: + +```typescript +// In api/src/db/sqlite.ts +db.on('trace', (sql) => { + console.log('SQL:', sql); +}); +``` + +--- + +## Deployment + +### Environment Variables + +Set required environment variables: + +```bash +export NODE_ENV=production +export PORT=3000 +export DB_FILE=/path/to/production/database.db +export DB_ENABLE_WAL=true +``` + +### Docker Deployment + +Build Docker image: + +```bash +cd api +docker build -t octocat-api . +``` + +Run container: + +```bash +docker run -p 3000:3000 \ + -e NODE_ENV=production \ + -e DB_FILE=/app/data/app.db \ + -v $(pwd)/data:/app/data \ + octocat-api +``` + +### Production Checklist + +- [ ] Set `NODE_ENV=production` +- [ ] Configure database backups +- [ ] Set up error logging (e.g., Sentry) +- [ ] Configure CORS for specific origins +- [ ] Add rate limiting +- [ ] Enable HTTPS +- [ ] Set up monitoring +- [ ] Configure log rotation +- [ ] Document deployment process + +--- + +## Troubleshooting + +### Common Issues + +#### Port Already in Use + +**Symptoms**: `Error: listen EADDRINUSE: address already in use :::3000` + +**Solutions**: +1. Stop the existing process using the port: + ```bash + # Find process ID + lsof -i :3000 + + # Kill process + kill -9 + ``` + +2. Use a different port: + ```bash + export PORT=3001 + npm run dev --workspace=api + ``` + +#### Database Locked + +**Symptoms**: `Error: SQLITE_BUSY: database is locked` + +**Causes**: +- Another process accessing the database +- Long-running transaction +- Unclosed database connection + +**Solutions**: +1. Ensure WAL mode is enabled (default) +2. Check for unclosed connections +3. Increase timeout setting: + ```bash + export DB_TIMEOUT=60000 + ``` + +#### Foreign Key Constraint Error + +**Symptoms**: `Error: FOREIGN KEY constraint failed` + +**Causes**: +- Trying to insert a record with invalid foreign key +- Deleting a parent record with dependent children + +**Solutions**: +1. Verify referenced entities exist before creating relationships +2. Use CASCADE delete when appropriate +3. Check the order of operations + +#### Module Not Found + +**Symptoms**: `Error: Cannot find module 'xyz'` + +**Solutions**: +1. Reinstall dependencies: + ```bash + rm -rf node_modules package-lock.json + npm install --workspace=api + ``` + +2. Verify import paths are correct + +#### Test Failures + +**Symptoms**: Tests fail unexpectedly + +**Solutions**: +1. Run tests in isolation: + ```bash + npm run test --workspace=api -- --run + ``` + +2. Check for test database issues: + - Ensure tests use in-memory database + - Verify migrations run before tests + +3. Review recent code changes + +#### Build Errors + +**Symptoms**: TypeScript compilation errors + +**Solutions**: +1. Check for type errors: + ```bash + npx tsc --noEmit + ``` + +2. Review error messages and fix type issues +3. Ensure all dependencies are installed + +### Getting Help + +- **GitHub Issues**: https://github.com/webmaxru/copilot_agent_mode/issues +- **Documentation**: Check other docs in `api/docs/` +- **Swagger UI**: http://localhost:3000/api-docs (when running) + +### Debug Mode + +Enable verbose logging: + +```bash +export DEBUG=* +npm run dev --workspace=api +``` + +--- + +## Best Practices + +### Code Organization + +1. Keep routes thin - delegate to repositories +2. Use TypeScript types everywhere +3. Handle errors consistently +4. Document complex logic with comments +5. Follow existing naming conventions + +### Database + +1. Always use parameterized queries +2. Run migrations before seeding +3. Test migrations on a copy first +4. Never modify existing migrations +5. Keep seed data minimal + +### Testing + +1. Write tests for new features +2. Mock external dependencies +3. Use in-memory database for speed +4. Aim for high coverage on critical paths +5. Run tests before committing + +### Git Workflow + +1. Create feature branches +2. Write descriptive commit messages +3. Keep commits focused and atomic +4. Test before pushing +5. Document breaking changes + +--- + +## Additional Resources + +- [API Overview](../README.md) - API architecture and quick start +- [Database Documentation](./database.md) - Schema and migrations +- [Models Documentation](./models.md) - Entity models +- [Endpoints Documentation](./endpoints.md) - API endpoints +- [Repository Pattern](./repository-pattern.md) - Data access layer +- [Architecture](../../docs/architecture.md) - System architecture +- [SQLite Integration](../../docs/sqlite-integration.md) - Database details diff --git a/api/docs/endpoints.md b/api/docs/endpoints.md new file mode 100644 index 0000000..046d357 --- /dev/null +++ b/api/docs/endpoints.md @@ -0,0 +1,950 @@ +# API Endpoints Documentation + +Complete reference for all REST API endpoints in the OctoCAT Supply Chain Management API. + +## Base URL + +- **Development**: `http://localhost:3000` +- **API Prefix**: `/api` + +All endpoints are prefixed with `/api`. Example: `http://localhost:3000/api/products` + +## Interactive Documentation + +For interactive testing and exploration: +- **Swagger UI**: http://localhost:3000/api-docs +- **OpenAPI JSON**: http://localhost:3000/api-docs.json + +## Common Response Patterns + +### Success Responses + +- **200 OK**: Resource retrieved successfully +- **201 Created**: Resource created successfully +- **204 No Content**: Resource deleted successfully + +### Error Responses + +- **400 Bad Request**: Invalid request data +- **404 Not Found**: Resource not found +- **409 Conflict**: Constraint violation (e.g., foreign key error) +- **500 Internal Server Error**: Server error + +Error responses return JSON: +```json +{ + "error": "Error message describing the problem" +} +``` + +## Authentication + +Currently, the API does not require authentication. This is suitable for demo and development purposes. + +--- + +## Suppliers + +Manage supplier information and contact details. + +### GET /api/suppliers + +Get all suppliers. + +**Response**: `200 OK` +```json +[ + { + "supplierId": 1, + "name": "TechSupply Co", + "description": "Leading technology supplier", + "contactPerson": "John Doe", + "email": "john@techsupply.com", + "phone": "555-0100" + } +] +``` + +### GET /api/suppliers/:id + +Get a supplier by ID. + +**Parameters**: +- `id` (path, integer) - Supplier ID + +**Response**: `200 OK` +```json +{ + "supplierId": 1, + "name": "TechSupply Co", + "description": "Leading technology supplier", + "contactPerson": "John Doe", + "email": "john@techsupply.com", + "phone": "555-0100" +} +``` + +**Errors**: +- `404 Not Found` - Supplier not found + +### POST /api/suppliers + +Create a new supplier. + +**Request Body**: +```json +{ + "name": "New Supplier Inc", + "description": "New supplier description", + "contactPerson": "Jane Smith", + "email": "jane@newsupplier.com", + "phone": "555-0200" +} +``` + +**Response**: `201 Created` +```json +{ + "supplierId": 2, + "name": "New Supplier Inc", + "description": "New supplier description", + "contactPerson": "Jane Smith", + "email": "jane@newsupplier.com", + "phone": "555-0200" +} +``` + +### PUT /api/suppliers/:id + +Update a supplier. + +**Parameters**: +- `id` (path, integer) - Supplier ID + +**Request Body** (all fields optional): +```json +{ + "name": "Updated Supplier Name", + "phone": "555-9999" +} +``` + +**Response**: `200 OK` +```json +{ + "supplierId": 1, + "name": "Updated Supplier Name", + "description": "Leading technology supplier", + "contactPerson": "John Doe", + "email": "john@techsupply.com", + "phone": "555-9999" +} +``` + +**Errors**: +- `404 Not Found` - Supplier not found + +### DELETE /api/suppliers/:id + +Delete a supplier. + +**Parameters**: +- `id` (path, integer) - Supplier ID + +**Response**: `204 No Content` + +**Errors**: +- `404 Not Found` - Supplier not found +- `409 Conflict` - Supplier has associated products or deliveries + +--- + +## Headquarters + +Manage company headquarters information. + +### GET /api/headquarters + +Get all headquarters. + +**Response**: `200 OK` +```json +[ + { + "headquartersId": 1, + "name": "Global HQ", + "description": "Main headquarters", + "address": "123 Main St, New York, NY", + "contactPerson": "Alice Johnson", + "email": "alice@company.com", + "phone": "555-0300" + } +] +``` + +### GET /api/headquarters/:id + +Get headquarters by ID. + +**Parameters**: +- `id` (path, integer) - Headquarters ID + +**Response**: `200 OK` + +**Errors**: +- `404 Not Found` - Headquarters not found + +### POST /api/headquarters + +Create new headquarters. + +**Request Body**: +```json +{ + "name": "Regional HQ East", + "description": "East coast regional headquarters", + "address": "456 Oak Ave, Boston, MA", + "contactPerson": "Bob Wilson", + "email": "bob@company.com", + "phone": "555-0400" +} +``` + +**Response**: `201 Created` + +### PUT /api/headquarters/:id + +Update headquarters. + +**Parameters**: +- `id` (path, integer) - Headquarters ID + +**Request Body** (all fields optional): +```json +{ + "address": "789 New Address St, Boston, MA" +} +``` + +**Response**: `200 OK` + +**Errors**: +- `404 Not Found` - Headquarters not found + +### DELETE /api/headquarters/:id + +Delete headquarters. + +**Parameters**: +- `id` (path, integer) - Headquarters ID + +**Response**: `204 No Content` + +**Errors**: +- `404 Not Found` - Headquarters not found +- `409 Conflict` - Headquarters has associated branches + +--- + +## Branches + +Manage branch locations linked to headquarters. + +### GET /api/branches + +Get all branches. + +**Response**: `200 OK` +```json +[ + { + "branchId": 1, + "headquartersId": 1, + "name": "Downtown Branch", + "description": "City center location", + "address": "100 Center St, New York, NY", + "contactPerson": "Carol Davis", + "email": "carol@branch.com", + "phone": "555-0500" + } +] +``` + +### GET /api/branches/:id + +Get branch by ID. + +**Parameters**: +- `id` (path, integer) - Branch ID + +**Response**: `200 OK` + +**Errors**: +- `404 Not Found` - Branch not found + +### POST /api/branches + +Create new branch. + +**Request Body**: +```json +{ + "headquartersId": 1, + "name": "Suburban Branch", + "description": "Suburban location", + "address": "200 Park Ave, Brooklyn, NY", + "contactPerson": "David Lee", + "email": "david@branch.com", + "phone": "555-0600" +} +``` + +**Response**: `201 Created` + +**Errors**: +- `400 Bad Request` - Invalid headquarters ID +- `404 Not Found` - Referenced headquarters not found + +### PUT /api/branches/:id + +Update branch. + +**Parameters**: +- `id` (path, integer) - Branch ID + +**Request Body** (all fields optional): +```json +{ + "contactPerson": "New Manager", + "phone": "555-9999" +} +``` + +**Response**: `200 OK` + +**Errors**: +- `404 Not Found` - Branch not found + +### DELETE /api/branches/:id + +Delete branch. + +**Parameters**: +- `id` (path, integer) - Branch ID + +**Response**: `204 No Content` + +**Errors**: +- `404 Not Found` - Branch not found +- `409 Conflict` - Branch has associated orders + +--- + +## Products + +Manage product catalog. + +### GET /api/products + +Get all products. + +**Response**: `200 OK` +```json +[ + { + "productId": 1, + "supplierId": 1, + "name": "OctoCat Plushie", + "description": "Adorable GitHub mascot plushie", + "price": 24.99, + "sku": "OCTOCAT-001", + "unit": "piece", + "imgName": "octocat-plushie.png", + "discount": 0.10 + } +] +``` + +### GET /api/products/:id + +Get product by ID. + +**Parameters**: +- `id` (path, integer) - Product ID + +**Response**: `200 OK` + +**Errors**: +- `404 Not Found` - Product not found + +### GET /api/products/name/:name + +Search products by name (partial match). + +**Parameters**: +- `name` (path, string) - Product name to search + +**Response**: `200 OK` +```json +[ + { + "productId": 1, + "name": "OctoCat Plushie", + ... + } +] +``` + +### POST /api/products + +Create new product. + +**Request Body**: +```json +{ + "supplierId": 1, + "name": "New Product", + "description": "Product description", + "price": 49.99, + "sku": "PROD-001", + "unit": "box", + "imgName": "product.png", + "discount": 0.0 +} +``` + +**Response**: `201 Created` + +**Errors**: +- `400 Bad Request` - Invalid supplier ID or missing required fields +- `404 Not Found` - Referenced supplier not found + +### PUT /api/products/:id + +Update product. + +**Parameters**: +- `id` (path, integer) - Product ID + +**Request Body** (all fields optional): +```json +{ + "price": 39.99, + "discount": 0.15 +} +``` + +**Response**: `200 OK` + +**Errors**: +- `404 Not Found` - Product not found + +### DELETE /api/products/:id + +Delete product. + +**Parameters**: +- `id` (path, integer) - Product ID + +**Response**: `204 No Content` + +**Errors**: +- `404 Not Found` - Product not found +- `409 Conflict` - Product is referenced in order details + +--- + +## Orders + +Manage customer orders. + +### GET /api/orders + +Get all orders. + +**Response**: `200 OK` +```json +[ + { + "orderId": 1, + "branchId": 1, + "orderDate": "2024-02-05T10:30:00.000Z", + "name": "Q1 Office Supplies", + "description": "Quarterly office supply order", + "status": "pending" + } +] +``` + +### GET /api/orders/:id + +Get order by ID. + +**Parameters**: +- `id` (path, integer) - Order ID + +**Response**: `200 OK` + +**Errors**: +- `404 Not Found` - Order not found + +### POST /api/orders + +Create new order. + +**Request Body**: +```json +{ + "branchId": 1, + "orderDate": "2024-02-05T10:30:00.000Z", + "name": "New Order", + "description": "Order description", + "status": "pending" +} +``` + +**Valid status values**: `pending`, `processing`, `completed`, `cancelled` + +**Response**: `201 Created` + +**Errors**: +- `400 Bad Request` - Invalid branch ID or status +- `404 Not Found` - Referenced branch not found + +### PUT /api/orders/:id + +Update order. + +**Parameters**: +- `id` (path, integer) - Order ID + +**Request Body** (all fields optional): +```json +{ + "status": "processing", + "description": "Updated description" +} +``` + +**Response**: `200 OK` + +**Errors**: +- `404 Not Found` - Order not found + +### DELETE /api/orders/:id + +Delete order. + +**Parameters**: +- `id` (path, integer) - Order ID + +**Response**: `204 No Content` + +**Errors**: +- `404 Not Found` - Order not found +- `409 Conflict` - Order has associated order details + +--- + +## Order Details + +Manage order line items. + +### GET /api/order-details + +Get all order details. + +**Response**: `200 OK` +```json +[ + { + "orderDetailId": 1, + "orderId": 1, + "productId": 1, + "quantity": 50, + "unitPrice": 24.99, + "notes": "Please expedite" + } +] +``` + +### GET /api/order-details/:id + +Get order detail by ID. + +**Parameters**: +- `id` (path, integer) - Order detail ID + +**Response**: `200 OK` + +**Errors**: +- `404 Not Found` - Order detail not found + +### POST /api/order-details + +Create new order detail. + +**Request Body**: +```json +{ + "orderId": 1, + "productId": 5, + "quantity": 100, + "unitPrice": 15.99, + "notes": "Standard delivery" +} +``` + +**Response**: `201 Created` + +**Errors**: +- `400 Bad Request` - Invalid order/product ID or quantity +- `404 Not Found` - Referenced order or product not found + +### PUT /api/order-details/:id + +Update order detail. + +**Parameters**: +- `id` (path, integer) - Order detail ID + +**Request Body** (all fields optional): +```json +{ + "quantity": 75, + "notes": "Updated notes" +} +``` + +**Response**: `200 OK` + +**Errors**: +- `404 Not Found` - Order detail not found + +### DELETE /api/order-details/:id + +Delete order detail. + +**Parameters**: +- `id` (path, integer) - Order detail ID + +**Response**: `204 No Content` + +**Errors**: +- `404 Not Found` - Order detail not found +- `409 Conflict` - Order detail has associated deliveries + +--- + +## Deliveries + +Manage deliveries from suppliers. + +### GET /api/deliveries + +Get all deliveries. + +**Response**: `200 OK` +```json +[ + { + "deliveryId": 1, + "supplierId": 1, + "deliveryDate": "2024-02-10T14:00:00.000Z", + "name": "Weekly Shipment #42", + "description": "Regular weekly supply", + "status": "in_transit" + } +] +``` + +### GET /api/deliveries/:id + +Get delivery by ID. + +**Parameters**: +- `id` (path, integer) - Delivery ID + +**Response**: `200 OK` + +**Errors**: +- `404 Not Found` - Delivery not found + +### POST /api/deliveries + +Create new delivery. + +**Request Body**: +```json +{ + "supplierId": 1, + "deliveryDate": "2024-02-15T09:00:00.000Z", + "name": "Express Delivery", + "description": "Urgent delivery", + "status": "pending" +} +``` + +**Valid status values**: `pending`, `in_transit`, `delivered`, `cancelled` + +**Response**: `201 Created` + +**Errors**: +- `400 Bad Request` - Invalid supplier ID or status +- `404 Not Found` - Referenced supplier not found + +### PUT /api/deliveries/:id + +Update delivery. + +**Parameters**: +- `id` (path, integer) - Delivery ID + +**Request Body** (all fields optional): +```json +{ + "status": "delivered" +} +``` + +**Response**: `200 OK` + +**Errors**: +- `404 Not Found` - Delivery not found + +### DELETE /api/deliveries/:id + +Delete delivery. + +**Parameters**: +- `id` (path, integer) - Delivery ID + +**Response**: `204 No Content` + +**Errors**: +- `404 Not Found` - Delivery not found +- `409 Conflict` - Delivery has associated order detail deliveries + +--- + +## Order Detail Deliveries + +Manage the fulfillment of order items by deliveries (junction table). + +### GET /api/order-detail-deliveries + +Get all order detail deliveries. + +**Response**: `200 OK` +```json +[ + { + "orderDetailDeliveryId": 1, + "orderDetailId": 1, + "deliveryId": 1, + "quantity": 50, + "notes": "Full shipment received" + } +] +``` + +### GET /api/order-detail-deliveries/:id + +Get order detail delivery by ID. + +**Parameters**: +- `id` (path, integer) - Order detail delivery ID + +**Response**: `200 OK` + +**Errors**: +- `404 Not Found` - Order detail delivery not found + +### POST /api/order-detail-deliveries + +Create new order detail delivery (link order detail to delivery). + +**Request Body**: +```json +{ + "orderDetailId": 1, + "deliveryId": 5, + "quantity": 25, + "notes": "Partial shipment" +} +``` + +**Response**: `201 Created` + +**Errors**: +- `400 Bad Request` - Invalid order detail/delivery ID or quantity +- `404 Not Found` - Referenced order detail or delivery not found + +### PUT /api/order-detail-deliveries/:id + +Update order detail delivery. + +**Parameters**: +- `id` (path, integer) - Order detail delivery ID + +**Request Body** (all fields optional): +```json +{ + "quantity": 50, + "notes": "Updated quantity" +} +``` + +**Response**: `200 OK` + +**Errors**: +- `404 Not Found` - Order detail delivery not found + +### DELETE /api/order-detail-deliveries/:id + +Delete order detail delivery. + +**Parameters**: +- `id` (path, integer) - Order detail delivery ID + +**Response**: `204 No Content` + +**Errors**: +- `404 Not Found` - Order detail delivery not found + +--- + +## Error Handling + +The API uses custom error classes that map to HTTP status codes: + +### NotFoundError (404) + +Returned when a requested resource doesn't exist. + +**Example Response**: +```json +{ + "error": "Product with ID 999 not found" +} +``` + +### ValidationError (400) + +Returned when request data is invalid or missing required fields. + +**Example Response**: +```json +{ + "error": "Validation failed: quantity must be a positive integer" +} +``` + +### ConflictError (409) + +Returned when an operation would violate database constraints (e.g., foreign key, unique constraint). + +**Example Response**: +```json +{ + "error": "Cannot delete supplier: associated products exist" +} +``` + +### DatabaseError (500) + +Returned for unexpected database errors. + +**Example Response**: +```json +{ + "error": "Internal server error" +} +``` + +Note: In production, error messages are sanitized to avoid exposing sensitive information. + +--- + +## CORS Configuration + +CORS is enabled for all origins in development. For production deployments, configure allowed origins in the Express CORS middleware. + +--- + +## Rate Limiting + +Currently, no rate limiting is implemented. For production deployments, consider adding rate limiting middleware. + +--- + +## Pagination + +Currently, endpoints return all results. For large datasets in production, implement pagination with query parameters: + +**Suggested pagination pattern** (not yet implemented): +``` +GET /api/products?page=1&limit=20 +``` + +--- + +## Testing Endpoints + +### Using cURL + +```bash +# Get all products +curl http://localhost:3000/api/products + +# Get product by ID +curl http://localhost:3000/api/products/1 + +# Create new product +curl -X POST http://localhost:3000/api/products \ + -H "Content-Type: application/json" \ + -d '{ + "supplierId": 1, + "name": "New Product", + "price": 29.99, + "sku": "PROD-001", + "unit": "box" + }' + +# Update product +curl -X PUT http://localhost:3000/api/products/1 \ + -H "Content-Type: application/json" \ + -d '{"price": 24.99}' + +# Delete product +curl -X DELETE http://localhost:3000/api/products/1 +``` + +### Using Swagger UI + +1. Start the API server: `npm run dev --workspace=api` +2. Open http://localhost:3000/api-docs +3. Expand an endpoint and click "Try it out" +4. Fill in parameters and click "Execute" +5. View the response + +--- + +## Best Practices + +1. **Use appropriate HTTP methods**: GET for reads, POST for creates, PUT for updates, DELETE for deletes +2. **Check for existence**: Verify referenced entities exist before creating relationships +3. **Handle errors**: Always check response status codes and handle errors appropriately +4. **Validate input**: Ensure required fields are provided and values are valid +5. **Use transactions**: For operations affecting multiple tables, consider implementing transactions +6. **Test with Swagger UI**: Use the interactive documentation to test endpoints before integrating + +--- + +## Additional Resources + +- [Models Documentation](./models.md) - Entity models and relationships +- [Repository Pattern](./repository-pattern.md) - Data access layer +- [Database Schema](./database.md) - Database structure +- [Development Guide](./development.md) - Setup and testing +- [Swagger UI](http://localhost:3000/api-docs) - Interactive API documentation diff --git a/api/docs/models.md b/api/docs/models.md new file mode 100644 index 0000000..bed1140 --- /dev/null +++ b/api/docs/models.md @@ -0,0 +1,578 @@ +# Models Documentation + +This document describes all TypeScript entity models used in the OctoCAT Supply Chain Management API. These models represent the domain entities and their relationships. + +## Overview + +Models are TypeScript interfaces that define the structure of data entities. They: +- Define type-safe data structures for the application +- Use **camelCase** naming convention (e.g., `productId`) +- Map to database tables that use **snake_case** (e.g., `product_id`) +- Include JSDoc comments with Swagger/OpenAPI annotations +- Are located in `api/src/models/` + +## Model Naming Conventions + +- **Interface Names**: Singular, PascalCase (e.g., `Product`, `Order`) +- **Field Names**: camelCase (e.g., `supplierId`, `orderDate`) +- **Primary Keys**: `{entityName}Id` (e.g., `productId`, `orderId`) +- **Foreign Keys**: `{referencedEntity}Id` (e.g., `supplierId`, `branchId`) + +## Models + +### Supplier + +Represents a supplier or vendor that provides products. + +**File**: `api/src/models/supplier.ts` + +```typescript +export interface Supplier { + supplierId: number; + name: string; + description: string; + contactPerson: string; + email: string; + phone: string; +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `supplierId` | number | Yes | Unique identifier | +| `name` | string | Yes | Supplier company name | +| `description` | string | No | Company description | +| `contactPerson` | string | No | Primary contact name | +| `email` | string | No | Contact email address | +| `phone` | string | No | Contact phone number | + +**Relationships:** +- One-to-many with `Product` (a supplier can provide multiple products) +- One-to-many with `Delivery` (a supplier can make multiple deliveries) + +**Database Table**: `suppliers` + +--- + +### Headquarters + +Represents company headquarters. + +**File**: `api/src/models/headquarters.ts` + +```typescript +export interface Headquarters { + headquartersId: number; + name: string; + description: string; + address: string; + contactPerson: string; + email: string; + phone: string; +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `headquartersId` | number | Yes | Unique identifier | +| `name` | string | Yes | Headquarters name | +| `description` | string | No | Description | +| `address` | string | No | Physical address | +| `contactPerson` | string | No | Primary contact name | +| `email` | string | No | Contact email address | +| `phone` | string | No | Contact phone number | + +**Relationships:** +- One-to-many with `Branch` (headquarters can have multiple branches) + +**Database Table**: `headquarters` + +--- + +### Branch + +Represents a branch location linked to headquarters. + +**File**: `api/src/models/branch.ts` + +```typescript +export interface Branch { + branchId: number; + headquartersId: number; + name: string; + description: string; + address: string; + contactPerson: string; + email: string; + phone: string; +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `branchId` | number | Yes | Unique identifier | +| `headquartersId` | number | Yes | Reference to headquarters | +| `name` | string | Yes | Branch name | +| `description` | string | No | Description | +| `address` | string | No | Physical address | +| `contactPerson` | string | No | Primary contact name | +| `email` | string | No | Contact email address | +| `phone` | string | No | Contact phone number | + +**Relationships:** +- Many-to-one with `Headquarters` (branch belongs to one headquarters) +- One-to-many with `Order` (branch can place multiple orders) + +**Database Table**: `branches` + +--- + +### Product + +Represents a product in the catalog. + +**File**: `api/src/models/product.ts` + +```typescript +export interface Product { + productId: number; + supplierId: number; + name: string; + description: string; + price: number; + sku: string; + unit: string; + imgName: string; + discount?: number; +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `productId` | number | Yes | Unique identifier | +| `supplierId` | number | Yes | Reference to supplier | +| `name` | string | Yes | Product name | +| `description` | string | No | Product description | +| `price` | number | Yes | Current price | +| `sku` | string | Yes | Stock Keeping Unit code | +| `unit` | string | Yes | Unit of measurement (e.g., "box", "pallet") | +| `imgName` | string | No | Product image filename | +| `discount` | number | No | Discount percentage (0.0-1.0, e.g., 0.25 = 25% off) | + +**Relationships:** +- Many-to-one with `Supplier` (product belongs to one supplier) +- One-to-many with `OrderDetail` (product can appear in multiple order details) + +**Database Table**: `products` + +--- + +### Order + +Represents a customer order placed at a branch. + +**File**: `api/src/models/order.ts` + +```typescript +export interface Order { + orderId: number; + branchId: number; + orderDate: string; + name: string; + description: string; + status: string; +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `orderId` | number | Yes | Unique identifier | +| `branchId` | number | Yes | Branch that placed the order | +| `orderDate` | string | Yes | Order date (ISO 8601 format) | +| `name` | string | Yes | Order name/reference | +| `description` | string | No | Order description | +| `status` | string | Yes | Order status: `pending`, `processing`, `completed`, `cancelled` | + +**Relationships:** +- Many-to-one with `Branch` (order belongs to one branch) +- One-to-many with `OrderDetail` (order contains multiple line items) + +**Database Table**: `orders` + +--- + +### OrderDetail + +Represents a line item in an order. + +**File**: `api/src/models/orderDetail.ts` + +```typescript +export interface OrderDetail { + orderDetailId: number; + orderId: number; + productId: number; + quantity: number; + unitPrice: number; + notes: string; +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `orderDetailId` | number | Yes | Unique identifier | +| `orderId` | number | Yes | Reference to order | +| `productId` | number | Yes | Reference to product | +| `quantity` | number | Yes | Quantity ordered | +| `unitPrice` | number | Yes | Price per unit at time of order | +| `notes` | string | No | Additional notes | + +**Relationships:** +- Many-to-one with `Order` (order detail belongs to one order) +- Many-to-one with `Product` (order detail references one product) +- One-to-many with `OrderDetailDelivery` (order detail can be fulfilled by multiple deliveries) + +**Database Table**: `order_details` + +--- + +### Delivery + +Represents a delivery from a supplier. + +**File**: `api/src/models/delivery.ts` + +```typescript +export interface Delivery { + deliveryId: number; + supplierId: number; + deliveryDate: string; + name: string; + description: string; + status: string; +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `deliveryId` | number | Yes | Unique identifier | +| `supplierId` | number | Yes | Supplier making the delivery | +| `deliveryDate` | string | Yes | Delivery date (ISO 8601 format) | +| `name` | string | Yes | Delivery name/reference | +| `description` | string | No | Delivery description | +| `status` | string | Yes | Delivery status: `pending`, `in_transit`, `delivered`, `cancelled` | + +**Relationships:** +- Many-to-one with `Supplier` (delivery comes from one supplier) +- One-to-many with `OrderDetailDelivery` (delivery can fulfill multiple order details) + +**Database Table**: `deliveries` + +--- + +### OrderDetailDelivery + +Junction table linking order details to deliveries. Represents the fulfillment of order items by specific deliveries. + +**File**: `api/src/models/orderDetailDelivery.ts` + +```typescript +export interface OrderDetailDelivery { + orderDetailDeliveryId: number; + orderDetailId: number; + deliveryId: number; + quantity: number; + notes: string; +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `orderDetailDeliveryId` | number | Yes | Unique identifier | +| `orderDetailId` | number | Yes | Reference to order detail | +| `deliveryId` | number | Yes | Reference to delivery | +| `quantity` | number | Yes | Quantity fulfilled by this delivery | +| `notes` | string | No | Additional notes | + +**Relationships:** +- Many-to-one with `OrderDetail` (links to one order detail) +- Many-to-one with `Delivery` (links to one delivery) + +**Database Table**: `order_detail_deliveries` + +--- + +## Entity Relationship Diagram + +```mermaid +erDiagram + Headquarters ||--o{ Branch: has + Branch ||--o{ Order: "placed at" + Order ||--o{ OrderDetail: contains + OrderDetail ||--o{ OrderDetailDelivery: "fulfilled by" + OrderDetail }|--|| Product: references + Delivery ||--o{ OrderDetailDelivery: includes + Supplier ||--o{ Delivery: provides + Supplier ||--o{ Product: supplies + + Headquarters { + int headquartersId PK + string name + string description + string address + string contactPerson + string email + string phone + } + + Branch { + int branchId PK + int headquartersId FK + string name + string description + string address + string contactPerson + string email + string phone + } + + Supplier { + int supplierId PK + string name + string description + string contactPerson + string email + string phone + } + + Product { + int productId PK + int supplierId FK + string name + string description + float price + string sku + string unit + string imgName + float discount + } + + Order { + int orderId PK + int branchId FK + string orderDate + string name + string description + string status + } + + OrderDetail { + int orderDetailId PK + int orderId FK + int productId FK + int quantity + float unitPrice + string notes + } + + Delivery { + int deliveryId PK + int supplierId FK + string deliveryDate + string name + string description + string status + } + + OrderDetailDelivery { + int orderDetailDeliveryId PK + int orderDetailId FK + int deliveryId FK + int quantity + string notes + } +``` + +## Model Usage Examples + +### Creating a New Product + +```typescript +import { Product } from '../models/product'; + +const newProduct: Omit = { + supplierId: 1, + name: 'OctoCat Plushie', + description: 'Adorable GitHub mascot plushie', + price: 24.99, + sku: 'OCTOCAT-001', + unit: 'piece', + imgName: 'octocat-plushie.png', + discount: 0.10 // 10% discount +}; +``` + +### Working with Order and OrderDetail + +```typescript +import { Order } from '../models/order'; +import { OrderDetail } from '../models/orderDetail'; + +// Create order +const order: Omit = { + branchId: 5, + orderDate: new Date().toISOString(), + name: 'Q1 2024 Office Supplies', + description: 'Quarterly office supply order', + status: 'pending' +}; + +// Add order detail +const orderDetail: Omit = { + orderId: 101, + productId: 15, + quantity: 50, + unitPrice: 24.99, + notes: 'Please expedite shipping' +}; +``` + +### Tracking Deliveries + +```typescript +import { Delivery } from '../models/delivery'; +import { OrderDetailDelivery } from '../models/orderDetailDelivery'; + +// Create delivery +const delivery: Omit = { + supplierId: 2, + deliveryDate: new Date().toISOString(), + name: 'Weekly Shipment #42', + description: 'Regular weekly supply delivery', + status: 'in_transit' +}; + +// Link delivery to order detail +const fulfillment: Omit = { + orderDetailId: 203, + deliveryId: 89, + quantity: 50, + notes: 'Full shipment received' +}; +``` + +## Type Safety + +All models are strictly typed using TypeScript interfaces, providing: +- **Compile-time validation**: Catch type errors before runtime +- **IDE support**: Autocomplete and inline documentation +- **Refactoring safety**: Type checking prevents breaking changes +- **Self-documenting code**: Types serve as inline documentation + +### Partial Updates + +For update operations, use TypeScript's `Partial` utility: + +```typescript +// Update only specific fields +const updateData: Partial = { + price: 19.99, + discount: 0.15 +}; +``` + +### Creating Records + +For create operations, omit the primary key using `Omit`: + +```typescript +// Omit productId since it's auto-generated +const newProduct: Omit = { + supplierId: 1, + name: 'New Product', + // ... other fields +}; +``` + +## Data Mapping + +### CamelCase to snake_case + +Models use camelCase for JavaScript/TypeScript conventions: +```typescript +{ productId: 1, supplierId: 5, unitPrice: 29.99 } +``` + +Database columns use snake_case: +```sql +SELECT product_id, supplier_id, unit_price FROM products +``` + +The `objectToCamelCase` utility (in `api/src/utils/sql.ts`) automatically handles conversion. + +### Date Handling + +- **Dates are stored as ISO 8601 strings** in the database +- Use `new Date().toISOString()` when creating date fields +- Parse dates with `new Date(dateString)` when needed + +Example: +```typescript +const order: Order = { + orderId: 1, + branchId: 5, + orderDate: '2024-02-05T10:30:00.000Z', // ISO 8601 format + name: 'Order #1', + description: '', + status: 'pending' +}; +``` + +## Swagger/OpenAPI Integration + +All models include Swagger annotations in JSDoc comments: + +```typescript +/** + * @swagger + * components: + * schemas: + * Product: + * type: object + * properties: + * productId: + * type: integer + * description: The unique identifier for the product + * name: + * type: string + * description: The name of the product + */ +export interface Product { + productId: number; + name: string; + // ... other fields +} +``` + +These annotations: +- Generate OpenAPI documentation automatically +- Appear in Swagger UI at `http://localhost:3000/api-docs` +- Provide request/response examples +- Define validation rules + +## Best Practices + +1. **Always use the model interfaces** - Don't use `any` or untyped objects +2. **Omit auto-generated fields** - Use `Omit` for create operations +3. **Use Partial for updates** - `Partial` for partial updates +4. **Validate foreign keys** - Ensure referenced entities exist before creating relationships +5. **Handle optional fields** - Use `?` for optional fields or provide defaults +6. **Document complex fields** - Add JSDoc comments for fields with special meaning +7. **Follow naming conventions** - camelCase for TypeScript, snake_case for SQL + +## Additional Resources + +- [Database Schema](./database.md) - Database table definitions +- [Repository Pattern](./repository-pattern.md) - Data access patterns +- [Endpoints Documentation](./endpoints.md) - API endpoints using these models +- [Swagger UI](http://localhost:3000/api-docs) - Interactive API documentation (when server is running) diff --git a/api/docs/repository-pattern.md b/api/docs/repository-pattern.md new file mode 100644 index 0000000..7a487d2 --- /dev/null +++ b/api/docs/repository-pattern.md @@ -0,0 +1,682 @@ +# Repository Pattern Documentation + +This document explains the repository pattern implementation in the OctoCAT Supply Chain Management API. + +## Overview + +The repository pattern provides a clean separation between the business logic and data access layers. It abstracts database operations behind well-defined interfaces, making the codebase more maintainable, testable, and flexible. + +### Key Benefits + +- **Separation of Concerns**: Business logic is isolated from database operations +- **Testability**: Repositories can be easily mocked for unit testing +- **Consistency**: Standardized data access patterns across all entities +- **Type Safety**: Full TypeScript support with compile-time validation +- **Maintainability**: Changes to data access logic are centralized +- **Flexibility**: Easy to swap data sources without affecting business logic + +## Architecture + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Routes β”‚ Express route handlers +β”‚ (API Layer)β”‚ - Validate requests +β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ - Call repositories + β”‚ - Return responses + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚Repositories β”‚ Data access layer +β”‚(Data Layer) β”‚ - CRUD operations +β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ - Custom queries + β”‚ - Error handling + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Database β”‚ SQLite database +β”‚ (SQLite) β”‚ - Persistent storage +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +## Repository Structure + +All repositories follow a consistent structure: + +```typescript +export class EntityRepository { + private db: DatabaseConnection; + + constructor(db: DatabaseConnection) { + this.db = db; + } + + async findAll(): Promise { /* ... */ } + async findById(id: number): Promise { /* ... */ } + async create(entity: Omit): Promise { /* ... */ } + async update(id: number, entity: Partial): Promise { /* ... */ } + async delete(id: number): Promise { /* ... */ } + + // Custom query methods + async customQuery(): Promise { /* ... */ } +} +``` + +### Factory Functions + +Each repository provides factory functions for creating instances: + +```typescript +// Create a new repository instance +export async function createEntityRepository( + isTest: boolean = false +): Promise { + const db = await getDatabase(isTest); + return new EntityRepository(db); +} + +// Get singleton repository instance (default usage) +export async function getEntityRepository( + isTest: boolean = false +): Promise { + if (!entityRepo) { + entityRepo = await createEntityRepository(isTest); + } + return entityRepo; +} +``` + +## Standard CRUD Operations + +### findAll() + +Retrieves all entities from the database. + +**Example: Products Repository** +```typescript +async findAll(): Promise { + try { + const rows = await this.db.all( + 'SELECT * FROM products ORDER BY product_id' + ); + return rows.map((row) => objectToCamelCase(row) as Product); + } catch (error) { + handleDatabaseError(error); + } +} +``` + +**Usage**: +```typescript +const repo = await getProductsRepository(); +const products = await repo.findAll(); +// Returns: Product[] +``` + +### findById(id) + +Retrieves a single entity by its primary key. + +**Example: Products Repository** +```typescript +async findById(id: number): Promise { + try { + const row = await this.db.get( + 'SELECT * FROM products WHERE product_id = ?', + [id] + ); + return row ? (objectToCamelCase(row) as Product) : null; + } catch (error) { + handleDatabaseError(error); + } +} +``` + +**Usage**: +```typescript +const repo = await getProductsRepository(); +const product = await repo.findById(1); +if (product) { + console.log(`Found: ${product.name}`); +} else { + console.log('Product not found'); +} +``` + +### create(entity) + +Creates a new entity in the database. + +**Example: Products Repository** +```typescript +async create(product: Omit): Promise { + try { + const { sql, values } = buildInsertSQL('products', product); + const result = await this.db.run(sql, values); + + const createdProduct = await this.findById(result.lastID!); + if (!createdProduct) { + throw new Error('Failed to retrieve created product'); + } + + return createdProduct; + } catch (error) { + handleDatabaseError(error); + } +} +``` + +**Usage**: +```typescript +const repo = await getProductsRepository(); +const newProduct = await repo.create({ + supplierId: 1, + name: 'OctoCat Plushie', + description: 'Adorable plushie', + price: 24.99, + sku: 'OCTOCAT-001', + unit: 'piece', + imgName: 'octocat.png' +}); +// Returns: Product (with generated productId) +``` + +### update(id, entity) + +Updates an existing entity. + +**Example: Products Repository** +```typescript +async update( + id: number, + product: Partial> +): Promise { + try { + const { sql, values } = buildUpdateSQL( + 'products', + product, + 'product_id = ?' + ); + const result = await this.db.run(sql, [...values, id]); + + if (result.changes === 0) { + throw new NotFoundError('Product', id); + } + + const updatedProduct = await this.findById(id); + if (!updatedProduct) { + throw new Error('Failed to retrieve updated product'); + } + + return updatedProduct; + } catch (error) { + handleDatabaseError(error, 'Product', id); + } +} +``` + +**Usage**: +```typescript +const repo = await getProductsRepository(); +const updated = await repo.update(1, { + price: 19.99, + discount: 0.15 +}); +// Returns: Product (with updated fields) +``` + +### delete(id) + +Deletes an entity from the database. + +**Example: Products Repository** +```typescript +async delete(id: number): Promise { + try { + const result = await this.db.run( + 'DELETE FROM products WHERE product_id = ?', + [id] + ); + + if (result.changes === 0) { + throw new NotFoundError('Product', id); + } + } catch (error) { + handleDatabaseError(error, 'Product', id); + } +} +``` + +**Usage**: +```typescript +const repo = await getProductsRepository(); +await repo.delete(1); +// Returns: void (throws NotFoundError if not found) +``` + +## Custom Query Methods + +Repositories can include custom query methods for specific use cases. + +### Example: Find Products by Supplier + +```typescript +async findBySupplierId(supplierId: number): Promise { + try { + const rows = await this.db.all( + 'SELECT * FROM products WHERE supplier_id = ? ORDER BY name', + [supplierId] + ); + return rows.map((row) => objectToCamelCase(row) as Product); + } catch (error) { + handleDatabaseError(error); + } +} +``` + +**Usage**: +```typescript +const repo = await getProductsRepository(); +const products = await repo.findBySupplierId(1); +// Returns: Product[] for supplier 1 +``` + +### Example: Search Products by Name + +```typescript +async findByName(name: string): Promise { + try { + const rows = await this.db.all( + `SELECT * FROM products WHERE name LIKE '%${name}%' ORDER BY name` + ); + return rows.map((row) => objectToCamelCase(row) as Product); + } catch (error) { + handleDatabaseError(error); + } +} +``` + +**Usage**: +```typescript +const repo = await getProductsRepository(); +const products = await repo.findByName('OctoCat'); +// Returns: Product[] matching 'OctoCat' +``` + +### Example: Check if Entity Exists + +```typescript +async exists(id: number): Promise { + try { + const result = await this.db.get<{ count: number }>( + 'SELECT COUNT(*) as count FROM products WHERE product_id = ?', + [id] + ); + return (result?.count || 0) > 0; + } catch (error) { + handleDatabaseError(error); + } +} +``` + +**Usage**: +```typescript +const repo = await getProductsRepository(); +const exists = await repo.exists(1); +if (exists) { + console.log('Product exists'); +} +``` + +## Data Mapping + +### camelCase ↔ snake_case Conversion + +The repository layer automatically converts between JavaScript/TypeScript naming conventions (camelCase) and SQL naming conventions (snake_case). + +**TypeScript Model (camelCase)**: +```typescript +{ + productId: 1, + supplierId: 5, + unitPrice: 29.99 +} +``` + +**Database Columns (snake_case)**: +```sql +product_id | supplier_id | unit_price + 1 | 5 | 29.99 +``` + +The `objectToCamelCase` utility (from `api/src/utils/sql.ts`) handles this conversion automatically when reading from the database. + +### SQL Helper Functions + +#### buildInsertSQL + +Generates parameterized INSERT statements. + +```typescript +const { sql, values } = buildInsertSQL('products', { + supplierId: 1, + name: 'Product', + price: 29.99 +}); +// sql: "INSERT INTO products (supplier_id, name, price) VALUES (?, ?, ?)" +// values: [1, 'Product', 29.99] +``` + +#### buildUpdateSQL + +Generates parameterized UPDATE statements. + +```typescript +const { sql, values } = buildUpdateSQL( + 'products', + { price: 24.99, discount: 0.10 }, + 'product_id = ?' +); +// sql: "UPDATE products SET price = ?, discount = ? WHERE product_id = ?" +// values: [24.99, 0.10] +``` + +## Error Handling + +Repositories use custom error classes from `api/src/utils/errors.ts`: + +### NotFoundError + +Thrown when an entity is not found. + +```typescript +if (result.changes === 0) { + throw new NotFoundError('Product', id); +} +``` + +**HTTP Status**: 404 + +### ValidationError + +Thrown for invalid data. + +```typescript +if (quantity <= 0) { + throw new ValidationError('Quantity must be positive'); +} +``` + +**HTTP Status**: 400 + +### ConflictError + +Thrown for constraint violations (e.g., foreign key errors). + +```typescript +try { + await this.db.run(sql, values); +} catch (error) { + if (error.code === 'SQLITE_CONSTRAINT') { + throw new ConflictError('Foreign key constraint violated'); + } +} +``` + +**HTTP Status**: 409 + +### DatabaseError + +Generic database error. + +```typescript +catch (error) { + throw new DatabaseError('Unexpected database error'); +} +``` + +**HTTP Status**: 500 + +### handleDatabaseError Utility + +The `handleDatabaseError` function automatically maps database errors to appropriate error types: + +```typescript +try { + // Database operation +} catch (error) { + handleDatabaseError(error, 'Product', id); +} +``` + +## Testing Repositories + +### Unit Tests with Mock Database + +Repositories can be tested using a mocked database connection: + +```typescript +import { vi } from 'vitest'; +import { ProductsRepository } from '../repositories/productsRepo'; + +describe('ProductsRepository', () => { + let mockDb: any; + let repo: ProductsRepository; + + beforeEach(() => { + mockDb = { + run: vi.fn(), + get: vi.fn(), + all: vi.fn(), + close: vi.fn() + }; + repo = new ProductsRepository(mockDb); + }); + + it('should find all products', async () => { + mockDb.all.mockResolvedValue([ + { product_id: 1, name: 'Product 1', price: 10.0 }, + { product_id: 2, name: 'Product 2', price: 20.0 } + ]); + + const products = await repo.findAll(); + + expect(products).toHaveLength(2); + expect(products[0].productId).toBe(1); + expect(mockDb.all).toHaveBeenCalledWith( + 'SELECT * FROM products ORDER BY product_id' + ); + }); +}); +``` + +### Integration Tests with In-Memory Database + +For integration tests, use an in-memory database: + +```typescript +import { getDatabase } from '../db/sqlite'; +import { createProductsRepository } from '../repositories/productsRepo'; + +describe('ProductsRepository Integration', () => { + let db: DatabaseConnection; + let repo: ProductsRepository; + + beforeAll(async () => { + db = await getDatabase(true); // true = test mode (in-memory) + repo = new ProductsRepository(db); + }); + + it('should create and retrieve a product', async () => { + const newProduct = await repo.create({ + supplierId: 1, + name: 'Test Product', + price: 29.99, + sku: 'TEST-001', + unit: 'piece', + description: '', + imgName: '' + }); + + expect(newProduct.productId).toBeDefined(); + + const retrieved = await repo.findById(newProduct.productId); + expect(retrieved?.name).toBe('Test Product'); + }); +}); +``` + +## Usage in Routes + +Routes use repositories to handle data access: + +```typescript +import express from 'express'; +import { getProductsRepository } from '../repositories/productsRepo'; + +const router = express.Router(); + +// GET /api/products +router.get('/', async (req, res, next) => { + try { + const repo = await getProductsRepository(); + const products = await repo.findAll(); + res.json(products); + } catch (error) { + next(error); // Error middleware handles custom errors + } +}); + +// POST /api/products +router.post('/', async (req, res, next) => { + try { + const repo = await getProductsRepository(); + const newProduct = await repo.create(req.body); + res.status(201).json(newProduct); + } catch (error) { + next(error); + } +}); + +// PUT /api/products/:id +router.put('/:id', async (req, res, next) => { + try { + const repo = await getProductsRepository(); + const updated = await repo.update(parseInt(req.params.id), req.body); + res.json(updated); + } catch (error) { + next(error); + } +}); + +export default router; +``` + +## Best Practices + +### 1. Use Parameterized Queries + +Always use parameterized queries to prevent SQL injection: + +βœ… **Good**: +```typescript +await this.db.get('SELECT * FROM products WHERE product_id = ?', [id]); +``` + +❌ **Bad**: +```typescript +await this.db.get(`SELECT * FROM products WHERE product_id = ${id}`); +``` + +### 2. Handle Errors Consistently + +Use `handleDatabaseError` for consistent error handling: + +```typescript +try { + // Database operation +} catch (error) { + handleDatabaseError(error, 'Product', id); +} +``` + +### 3. Return Consistent Types + +Ensure methods return consistent types: + +```typescript +// findById returns Entity | null (not undefined) +async findById(id: number): Promise { + const row = await this.db.get(...); + return row ? objectToCamelCase(row) as Product : null; +} +``` + +### 4. Use Omit and Partial + +Use TypeScript utility types for type safety: + +```typescript +// Create: omit auto-generated ID +async create(entity: Omit): Promise + +// Update: allow partial updates +async update(id: number, entity: Partial): Promise +``` + +### 5. Validate Foreign Keys + +Check that referenced entities exist before creating relationships: + +```typescript +// In routes or service layer +const supplier = await suppliersRepo.findById(product.supplierId); +if (!supplier) { + throw new NotFoundError('Supplier', product.supplierId); +} +``` + +### 6. Use Transactions for Multi-Step Operations + +For operations affecting multiple tables, use transactions (to be implemented): + +```typescript +// Future enhancement +await db.transaction(async (tx) => { + await ordersRepo.create(order, tx); + await orderDetailsRepo.create(detail, tx); +}); +``` + +### 7. Document Custom Methods + +Add JSDoc comments for custom query methods: + +```typescript +/** + * Find products by supplier ID + * @param supplierId - The supplier's unique identifier + * @returns Array of products from the specified supplier + */ +async findBySupplierId(supplierId: number): Promise { + // Implementation +} +``` + +## Available Repositories + +| Entity | Repository File | Factory Function | +|--------|----------------|------------------| +| Supplier | `suppliersRepo.ts` | `getSuppliersRepository()` | +| Headquarters | `headquartersRepo.ts` | `getHeadquartersRepository()` | +| Branch | `branchesRepo.ts` | `getBranchesRepository()` | +| Product | `productsRepo.ts` | `getProductsRepository()` | +| Order | `ordersRepo.ts` | `getOrdersRepository()` | +| OrderDetail | `orderDetailsRepo.ts` | `getOrderDetailsRepository()` | +| Delivery | `deliveriesRepo.ts` | `getDeliveriesRepository()` | +| OrderDetailDelivery | `orderDetailDeliveriesRepo.ts` | `getOrderDetailDeliveriesRepository()` | + +## Additional Resources + +- [Models Documentation](./models.md) - Entity model definitions +- [Database Schema](./database.md) - Database structure and relationships +- [Endpoints Documentation](./endpoints.md) - API endpoint reference +- [Development Guide](./development.md) - Testing and development practices +- [SQLite Integration](../../docs/sqlite-integration.md) - Database integration details