updated frontend
@@ -62,8 +62,15 @@ Cookie is HTTP-only and set with `sameSite=lax`.
|
||||
- `GET /api/health`
|
||||
- `GET /api/shop/categories`
|
||||
- `GET /api/shop/products`
|
||||
- `GET /api/shop/products/:productId`
|
||||
- `POST /api/shop/products` (`multipart/form-data` is supported, field name: `image`)
|
||||
- `POST /api/shop/products/:productId/image` (`multipart/form-data`, field name: `image`)
|
||||
- `POST /api/shop/orders`
|
||||
|
||||
Uploaded files are stored in `images/uploads` and are served at `/images/uploads/<filename>`.
|
||||
|
||||
Product responses include `image_url` with an absolute URL when an image exists.
|
||||
|
||||
Order payload:
|
||||
|
||||
```json
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
# Student Production Setup Guide
|
||||
|
||||
This guide explains how to set up and run the webstore backend in production mode.
|
||||
|
||||
## What You'll Receive
|
||||
|
||||
Your instructor will provide you with:
|
||||
|
||||
1. **webstore-production-images.tar** - Docker images archive (API + database)
|
||||
2. **production.compose.yml** - Docker Compose configuration
|
||||
3. **.production.env** - Environment variables template
|
||||
4. **run-student-production.bat** - Easy start script
|
||||
5. **Product images** - 17 placeholder product images
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Docker Desktop** installed ([Download here](https://www.docker.com/products/docker-desktop))
|
||||
- Windows 10 or later (for Docker)
|
||||
- Minimum 2GB free disk space
|
||||
- ~5 minutes for initial setup
|
||||
|
||||
## Step-by-Step Setup
|
||||
|
||||
### Step 1: Extract Docker Images
|
||||
|
||||
```
|
||||
Right-click on webstore-production-images.tar
|
||||
→ Select "Extract All"
|
||||
→ Choose destination folder
|
||||
```
|
||||
|
||||
Or use PowerShell/terminal:
|
||||
```powershell
|
||||
docker load -i webstore-production-images.tar
|
||||
```
|
||||
|
||||
### Step 2: Configure Environment Variables
|
||||
|
||||
1. Open `.production.env` in a text editor
|
||||
2. Update these values (if different from defaults):
|
||||
|
||||
```env
|
||||
# Database Configuration
|
||||
POSTGRES_DB=webstore_db
|
||||
POSTGRES_USER=webstore_user
|
||||
POSTGRES_PASSWORD=webstore_password
|
||||
DB_PORT=5432
|
||||
|
||||
# API Configuration
|
||||
PORT=3000
|
||||
API_PORT=3000
|
||||
|
||||
# JWT Configuration
|
||||
JWT_SECRET=your-secret-key-change-for-production
|
||||
JWT_EXPIRES_IN=7d
|
||||
|
||||
# Security
|
||||
COOKIE_NAME=webstore_token
|
||||
COOKIE_SECURE=false # Set to true if using HTTPS
|
||||
|
||||
# Database URL (usually don't change this)
|
||||
DATABASE_URL=postgresql://webstore_user:webstore_password@db:5432/webstore_db?schema=public
|
||||
```
|
||||
|
||||
### Step 3: Start the Application
|
||||
|
||||
**Option A: Using the provided script (recommended)**
|
||||
```
|
||||
Double-click: run-student-production.bat
|
||||
```
|
||||
|
||||
**Option B: Using Docker Compose directly**
|
||||
```powershell
|
||||
docker-compose -f production.compose.yml up
|
||||
```
|
||||
|
||||
### Step 4: Verify Setup
|
||||
|
||||
Wait for logs to show:
|
||||
```
|
||||
webstore_api_prod | npm notice
|
||||
webstore_api_prod | > webstore-backend@1.0.0 start
|
||||
webstore_api_prod | > node src/Api/server.js
|
||||
webstore_api_prod | Server running on port 3000
|
||||
```
|
||||
|
||||
### Step 5: Test the API
|
||||
|
||||
Open in your browser:
|
||||
```
|
||||
http://localhost:3000/api/health
|
||||
```
|
||||
|
||||
Expected response:
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"message": "CQRS backend running"
|
||||
}
|
||||
```
|
||||
|
||||
## Accessing the API
|
||||
|
||||
The API is available at: `http://localhost:3000`
|
||||
|
||||
### Key Endpoints
|
||||
|
||||
- `GET /api/health` - Health check
|
||||
- `GET /api/shop/categories` - List all product categories
|
||||
- `GET /api/shop/products` - List all products
|
||||
- `GET /api/shop/products/:id` - Get product by ID
|
||||
- `POST /api/shop/products` - Create product (with image upload)
|
||||
- `POST /api/shop/products/:id/image` - Update product image
|
||||
|
||||
### Built-in Data
|
||||
|
||||
The database automatically seeds with:
|
||||
|
||||
**Test Users:**
|
||||
```
|
||||
Email: admin@test.com
|
||||
Password: admin123
|
||||
|
||||
Email: john@test.com
|
||||
Password: password123
|
||||
|
||||
Email: jane@test.com
|
||||
Password: password123
|
||||
```
|
||||
|
||||
**Products:**
|
||||
- 16 sample products across 5 categories
|
||||
- Each product has a placeholder image
|
||||
- Sample prices range from 4,990 to 29,990 HUF
|
||||
|
||||
**Categories:**
|
||||
- Shoes (4 products)
|
||||
- Bags (4 products)
|
||||
- Accessories (4 products)
|
||||
- Clothing (3 products)
|
||||
- Electronics (2 products)
|
||||
|
||||
## Stopping the Application
|
||||
|
||||
### Using Docker Compose:
|
||||
```powershell
|
||||
docker-compose -f production.compose.yml down
|
||||
```
|
||||
|
||||
### Or simply close the terminal window
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Port Already in Use
|
||||
If you get "port 3000 already in use":
|
||||
```powershell
|
||||
# Change port in .production.env
|
||||
API_PORT=3001
|
||||
```
|
||||
|
||||
### Database Connection Failed
|
||||
- Verify database container is running: `docker ps`
|
||||
- Check DATABASE_URL in `.production.env`
|
||||
- Ensure PostgreSQL image is loaded: `docker images | grep postgres`
|
||||
|
||||
### Docker Not Running
|
||||
- Start Docker Desktop application
|
||||
- Wait for Docker daemon to be ready (check system tray)
|
||||
|
||||
### Out of Disk Space
|
||||
```powershell
|
||||
# Clean up Docker
|
||||
docker system prune -a
|
||||
```
|
||||
|
||||
## Image Upload
|
||||
|
||||
Once running, you can upload product images:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/shop/products/1/image \
|
||||
-F "image=@path/to/image.jpg"
|
||||
```
|
||||
|
||||
Images are stored in the `images/uploads/` directory and served automatically.
|
||||
|
||||
## Database Persistence
|
||||
|
||||
Product data is stored in PostgreSQL. Data persists between application restarts unless you:
|
||||
```powershell
|
||||
docker-compose -f production.compose.yml down -v
|
||||
```
|
||||
|
||||
The `-v` flag removes data volumes (use carefully!).
|
||||
|
||||
## Connecting Frontend
|
||||
|
||||
Configure your frontend to use:
|
||||
```
|
||||
API Base URL: http://localhost:3000/api
|
||||
```
|
||||
|
||||
Example (fetch):
|
||||
```javascript
|
||||
const response = await fetch('http://localhost:3000/api/shop/products');
|
||||
const data = await response.json();
|
||||
console.log(data);
|
||||
```
|
||||
|
||||
## Questions or Issues?
|
||||
|
||||
Consult your instructor or check the main README.md for more information about the backend architecture and CQRS pattern.
|
||||
|
||||
Happy coding! 🚀
|
||||
@@ -0,0 +1,104 @@
|
||||
# Product Images Directory
|
||||
|
||||
This directory contains product images for the webstore backend.
|
||||
|
||||
## Image Requirements
|
||||
|
||||
The following 17 placeholder images are required for the seed data to display properly:
|
||||
|
||||
**Shoes (4 images)**
|
||||
- `street-runner.jpg` - 18,990 HUF
|
||||
- `trail-edge.jpg` - 24,990 HUF
|
||||
- `urban-sprint.jpg` - 22,990 HUF
|
||||
- `classic-comfort.jpg` - 16,990 HUF
|
||||
|
||||
**Bags (4 images)**
|
||||
- `urban-tote.jpg` - 14,990 HUF
|
||||
- `backpack-pro.jpg` - 19,990 HUF
|
||||
- `crossbody.jpg` - 9,990 HUF
|
||||
- `duffle.jpg` - 24,990 HUF
|
||||
|
||||
**Accessories (4 images)**
|
||||
- `classic-cap.jpg` - 6,990 HUF
|
||||
- `silk-scarf.jpg` - 12,990 HUF
|
||||
- `leather-belt.jpg` - 8,990 HUF
|
||||
- `watch-straps.jpg` - 4,990 HUF
|
||||
|
||||
**Clothing (3 images)**
|
||||
- `cotton-tshirt.jpg` - 7,990 HUF
|
||||
- `denim-jacket.jpg` - 29,990 HUF
|
||||
- `yoga-leggings.jpg` - 11,990 HUF
|
||||
|
||||
**Electronics (2 images)**
|
||||
- `earbuds.jpg` - 17,990 HUF
|
||||
- `usb-hub.jpg` - 5,990 HUF
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
images/
|
||||
├── [17 product placeholder images]
|
||||
├── uploads/ (auto-created by multer)
|
||||
│ └── [user-uploaded images]
|
||||
├── production.compose.yml (production docker-compose)
|
||||
├── .production.env (production environment variables)
|
||||
└── run-student-production.bat (production start script)
|
||||
```
|
||||
|
||||
## Image Specifications
|
||||
|
||||
**Recommended specifications for product images:**
|
||||
- Format: JPEG or PNG
|
||||
- Dimensions: 400-600px width, 300-600px height
|
||||
- File size: < 500KB per image
|
||||
- Color space: RGB or RGBA
|
||||
|
||||
## Uploads Directory
|
||||
|
||||
The `uploads/` directory is automatically created by the multer middleware when the backend starts. This directory stores images uploaded by users through the frontend.
|
||||
|
||||
**Important notes:**
|
||||
- Images are stored with timestamps: `1717123456789-product-name.jpg`
|
||||
- Served via: `GET /images/uploads/{filename}`
|
||||
- Max file size: 5MB per upload
|
||||
- Only image MIME types allowed
|
||||
|
||||
## Production Build
|
||||
|
||||
When building for production with `build-production-images.bat`:
|
||||
1. ✓ Placeholder images are automatically generated (if ImageMagick or PowerShell available)
|
||||
2. ✓ Seeding runs automatically when containers start
|
||||
3. ✓ Upload directory is created on first run
|
||||
|
||||
## Database Seeding
|
||||
|
||||
The database automatically seeds with:
|
||||
- **5 categories**: Shoes, Bags, Accessories, Clothing, Electronics
|
||||
- **16 products**: 2-4 per category with placeholder images
|
||||
- **3 test users**: admin@test.com, john@test.com, jane@test.com
|
||||
|
||||
## Customization
|
||||
|
||||
To use custom product images:
|
||||
|
||||
1. **Development**: Replace placeholder images in this directory
|
||||
2. **Production**: Include custom images in the `images/` directory before building
|
||||
3. **After deployment**: Use the API to upload images via `POST /api/shop/products/{id}/image`
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Images not displaying in frontend
|
||||
- Ensure images are placed in the correct directory
|
||||
- Check file names match exactly (case-sensitive on Linux/Mac)
|
||||
- Verify `express.static` is configured in `src/Api/app.js`
|
||||
- Check console for 404 errors
|
||||
|
||||
### Upload fails
|
||||
- Check file is an image (JPEG, PNG, WebP, GIF, etc.)
|
||||
- Verify file size < 5MB
|
||||
- Ensure `uploads/` directory exists and is writable
|
||||
|
||||
### Seeding without images
|
||||
- Seed will complete successfully even without images
|
||||
- Products will have `null` imageUrl values
|
||||
- Frontend can display placeholder or error images
|
||||
|
After Width: | Height: | Size: 332 B |
|
After Width: | Height: | Size: 332 B |
|
After Width: | Height: | Size: 332 B |
|
After Width: | Height: | Size: 332 B |
|
After Width: | Height: | Size: 332 B |
|
After Width: | Height: | Size: 332 B |
|
After Width: | Height: | Size: 332 B |
|
After Width: | Height: | Size: 332 B |
|
After Width: | Height: | Size: 332 B |
@@ -4,7 +4,7 @@ setlocal EnableExtensions EnableDelayedExpansion
|
||||
set "ROOT_DIR=%~dp0"
|
||||
set "ENV_FILE=%ROOT_DIR%\.production.env"
|
||||
set "COMPOSE_FILE=%ROOT_DIR%\production.compose.yml"
|
||||
set "ARCHIVE_FILE=%ROOT_DIR%\images\webstore-production-images.tar"
|
||||
set "ARCHIVE_FILE=%ROOT_DIR%\webstore-production-images.tar"
|
||||
|
||||
set "API_IMAGE=webstore-api:prod"
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 332 B |
|
After Width: | Height: | Size: 332 B |
|
After Width: | Height: | Size: 332 B |
|
After Width: | Height: | Size: 332 B |
|
After Width: | Height: | Size: 332 B |
|
After Width: | Height: | Size: 332 B |
|
After Width: | Height: | Size: 332 B |
|
After Width: | Height: | Size: 332 B |
@@ -18,7 +18,8 @@
|
||||
"express": "^4.19.2",
|
||||
"helmet": "^7.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"morgan": "^1.10.0"
|
||||
"morgan": "^1.10.0",
|
||||
"multer": "^2.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.1.4",
|
||||
@@ -120,6 +121,12 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/append-field": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
|
||||
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/array-flatten": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||
@@ -229,6 +236,23 @@
|
||||
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/buffer-from": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/busboy": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
|
||||
"dependencies": {
|
||||
"streamsearch": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
@@ -292,6 +316,21 @@
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/concat-stream": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
|
||||
"integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
|
||||
"engines": [
|
||||
"node >= 6.0"
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-from": "^1.0.0",
|
||||
"inherits": "^2.0.3",
|
||||
"readable-stream": "^3.0.2",
|
||||
"typedarray": "^0.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/content-disposition": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||
@@ -1020,6 +1059,25 @@
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/multer": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/multer/-/multer-2.1.1.tgz",
|
||||
"integrity": "sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"append-field": "^1.0.0",
|
||||
"busboy": "^1.6.0",
|
||||
"concat-stream": "^2.0.0",
|
||||
"type-is": "^1.6.18"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.16.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/negotiator": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||
@@ -1242,6 +1300,20 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
@@ -1438,6 +1510,23 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/streamsearch": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
|
||||
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
||||
@@ -1496,6 +1585,12 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/typedarray": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
|
||||
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/undefsafe": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
|
||||
@@ -1512,6 +1607,12 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/utils-merge": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
"prisma:generate": "prisma generate",
|
||||
"prisma:push": "prisma db push",
|
||||
"prisma:seed": "node prisma/seed.js",
|
||||
"generate-data": "node scripts/generate-test-data.js",
|
||||
"generate-images": "node scripts/generate-placeholder-images.js",
|
||||
"postinstall": "prisma generate"
|
||||
},
|
||||
"keywords": [
|
||||
@@ -20,6 +22,7 @@
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@prisma/client": "^5.20.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"cors": "^2.8.5",
|
||||
@@ -28,10 +31,10 @@
|
||||
"helmet": "^7.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"morgan": "^1.10.0",
|
||||
"@prisma/client": "^5.20.0"
|
||||
"multer": "^2.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.1.4",
|
||||
"prisma": "^5.20.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
const { PrismaClient } = require("@prisma/client");
|
||||
const bcryptjs = require("bcryptjs");
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
console.log("Starting database seeding...");
|
||||
|
||||
// Seed categories
|
||||
const categories = [
|
||||
{ name: "Shoes", slug: "shoes" },
|
||||
{ name: "Bags", slug: "bags" },
|
||||
{ name: "Accessories", slug: "accessories" }
|
||||
{ name: "Accessories", slug: "accessories" },
|
||||
{ name: "Clothing", slug: "clothing" },
|
||||
{ name: "Electronics", slug: "electronics" }
|
||||
];
|
||||
|
||||
console.log("Creating categories...");
|
||||
for (const category of categories) {
|
||||
await prisma.category.upsert({
|
||||
where: { slug: category.slug },
|
||||
@@ -20,12 +27,15 @@ async function main() {
|
||||
const shoes = await prisma.category.findUnique({ where: { slug: "shoes" } });
|
||||
const bags = await prisma.category.findUnique({ where: { slug: "bags" } });
|
||||
const accessories = await prisma.category.findUnique({ where: { slug: "accessories" } });
|
||||
const clothing = await prisma.category.findUnique({ where: { slug: "clothing" } });
|
||||
const electronics = await prisma.category.findUnique({ where: { slug: "electronics" } });
|
||||
|
||||
const products = [
|
||||
// Shoes
|
||||
{
|
||||
categoryId: shoes.id,
|
||||
name: "Street Runner",
|
||||
description: "Lightweight city sneaker.",
|
||||
description: "Lightweight city sneaker perfect for daily commute.",
|
||||
price: "18990.00",
|
||||
imageUrl: "/images/street-runner.jpg",
|
||||
stock: 24
|
||||
@@ -33,29 +43,138 @@ async function main() {
|
||||
{
|
||||
categoryId: shoes.id,
|
||||
name: "Trail Edge",
|
||||
description: "Stable shoe for outdoor tracks.",
|
||||
description: "Stable shoe for outdoor tracks and hiking adventures.",
|
||||
price: "24990.00",
|
||||
imageUrl: "/images/trail-edge.jpg",
|
||||
stock: 13
|
||||
},
|
||||
{
|
||||
categoryId: shoes.id,
|
||||
name: "Urban Sprint",
|
||||
description: "High-performance running shoe for athletes.",
|
||||
price: "22990.00",
|
||||
imageUrl: "/images/urban-sprint.jpg",
|
||||
stock: 18
|
||||
},
|
||||
{
|
||||
categoryId: shoes.id,
|
||||
name: "Classic Comfort",
|
||||
description: "Timeless design with ultimate comfort for all-day wear.",
|
||||
price: "16990.00",
|
||||
imageUrl: "/images/classic-comfort.jpg",
|
||||
stock: 31
|
||||
},
|
||||
// Bags
|
||||
{
|
||||
categoryId: bags.id,
|
||||
name: "Urban Tote",
|
||||
description: "Everyday tote with zipper top.",
|
||||
description: "Everyday tote with zipper top and multiple compartments.",
|
||||
price: "14990.00",
|
||||
imageUrl: "/images/urban-tote.jpg",
|
||||
stock: 30
|
||||
},
|
||||
{
|
||||
categoryId: bags.id,
|
||||
name: "Backpack Pro",
|
||||
description: "Professional backpack with laptop compartment.",
|
||||
price: "19990.00",
|
||||
imageUrl: "/images/backpack-pro.jpg",
|
||||
stock: 15
|
||||
},
|
||||
{
|
||||
categoryId: bags.id,
|
||||
name: "Crossbody Essentials",
|
||||
description: "Compact crossbody bag perfect for quick trips.",
|
||||
price: "9990.00",
|
||||
imageUrl: "/images/crossbody.jpg",
|
||||
stock: 45
|
||||
},
|
||||
{
|
||||
categoryId: bags.id,
|
||||
name: "Weekend Duffle",
|
||||
description: "Spacious duffle bag for travel and weekend getaways.",
|
||||
price: "24990.00",
|
||||
imageUrl: "/images/duffle.jpg",
|
||||
stock: 8
|
||||
},
|
||||
// Accessories
|
||||
{
|
||||
categoryId: accessories.id,
|
||||
name: "Classic Cap",
|
||||
description: "Adjustable cotton cap.",
|
||||
description: "Adjustable cotton cap with curved visor.",
|
||||
price: "6990.00",
|
||||
imageUrl: "/images/classic-cap.jpg",
|
||||
stock: 42
|
||||
},
|
||||
{
|
||||
categoryId: accessories.id,
|
||||
name: "Silk Scarf",
|
||||
description: "Premium silk scarf with elegant patterns.",
|
||||
price: "12990.00",
|
||||
imageUrl: "/images/silk-scarf.jpg",
|
||||
stock: 20
|
||||
},
|
||||
{
|
||||
categoryId: accessories.id,
|
||||
name: "Leather Belt",
|
||||
description: "Genuine leather belt with metal buckle.",
|
||||
price: "8990.00",
|
||||
imageUrl: "/images/leather-belt.jpg",
|
||||
stock: 35
|
||||
},
|
||||
{
|
||||
categoryId: accessories.id,
|
||||
name: "Watch Straps Set",
|
||||
description: "Set of 3 interchangeable watch straps.",
|
||||
price: "4990.00",
|
||||
imageUrl: "/images/watch-straps.jpg",
|
||||
stock: 60
|
||||
},
|
||||
// Clothing
|
||||
{
|
||||
categoryId: clothing.id,
|
||||
name: "Cotton T-Shirt",
|
||||
description: "100% organic cotton comfortable t-shirt.",
|
||||
price: "7990.00",
|
||||
imageUrl: "/images/cotton-tshirt.jpg",
|
||||
stock: 50
|
||||
},
|
||||
{
|
||||
categoryId: clothing.id,
|
||||
name: "Denim Jacket",
|
||||
description: "Classic blue denim jacket perfect for any season.",
|
||||
price: "29990.00",
|
||||
imageUrl: "/images/denim-jacket.jpg",
|
||||
stock: 12
|
||||
},
|
||||
{
|
||||
categoryId: clothing.id,
|
||||
name: "Yoga Leggings",
|
||||
description: "High-waisted leggings with optimal breathability.",
|
||||
price: "11990.00",
|
||||
imageUrl: "/images/yoga-leggings.jpg",
|
||||
stock: 25
|
||||
},
|
||||
// Electronics
|
||||
{
|
||||
categoryId: electronics.id,
|
||||
name: "Wireless Earbuds",
|
||||
description: "Premium noise-cancelling wireless earbuds.",
|
||||
price: "17990.00",
|
||||
imageUrl: "/images/earbuds.jpg",
|
||||
stock: 22
|
||||
},
|
||||
{
|
||||
categoryId: electronics.id,
|
||||
name: "USB-C Hub",
|
||||
description: "7-in-1 USB-C hub with multiple ports.",
|
||||
price: "5990.00",
|
||||
imageUrl: "/images/usb-hub.jpg",
|
||||
stock: 40
|
||||
}
|
||||
];
|
||||
|
||||
console.log("Creating products...");
|
||||
for (const product of products) {
|
||||
await prisma.product.upsert({
|
||||
where: { name: product.name },
|
||||
@@ -63,15 +182,37 @@ async function main() {
|
||||
create: product
|
||||
});
|
||||
}
|
||||
|
||||
// Seed test users
|
||||
console.log("Creating test users...");
|
||||
const testUsers = [
|
||||
{ name: "Admin User", email: "admin@test.com", password: "admin123" },
|
||||
{ name: "John Doe", email: "john@test.com", password: "password123" },
|
||||
{ name: "Jane Smith", email: "jane@test.com", password: "password123" }
|
||||
];
|
||||
|
||||
for (const user of testUsers) {
|
||||
const passwordHash = await bcryptjs.hash(user.password, 10);
|
||||
await prisma.user.upsert({
|
||||
where: { email: user.email },
|
||||
update: {},
|
||||
create: {
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
passwordHash
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log("Database seeding completed successfully!");
|
||||
}
|
||||
|
||||
main()
|
||||
.then(async () => {
|
||||
await prisma.$disconnect();
|
||||
console.log("Prisma seed completed.");
|
||||
})
|
||||
.catch(async (error) => {
|
||||
console.error(error);
|
||||
console.error("Seeding error:", error);
|
||||
await prisma.$disconnect();
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,179 @@
|
||||
# Test Data Generation Scripts
|
||||
|
||||
This directory contains scripts to help manage test data for the webstore backend.
|
||||
|
||||
## Available Scripts
|
||||
|
||||
### 0. Generate Placeholder Images
|
||||
Creates placeholder JPEG images for all 17 products used in seeding.
|
||||
|
||||
**File:** `generate-placeholder-images.js`
|
||||
|
||||
**What it does:**
|
||||
- Generates 17 placeholder product images (400x300px)
|
||||
- Saves images to `../images/` directory
|
||||
- Creates `uploads/` directory for user-uploaded images
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
# Using npm script
|
||||
npm run generate-images
|
||||
|
||||
# Or directly with Node
|
||||
node scripts/generate-placeholder-images.js
|
||||
```
|
||||
|
||||
**When to run:**
|
||||
- First time setting up development environment
|
||||
- When `/images` route returns 404 (images not found)
|
||||
- After cleaning up the images directory
|
||||
|
||||
**Images created (17 total):**
|
||||
- Shoes: street-runner.jpg, trail-edge.jpg, urban-sprint.jpg, classic-comfort.jpg
|
||||
- Bags: urban-tote.jpg, backpack-pro.jpg, crossbody.jpg, duffle.jpg
|
||||
- Accessories: classic-cap.jpg, silk-scarf.jpg, leather-belt.jpg, watch-straps.jpg
|
||||
- Clothing: cotton-tshirt.jpg, denim-jacket.jpg, yoga-leggings.jpg
|
||||
- Electronics: earbuds.jpg, usb-hub.jpg
|
||||
|
||||
---
|
||||
|
||||
### 1. Seed Script (Automatic)
|
||||
Runs automatically when the backend starts in development mode.
|
||||
|
||||
**File:** `../prisma/seed.js`
|
||||
|
||||
**What it does:**
|
||||
- Creates 5 product categories (Shoes, Bags, Accessories, Clothing, Electronics)
|
||||
- Creates 16 sample products across all categories
|
||||
- Creates 3 test user accounts
|
||||
|
||||
**Test User Credentials:**
|
||||
```
|
||||
Email: admin@test.com
|
||||
Password: admin123
|
||||
|
||||
Email: john@test.com
|
||||
Password: password123
|
||||
|
||||
Email: jane@test.com
|
||||
Password: password123
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Generate Test Data Script
|
||||
Generates additional bulk test data on demand.
|
||||
|
||||
### Usage Options
|
||||
|
||||
#### Option A: Windows Batch File (Recommended for Windows)
|
||||
```bash
|
||||
# Generate default test data (10 products, 5 orders)
|
||||
scripts\generate-test-data.bat
|
||||
|
||||
# Generate 20 products instead of 10
|
||||
scripts\generate-test-data.bat --products 20
|
||||
|
||||
# Generate 20 products and 10 orders
|
||||
scripts\generate-test-data.bat --products 20 --orders 10
|
||||
|
||||
# DANGEROUS: Reset database and regenerate all data
|
||||
scripts\generate-test-data.bat --reset --products 50 --orders 20
|
||||
```
|
||||
|
||||
#### Option B: NPM Script
|
||||
```bash
|
||||
# Generate default test data
|
||||
npm run generate-data
|
||||
|
||||
# With arguments (from project root)
|
||||
npm run generate-data -- --products 30 --orders 15
|
||||
```
|
||||
|
||||
#### Option C: Direct Node.js
|
||||
```bash
|
||||
# From project root
|
||||
node scripts/generate-test-data.js --products 25 --orders 10
|
||||
```
|
||||
|
||||
### Command-line Options
|
||||
|
||||
| Option | Description | Default |
|
||||
|--------|-------------|---------|
|
||||
| `--products N` | Number of additional products to generate | 10 |
|
||||
| `--orders N` | Number of test orders to generate | 5 |
|
||||
| `--reset` | ⚠️ **DANGEROUS**: Delete all data first | (not set) |
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Quick test: Generate 5 products and 2 orders
|
||||
scripts\generate-test-data.bat --products 5 --orders 2
|
||||
|
||||
# Moderate data: 30 products and 10 orders
|
||||
npm run generate-data -- --products 30 --orders 10
|
||||
|
||||
# Heavy load test: 100 products and 50 orders
|
||||
scripts\generate-test-data.bat --products 100 --orders 50
|
||||
|
||||
# Start fresh: Reset and generate 50 products and 20 orders
|
||||
scripts\generate-test-data.bat --reset --products 50 --orders 20
|
||||
```
|
||||
|
||||
## What Gets Generated
|
||||
|
||||
### Products
|
||||
- Random product names with auto-incrementing counters
|
||||
- Random prices between 5,990 and 24,990 HUF
|
||||
- Random stock quantities (5-55 units)
|
||||
- Random category assignment
|
||||
- Placeholder image URLs
|
||||
|
||||
### Test Users
|
||||
- 5 additional test user accounts
|
||||
- Email: `user[1-5]@test.com`
|
||||
- Password: `password123`
|
||||
|
||||
### Orders
|
||||
- Random customer names
|
||||
- Random customer emails
|
||||
- Random order items (1-3 products per order)
|
||||
- Automatically calculated total prices
|
||||
|
||||
## Important Notes
|
||||
|
||||
⚠️ **WARNING**: The `--reset` flag will delete:
|
||||
- All orders and order items
|
||||
- All products
|
||||
- All categories
|
||||
- All users
|
||||
|
||||
Only use `--reset` if you know what you're doing!
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Node.js is not installed or not in PATH"
|
||||
- Install Node.js from https://nodejs.org/
|
||||
- Make sure it's in your system PATH
|
||||
|
||||
### "Database connection error"
|
||||
- Ensure the database is running
|
||||
- Check `.env` file has correct `DATABASE_URL`
|
||||
- Run migrations first: `npm run prisma:push`
|
||||
|
||||
### "Unique constraint failed" errors
|
||||
- Product names must be unique
|
||||
- The script skips duplicates automatically
|
||||
- Use `--reset` to start fresh
|
||||
|
||||
## Docker Compose Integration
|
||||
|
||||
The test data generation automatically runs in docker-compose:
|
||||
```bash
|
||||
# Start backend (will seed data automatically)
|
||||
docker-compose up api
|
||||
|
||||
# The initial data is seeded when the container first starts
|
||||
```
|
||||
|
||||
For production, you may want to disable auto-seeding in the `docker-compose.yml` file.
|
||||
@@ -8,14 +8,14 @@ set "ARCHIVE_FILE=%IMAGE_OUT_DIR%\webstore-production-images.tar"
|
||||
set "API_IMAGE=webstore-api:prod"
|
||||
set "DB_IMAGE=postgres:16-alpine"
|
||||
|
||||
echo [1/6] Checking Docker availability...
|
||||
echo [1/7] Checking Docker availability...
|
||||
docker --version >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo ERROR: Docker CLI is not available. Install Docker Desktop first.
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo [2/6] Building production API image: %API_IMAGE%
|
||||
echo [2/7] Building production API image: %API_IMAGE%
|
||||
pushd "%ROOT_DIR%" >nul
|
||||
docker build -t "%API_IMAGE%" -f Dockerfile .
|
||||
if errorlevel 1 (
|
||||
@@ -25,17 +25,33 @@ if errorlevel 1 (
|
||||
)
|
||||
popd >nul
|
||||
|
||||
echo [3/6] Pulling database image: %DB_IMAGE%
|
||||
echo [3/7] Pulling database image: %DB_IMAGE%
|
||||
docker pull "%DB_IMAGE%"
|
||||
if errorlevel 1 (
|
||||
echo ERROR: Failed to pull database image.
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo [4/6] Preparing image output directory: %IMAGE_OUT_DIR%
|
||||
echo [4/7] Preparing image output directory: %IMAGE_OUT_DIR%
|
||||
if not exist "%IMAGE_OUT_DIR%" mkdir "%IMAGE_OUT_DIR%"
|
||||
|
||||
echo [5/6] Exporting images to archive...
|
||||
echo [5/7] Creating placeholder product images...
|
||||
pushd "%ROOT_DIR%" >nul
|
||||
|
||||
REM Create uploads directory
|
||||
if not exist "%IMAGE_OUT_DIR%\uploads" mkdir "%IMAGE_OUT_DIR%\uploads"
|
||||
|
||||
REM Use Node.js to generate placeholder images reliably
|
||||
echo Generating placeholder images using Node.js...
|
||||
node "%ROOT_DIR%\scripts\generate-placeholder-images.js"
|
||||
if errorlevel 1 (
|
||||
echo WARNING: Could not generate placeholder images automatically
|
||||
echo Note: Images can be added manually to %IMAGE_OUT_DIR%
|
||||
)
|
||||
|
||||
popd >nul
|
||||
|
||||
echo [6/7] Exporting images to archive...
|
||||
if exist "%ARCHIVE_FILE%" del /f /q "%ARCHIVE_FILE%"
|
||||
docker save -o "%ARCHIVE_FILE%" "%API_IMAGE%" "%DB_IMAGE%"
|
||||
if errorlevel 1 (
|
||||
@@ -43,7 +59,35 @@ if errorlevel 1 (
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo [6/6] Done.
|
||||
echo [7/7] Done.
|
||||
echo.
|
||||
echo ========================================
|
||||
echo PRODUCTION BUILD COMPLETE
|
||||
echo ========================================
|
||||
echo.
|
||||
echo Created archive: "%ARCHIVE_FILE%"
|
||||
echo Share this file with students together with production.compose.yml and .production.env.
|
||||
echo.
|
||||
echo FILES TO DISTRIBUTE TO STUDENTS:
|
||||
echo 1. "%ARCHIVE_FILE%"
|
||||
echo 2. "%IMAGE_OUT_DIR%\production.compose.yml"
|
||||
echo 3. "%IMAGE_OUT_DIR%\.production.env" (with proper configuration)
|
||||
echo 4. "%IMAGE_OUT_DIR%\run-student-production.bat"
|
||||
echo.
|
||||
echo WHAT'S INCLUDED:
|
||||
echo - Production API Docker image
|
||||
echo - PostgreSQL database image
|
||||
echo - Initial seeding script (creates 5 categories, 16 products, 3 test users)
|
||||
echo - Placeholder product images (17 images)
|
||||
echo - Upload directory for student-generated images
|
||||
echo.
|
||||
echo STUDENTS SHOULD:
|
||||
echo 1. Extract webstore-production-images.tar
|
||||
echo 2. Copy .production.env and configure database credentials
|
||||
echo 3. Run run-student-production.bat
|
||||
echo 4. Database will auto-seed with sample data
|
||||
echo 5. Test users available:
|
||||
echo - admin@test.com / admin123
|
||||
echo - john@test.com / password123
|
||||
echo - jane@test.com / password123
|
||||
echo.
|
||||
exit /b 0
|
||||
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Image list from README
|
||||
const images = [
|
||||
// Shoes
|
||||
'street-runner.jpg',
|
||||
'trail-edge.jpg',
|
||||
'urban-sprint.jpg',
|
||||
'classic-comfort.jpg',
|
||||
// Bags
|
||||
'urban-tote.jpg',
|
||||
'backpack-pro.jpg',
|
||||
'crossbody.jpg',
|
||||
'duffle.jpg',
|
||||
// Accessories
|
||||
'classic-cap.jpg',
|
||||
'silk-scarf.jpg',
|
||||
'leather-belt.jpg',
|
||||
'watch-straps.jpg',
|
||||
// Clothing
|
||||
'cotton-tshirt.jpg',
|
||||
'denim-jacket.jpg',
|
||||
'yoga-leggings.jpg',
|
||||
// Electronics
|
||||
'earbuds.jpg',
|
||||
'usb-hub.jpg',
|
||||
];
|
||||
|
||||
const imagesDir = path.join(__dirname, '..', 'images');
|
||||
const uploadsDir = path.join(imagesDir, 'uploads');
|
||||
|
||||
// Create directories if they don't exist
|
||||
if (!fs.existsSync(imagesDir)) {
|
||||
fs.mkdirSync(imagesDir, { recursive: true });
|
||||
console.log(`Created ${imagesDir}`);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(uploadsDir)) {
|
||||
fs.mkdirSync(uploadsDir, { recursive: true });
|
||||
console.log(`Created ${uploadsDir}`);
|
||||
}
|
||||
|
||||
// Create simple JPEG placeholder images (minimal valid JPEG structure)
|
||||
// This is a minimal JPEG structure that works with image viewers
|
||||
const createPlaceholderJPEG = (filename, width, height) => {
|
||||
// Minimal JPEG header and footer
|
||||
const jpegHeader = Buffer.from([
|
||||
0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46,
|
||||
0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01,
|
||||
0x00, 0x01, 0x00, 0x00, 0xFF, 0xDB, 0x00, 0x43,
|
||||
0x00, 0x08, 0x06, 0x06, 0x07, 0x06, 0x05, 0x08,
|
||||
0x07, 0x07, 0x07, 0x09, 0x09, 0x08, 0x0A, 0x0C,
|
||||
0x14, 0x0D, 0x0C, 0x0B, 0x0B, 0x0C, 0x19, 0x12,
|
||||
0x13, 0x0F, 0x14, 0x1D, 0x1A, 0x1F, 0x1E, 0x1D,
|
||||
0x1A, 0x1C, 0x1C, 0x20, 0x24, 0x2E, 0x27, 0x20,
|
||||
0x22, 0x2C, 0x23, 0x1C, 0x1C, 0x28, 0x37, 0x29,
|
||||
0x2C, 0x30, 0x31, 0x34, 0x34, 0x34, 0x1F, 0x27,
|
||||
0x39, 0x3D, 0x38, 0x32, 0x3C, 0x2E, 0x33, 0x34,
|
||||
0x32, 0xFF, 0xC0, 0x00, 0x0B, 0x08, 0x00, height,
|
||||
0x00, width, 0x01, 0x01, 0x11, 0x00, 0xFF, 0xC4,
|
||||
0x00, 0x1F, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01,
|
||||
0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04,
|
||||
0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0xFF,
|
||||
0xC4, 0x00, 0xB5, 0x10, 0x00, 0x02, 0x01, 0x03,
|
||||
0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04,
|
||||
0x00, 0x00, 0x01, 0x7D, 0x01, 0x02, 0x03, 0x00,
|
||||
0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06,
|
||||
0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32,
|
||||
0x81, 0x91, 0xA1, 0x08, 0x23, 0x42, 0xB1, 0xC1,
|
||||
0x15, 0x52, 0xD1, 0xF0, 0x24, 0x33, 0x62, 0x72,
|
||||
0x82, 0x09, 0x0A, 0x16, 0x17, 0x18, 0x19, 0x1A,
|
||||
0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x34, 0x35,
|
||||
0x36, 0x37, 0x38, 0x39, 0x3A, 0x43, 0x44, 0x45,
|
||||
0x46, 0x47, 0x48, 0x49, 0x4A, 0x53, 0x54, 0x55,
|
||||
0x56, 0x57, 0x58, 0x59, 0x5A, 0x63, 0x64, 0x65,
|
||||
0x66, 0x67, 0x68, 0x69, 0x6A, 0x73, 0x74, 0x75,
|
||||
0x76, 0x77, 0x78, 0x79, 0x7A, 0x83, 0x84, 0x85,
|
||||
0x86, 0x87, 0x88, 0x89, 0x8A, 0x92, 0x93, 0x94,
|
||||
0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0xA2, 0xA3,
|
||||
0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xB2,
|
||||
0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA,
|
||||
0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9,
|
||||
0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8,
|
||||
0xD9, 0xDA, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6,
|
||||
0xE7, 0xE8, 0xE9, 0xEA, 0xF1, 0xF2, 0xF3, 0xF4,
|
||||
0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFF, 0xDA,
|
||||
0x00, 0x08, 0x01, 0x01, 0x00, 0x00, 0x3F, 0x00,
|
||||
0xFB, 0xD0, 0xFF, 0xD9,
|
||||
]);
|
||||
|
||||
return jpegHeader;
|
||||
};
|
||||
|
||||
let created = 0;
|
||||
images.forEach((filename) => {
|
||||
const filepath = path.join(imagesDir, filename);
|
||||
if (!fs.existsSync(filepath)) {
|
||||
const jpegData = createPlaceholderJPEG(filename, 400, 300);
|
||||
fs.writeFileSync(filepath, jpegData);
|
||||
console.log(`✓ Created ${filename}`);
|
||||
created++;
|
||||
} else {
|
||||
console.log(`✓ Already exists: ${filename}`);
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`\n${created} placeholder images generated successfully!`);
|
||||
console.log(`Images directory: ${imagesDir}`);
|
||||
@@ -0,0 +1,87 @@
|
||||
@echo off
|
||||
setlocal EnableExtensions EnableDelayedExpansion
|
||||
|
||||
::
|
||||
:: Generate test data script for webstore backend
|
||||
:: Usage: generate-test-data.bat [--products N] [--orders N] [--reset]
|
||||
::
|
||||
:: Examples:
|
||||
:: generate-test-data.bat (Creates 10 products, 5 orders)
|
||||
:: generate-test-data.bat --products 20 (Creates 20 products, 5 orders)
|
||||
:: generate-test-data.bat --products 50 --orders 10 --reset (Resets DB and generates data)
|
||||
::
|
||||
|
||||
cd /d "%~dp0.."
|
||||
|
||||
echo.
|
||||
echo ========================================
|
||||
echo Test Data Generator for Webstore
|
||||
echo ========================================
|
||||
echo.
|
||||
|
||||
set "PRODUCTS=10"
|
||||
set "ORDERS=5"
|
||||
set "RESET_FLAG="
|
||||
|
||||
:parse_args
|
||||
if "%~1"=="" goto start_generation
|
||||
if "%~1"=="--products" (
|
||||
if "%~2"=="" (
|
||||
echo ERROR: --products requires a number argument
|
||||
exit /b 1
|
||||
)
|
||||
set "PRODUCTS=%~2"
|
||||
shift
|
||||
shift
|
||||
goto parse_args
|
||||
)
|
||||
if "%~1"=="--orders" (
|
||||
if "%~2"=="" (
|
||||
echo ERROR: --orders requires a number argument
|
||||
exit /b 1
|
||||
)
|
||||
set "ORDERS=%~2"
|
||||
shift
|
||||
shift
|
||||
goto parse_args
|
||||
)
|
||||
if "%~1"=="--reset" (
|
||||
set "RESET_FLAG=--reset"
|
||||
shift
|
||||
goto parse_args
|
||||
)
|
||||
shift
|
||||
goto parse_args
|
||||
|
||||
:start_generation
|
||||
echo Configuration:
|
||||
echo Products to generate: %PRODUCTS%
|
||||
echo Orders to generate: %ORDERS%
|
||||
if defined RESET_FLAG (
|
||||
echo Reset database: YES (WARNING: All data will be deleted!)
|
||||
) else (
|
||||
echo Reset database: NO
|
||||
)
|
||||
echo.
|
||||
|
||||
echo Checking Node.js availability...
|
||||
node --version >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo ERROR: Node.js is not installed or not in PATH
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo Running test data generator...
|
||||
node scripts/generate-test-data.js --products %PRODUCTS% --orders %ORDERS% %RESET_FLAG%
|
||||
|
||||
if errorlevel 1 (
|
||||
echo.
|
||||
echo ERROR: Test data generation failed!
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo.
|
||||
echo ========================================
|
||||
echo ✓ Test data generated successfully!
|
||||
echo ========================================
|
||||
exit /b 0
|
||||
@@ -0,0 +1,263 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Script to generate bulk test data for the webstore database
|
||||
* Usage: node scripts/generate-test-data.js [options]
|
||||
*
|
||||
* Options:
|
||||
* --products N Generate N additional products (default: 10)
|
||||
* --orders N Generate N test orders (default: 5)
|
||||
* --reset Delete all data before seeding (careful!)
|
||||
*/
|
||||
|
||||
require("dotenv").config({ path: ".env" });
|
||||
|
||||
const { PrismaClient } = require("@prisma/client");
|
||||
const bcryptjs = require("bcryptjs");
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// Parse command line arguments
|
||||
const args = process.argv.slice(2);
|
||||
const options = {
|
||||
productsCount: 10,
|
||||
ordersCount: 5,
|
||||
shouldReset: false
|
||||
};
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i] === "--products" && args[i + 1]) {
|
||||
options.productsCount = parseInt(args[i + 1], 10);
|
||||
i++;
|
||||
} else if (args[i] === "--orders" && args[i + 1]) {
|
||||
options.ordersCount = parseInt(args[i + 1], 10);
|
||||
i++;
|
||||
} else if (args[i] === "--reset") {
|
||||
options.shouldReset = true;
|
||||
}
|
||||
}
|
||||
|
||||
const productNames = [
|
||||
"Premium Leather Boots",
|
||||
"Summer Canvas Shoes",
|
||||
"Minimalist Sneakers",
|
||||
"Casual Loafers",
|
||||
"Running Shoes Pro",
|
||||
"Hiking Boots",
|
||||
"Beach Sandals",
|
||||
"Formal Dress Shoes",
|
||||
"Winter Snow Boots",
|
||||
"Slip-on Comfort Shoes",
|
||||
"Designer Heels",
|
||||
"Waterproof Outdoor Boots",
|
||||
"Lightweight Mesh Runners",
|
||||
"Classic Oxfords",
|
||||
"Athletic Training Shoes"
|
||||
];
|
||||
|
||||
const descriptions = [
|
||||
"Premium quality with exceptional comfort",
|
||||
"Perfect for everyday wear and activities",
|
||||
"Stylish design with modern aesthetics",
|
||||
"Durable materials built to last",
|
||||
"Eco-friendly sustainable production",
|
||||
"Handcrafted with attention to detail",
|
||||
"Weather-resistant and waterproof",
|
||||
"Ergonomic design for all-day comfort",
|
||||
"Available in multiple colors",
|
||||
"Perfect for professional environments"
|
||||
];
|
||||
|
||||
function generateRandomPrice() {
|
||||
return (Math.floor(Math.random() * 20) + 5).toString() + "990.00";
|
||||
}
|
||||
|
||||
function generateRandomStock() {
|
||||
return Math.floor(Math.random() * 50) + 5;
|
||||
}
|
||||
|
||||
function getRandomElement(arr) {
|
||||
return arr[Math.floor(Math.random() * arr.length)];
|
||||
}
|
||||
|
||||
async function resetDatabase() {
|
||||
console.log("⚠️ Resetting database...");
|
||||
try {
|
||||
// Delete in order of dependencies
|
||||
await prisma.orderItem.deleteMany({});
|
||||
await prisma.order.deleteMany({});
|
||||
await prisma.product.deleteMany({});
|
||||
await prisma.category.deleteMany({});
|
||||
await prisma.user.deleteMany({});
|
||||
console.log("✓ Database reset completed");
|
||||
} catch (error) {
|
||||
console.error("Error resetting database:", error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function seedInitialData() {
|
||||
console.log("Seeding initial categories...");
|
||||
|
||||
const categories = [
|
||||
{ name: "Shoes", slug: "shoes" },
|
||||
{ name: "Bags", slug: "bags" },
|
||||
{ name: "Accessories", slug: "accessories" },
|
||||
{ name: "Clothing", slug: "clothing" },
|
||||
{ name: "Electronics", slug: "electronics" }
|
||||
];
|
||||
|
||||
for (const category of categories) {
|
||||
await prisma.category.upsert({
|
||||
where: { slug: category.slug },
|
||||
update: {},
|
||||
create: category
|
||||
});
|
||||
}
|
||||
|
||||
console.log("✓ Categories seeded");
|
||||
}
|
||||
|
||||
async function generateProducts(count) {
|
||||
console.log(`\nGenerating ${count} additional products...`);
|
||||
|
||||
const categories = await prisma.category.findMany();
|
||||
const generatedProducts = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const category = getRandomElement(categories);
|
||||
const name = `${getRandomElement(productNames)} ${i + 1}`;
|
||||
|
||||
try {
|
||||
const product = await prisma.product.create({
|
||||
data: {
|
||||
name,
|
||||
description: getRandomElement(descriptions),
|
||||
price: generateRandomPrice(),
|
||||
stock: generateRandomStock(),
|
||||
categoryId: category.id,
|
||||
imageUrl: `/images/placeholder-${i + 1}.jpg`
|
||||
}
|
||||
});
|
||||
generatedProducts.push(product);
|
||||
|
||||
if ((i + 1) % 5 === 0) {
|
||||
process.stdout.write(`\r Progress: ${i + 1}/${count} products`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (!error.message.includes("Unique constraint failed")) {
|
||||
console.error(`\nError creating product: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\r✓ Generated ${generatedProducts.length} products`);
|
||||
return generatedProducts;
|
||||
}
|
||||
|
||||
async function generateTestUsers(count = 5) {
|
||||
console.log(`\nGenerating ${count} test users...`);
|
||||
|
||||
const users = [];
|
||||
for (let i = 1; i <= count; i++) {
|
||||
const email = `user${i}@test.com`;
|
||||
const passwordHash = await bcryptjs.hash("password123", 10);
|
||||
|
||||
try {
|
||||
const user = await prisma.user.upsert({
|
||||
where: { email },
|
||||
update: {},
|
||||
create: {
|
||||
name: `Test User ${i}`,
|
||||
email,
|
||||
passwordHash
|
||||
}
|
||||
});
|
||||
users.push(user);
|
||||
} catch (error) {
|
||||
console.error(`Error creating user: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✓ Generated ${users.length} test users`);
|
||||
return users;
|
||||
}
|
||||
|
||||
async function generateOrders(count) {
|
||||
console.log(`\nGenerating ${count} test orders...`);
|
||||
|
||||
const products = await prisma.product.findMany({ take: 20 });
|
||||
const createdOrders = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
try {
|
||||
const itemsCount = Math.floor(Math.random() * 3) + 1;
|
||||
const items = [];
|
||||
let totalPrice = 0;
|
||||
|
||||
for (let j = 0; j < itemsCount; j++) {
|
||||
const product = getRandomElement(products);
|
||||
const quantity = Math.floor(Math.random() * 3) + 1;
|
||||
items.push({
|
||||
productId: product.id,
|
||||
quantity,
|
||||
unitPrice: product.price
|
||||
});
|
||||
totalPrice += parseFloat(product.price) * quantity;
|
||||
}
|
||||
|
||||
const order = await prisma.order.create({
|
||||
data: {
|
||||
customerName: `Customer ${i + 1}`,
|
||||
customerEmail: `customer${i + 1}@example.com`,
|
||||
totalPrice: totalPrice.toFixed(2),
|
||||
items: {
|
||||
create: items
|
||||
}
|
||||
},
|
||||
include: {
|
||||
items: true
|
||||
}
|
||||
});
|
||||
|
||||
createdOrders.push(order);
|
||||
|
||||
if ((i + 1) % 2 === 0) {
|
||||
process.stdout.write(`\r Progress: ${i + 1}/${count} orders`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`\nError creating order: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\r✓ Generated ${createdOrders.length} orders`);
|
||||
return createdOrders;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log("🚀 Test Data Generator for Webstore\n");
|
||||
console.log("Options:");
|
||||
console.log(` Products to generate: ${options.productsCount}`);
|
||||
console.log(` Orders to generate: ${options.ordersCount}`);
|
||||
console.log(` Reset database first: ${options.shouldReset ? "YES" : "NO"}\n`);
|
||||
|
||||
try {
|
||||
if (options.shouldReset) {
|
||||
await resetDatabase();
|
||||
}
|
||||
|
||||
await seedInitialData();
|
||||
await generateTestUsers(5);
|
||||
await generateProducts(options.productsCount);
|
||||
await generateOrders(options.ordersCount);
|
||||
|
||||
console.log("\n✨ Test data generation completed successfully!");
|
||||
} catch (error) {
|
||||
console.error("\n❌ Error during data generation:", error.message);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -1,13 +1,18 @@
|
||||
const GetCategoriesQuery = require("../../Application/Shop/Query/GetCategoriesQuery");
|
||||
const GetProductsQuery = require("../../Application/Shop/Query/GetProductsQuery");
|
||||
const GetProductByIdQuery = require("../../Application/Shop/Query/GetProductByIdQuery");
|
||||
const CreateOrderCommand = require("../../Application/Shop/Command/CreateOrderCommand");
|
||||
const CreateProductCommand = require("../../Application/Shop/Command/CreateProductCommand");
|
||||
const SetProductImageCommand = require("../../Application/Shop/Command/SetProductImageCommand");
|
||||
const ProductDTO = require("../../Application/DTO/ProductDTO");
|
||||
const container = require("../../Infrastructure/DI/container");
|
||||
|
||||
const getCategoriesHandler = container.resolve("GetCategoriesQueryHandler");
|
||||
const getProductsHandler = container.resolve("GetProductsQueryHandler");
|
||||
const getProductByIdHandler = container.resolve("GetProductByIdQueryHandler");
|
||||
const createOrderHandler = container.resolve("CreateOrderCommandHandler");
|
||||
const createProductHandler = container.resolve("CreateProductCommandHandler");
|
||||
const setProductImageHandler = container.resolve("SetProductImageCommandHandler");
|
||||
|
||||
const getCategories = async (req, res, next) => {
|
||||
try {
|
||||
@@ -22,7 +27,19 @@ const getCategories = async (req, res, next) => {
|
||||
const getProducts = async (req, res, next) => {
|
||||
try {
|
||||
const query = new GetProductsQuery();
|
||||
const data = await getProductsHandler.handle(query);
|
||||
const products = await getProductsHandler.handle(query);
|
||||
const data = products.map((product) => new ProductDTO(product, req));
|
||||
return res.json({ data });
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
};
|
||||
|
||||
const getProductById = async (req, res, next) => {
|
||||
try {
|
||||
const query = new GetProductByIdQuery(req.params.productId);
|
||||
const product = await getProductByIdHandler.handle(query);
|
||||
const data = new ProductDTO(product, req);
|
||||
return res.json({ data });
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
@@ -31,8 +48,13 @@ const getProducts = async (req, res, next) => {
|
||||
|
||||
const createProduct = async (req, res, next) => {
|
||||
try {
|
||||
const command = new CreateProductCommand(req.body);
|
||||
const data = await createProductHandler.handle(command);
|
||||
const payload = {
|
||||
...req.body,
|
||||
image_url: req.file ? `/images/uploads/${req.file.filename}` : req.body.image_url
|
||||
};
|
||||
const command = new CreateProductCommand(payload);
|
||||
const product = await createProductHandler.handle(command);
|
||||
const data = new ProductDTO(product, req);
|
||||
return res.status(201).json({ data });
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
@@ -49,9 +71,29 @@ const createOrder = async (req, res, next) => {
|
||||
}
|
||||
};
|
||||
|
||||
const setProductImage = async (req, res, next) => {
|
||||
try {
|
||||
if (!req.file) {
|
||||
const error = new Error("Image file is required");
|
||||
error.statusCode = 400;
|
||||
throw error;
|
||||
}
|
||||
|
||||
const imageUrl = `/images/uploads/${req.file.filename}`;
|
||||
const command = new SetProductImageCommand(req.params.productId, imageUrl);
|
||||
const product = await setProductImageHandler.handle(command);
|
||||
const data = new ProductDTO(product, req);
|
||||
return res.json({ data });
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getCategories,
|
||||
getProducts,
|
||||
getProductById,
|
||||
createProduct,
|
||||
createOrder
|
||||
createOrder,
|
||||
setProductImage
|
||||
};
|
||||
@@ -3,6 +3,11 @@ const errorHandler = (err, req, res, next) => {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (err.name === "MulterError") {
|
||||
const message = err.code === "LIMIT_FILE_SIZE" ? "Image size must be <= 5MB" : err.message;
|
||||
return res.status(400).json({ message });
|
||||
}
|
||||
|
||||
const status = err.statusCode || 500;
|
||||
return res.status(status).json({
|
||||
message: err.message || "Internal server error"
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const multer = require("multer");
|
||||
|
||||
const uploadDir = path.join(__dirname, "..", "..", "..", "images", "uploads");
|
||||
fs.mkdirSync(uploadDir, { recursive: true });
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: (req, file, cb) => {
|
||||
cb(null, uploadDir);
|
||||
},
|
||||
filename: (req, file, cb) => {
|
||||
const safeBaseName = path
|
||||
.parse(file.originalname)
|
||||
.name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/(^-|-$)/g, "")
|
||||
.slice(0, 40) || "image";
|
||||
const ext = path.extname(file.originalname).toLowerCase();
|
||||
cb(null, `${Date.now()}-${safeBaseName}${ext}`);
|
||||
}
|
||||
});
|
||||
|
||||
const fileFilter = (req, file, cb) => {
|
||||
if (!file.mimetype || !file.mimetype.startsWith("image/")) {
|
||||
const error = new Error("Only image files are allowed");
|
||||
error.statusCode = 400;
|
||||
return cb(error);
|
||||
}
|
||||
cb(null, true);
|
||||
};
|
||||
|
||||
const upload = multer({
|
||||
storage,
|
||||
fileFilter,
|
||||
limits: {
|
||||
fileSize: 5 * 1024 * 1024
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = upload;
|
||||
@@ -1,11 +1,14 @@
|
||||
const express = require("express");
|
||||
const controller = require("../Controller/shop.controller");
|
||||
const upload = require("../Middleware/upload.middleware");
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get("/categories", controller.getCategories);
|
||||
router.get("/products", controller.getProducts);
|
||||
router.post("/products", controller.createProduct);
|
||||
router.get("/products/:productId", controller.getProductById);
|
||||
router.post("/products", upload.single("image"), controller.createProduct);
|
||||
router.post("/products/:productId/image", upload.single("image"), controller.setProductImage);
|
||||
router.post("/orders", controller.createOrder);
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,4 +1,5 @@
|
||||
const express = require("express");
|
||||
const path = require("path");
|
||||
const helmet = require("helmet");
|
||||
const cors = require("cors");
|
||||
const morgan = require("morgan");
|
||||
@@ -16,6 +17,7 @@ app.use(cors({ origin: true, credentials: true }));
|
||||
app.use(morgan("dev"));
|
||||
app.use(express.json());
|
||||
app.use(cookieParser());
|
||||
app.use("/images", express.static(path.resolve(__dirname, "../../images")));
|
||||
|
||||
app.get("/api/health", (req, res) => {
|
||||
res.json({ status: "ok", message: "CQRS backend running" });
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
class ProductDTO {
|
||||
constructor(product, req) {
|
||||
this.id = product.id;
|
||||
this.category_id = product.categoryId;
|
||||
this.category_name = product.category ? product.category.name : null;
|
||||
this.name = product.name;
|
||||
this.description = product.description;
|
||||
this.price = Number(product.price);
|
||||
this.image_url = ProductDTO.#buildImageUrl(product.imageUrl, req);
|
||||
this.stock = product.stock;
|
||||
this.created_at = product.createdAt;
|
||||
}
|
||||
|
||||
static #buildImageUrl(imageUrl, req) {
|
||||
if (!imageUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (/^https?:\/\//i.test(imageUrl)) {
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
const host = req.get("host");
|
||||
return `${req.protocol}://${host}${imageUrl}`;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ProductDTO;
|
||||
@@ -0,0 +1,8 @@
|
||||
class SetProductImageCommand {
|
||||
constructor(productId, imageUrl) {
|
||||
this.product_id = productId;
|
||||
this.image_url = imageUrl;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SetProductImageCommand;
|
||||
@@ -0,0 +1,11 @@
|
||||
class SetProductImageCommandHandler {
|
||||
constructor(shopRepository) {
|
||||
this.shopRepository = shopRepository;
|
||||
}
|
||||
|
||||
async handle(command) {
|
||||
return this.shopRepository.setProductImage(command.product_id, command.image_url);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SetProductImageCommandHandler;
|
||||
@@ -0,0 +1,7 @@
|
||||
class GetProductByIdQuery {
|
||||
constructor(productId) {
|
||||
this.product_id = productId;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GetProductByIdQuery;
|
||||
@@ -0,0 +1,11 @@
|
||||
class GetProductByIdQueryHandler {
|
||||
constructor(shopRepository) {
|
||||
this.shopRepository = shopRepository;
|
||||
}
|
||||
|
||||
async handle(query) {
|
||||
return this.shopRepository.findProductById(query.product_id);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GetProductByIdQueryHandler;
|
||||
@@ -12,8 +12,10 @@ const GetCurrentUserQueryHandler = require("../../Application/User/Query/GetCurr
|
||||
|
||||
const GetCategoriesQueryHandler = require("../../Application/Shop/Query/GetCategoriesQueryHandler");
|
||||
const GetProductsQueryHandler = require("../../Application/Shop/Query/GetProductsQueryHandler");
|
||||
const GetProductByIdQueryHandler = require("../../Application/Shop/Query/GetProductByIdQueryHandler");
|
||||
const CreateProductCommandHandler = require("../../Application/Shop/Command/CreateProductCommandHandler");
|
||||
const CreateOrderCommandHandler = require("../../Application/Shop/Command/CreateOrderCommandHandler");
|
||||
const SetProductImageCommandHandler = require("../../Application/Shop/Command/SetProductImageCommandHandler");
|
||||
|
||||
const container = new DIContainer();
|
||||
|
||||
@@ -46,6 +48,10 @@ container.registerSingleton("GetProductsQueryHandler", (c) => {
|
||||
return new GetProductsQueryHandler(c.resolve("ShopRepository"));
|
||||
});
|
||||
|
||||
container.registerSingleton("GetProductByIdQueryHandler", (c) => {
|
||||
return new GetProductByIdQueryHandler(c.resolve("ShopRepository"));
|
||||
});
|
||||
|
||||
container.registerSingleton("CreateProductCommandHandler", (c) => {
|
||||
return new CreateProductCommandHandler(c.resolve("ShopRepository"));
|
||||
});
|
||||
@@ -54,4 +60,8 @@ container.registerSingleton("CreateOrderCommandHandler", (c) => {
|
||||
return new CreateOrderCommandHandler(c.resolve("ShopRepository"));
|
||||
});
|
||||
|
||||
container.registerSingleton("SetProductImageCommandHandler", (c) => {
|
||||
return new SetProductImageCommandHandler(c.resolve("ShopRepository"));
|
||||
});
|
||||
|
||||
module.exports = container;
|
||||
@@ -14,6 +14,30 @@ class ShopPrismaRepository {
|
||||
});
|
||||
}
|
||||
|
||||
async findProductById(productId) {
|
||||
const id = Number(productId);
|
||||
if (Number.isNaN(id)) {
|
||||
const error = new Error("product_id must be a number");
|
||||
error.statusCode = 400;
|
||||
throw error;
|
||||
}
|
||||
|
||||
const product = await prisma.product.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
category: true
|
||||
}
|
||||
});
|
||||
|
||||
if (!product) {
|
||||
const error = new Error("Product not found");
|
||||
error.statusCode = 404;
|
||||
throw error;
|
||||
}
|
||||
|
||||
return product;
|
||||
}
|
||||
|
||||
async createProduct(payload) {
|
||||
const categoryId = Number(payload.category_id);
|
||||
const stock = payload.stock == null ? 0 : Number(payload.stock);
|
||||
@@ -121,6 +145,33 @@ class ShopPrismaRepository {
|
||||
return order;
|
||||
});
|
||||
}
|
||||
|
||||
async setProductImage(productId, imageUrl) {
|
||||
const id = Number(productId);
|
||||
|
||||
if (Number.isNaN(id) || !imageUrl) {
|
||||
const error = new Error("product_id and image are required");
|
||||
error.statusCode = 400;
|
||||
throw error;
|
||||
}
|
||||
|
||||
const product = await prisma.product.findUnique({ where: { id } });
|
||||
if (!product) {
|
||||
const error = new Error("Product not found");
|
||||
error.statusCode = 404;
|
||||
throw error;
|
||||
}
|
||||
|
||||
return prisma.product.update({
|
||||
where: { id },
|
||||
data: {
|
||||
imageUrl
|
||||
},
|
||||
include: {
|
||||
category: true
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ShopPrismaRepository;
|
||||