updated frontend
@@ -62,8 +62,15 @@ Cookie is HTTP-only and set with `sameSite=lax`.
|
|||||||
- `GET /api/health`
|
- `GET /api/health`
|
||||||
- `GET /api/shop/categories`
|
- `GET /api/shop/categories`
|
||||||
- `GET /api/shop/products`
|
- `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`
|
- `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:
|
Order payload:
|
||||||
|
|
||||||
```json
|
```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 "ROOT_DIR=%~dp0"
|
||||||
set "ENV_FILE=%ROOT_DIR%\.production.env"
|
set "ENV_FILE=%ROOT_DIR%\.production.env"
|
||||||
set "COMPOSE_FILE=%ROOT_DIR%\production.compose.yml"
|
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"
|
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",
|
"express": "^4.19.2",
|
||||||
"helmet": "^7.1.0",
|
"helmet": "^7.1.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"morgan": "^1.10.0"
|
"morgan": "^1.10.0",
|
||||||
|
"multer": "^2.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"nodemon": "^3.1.4",
|
"nodemon": "^3.1.4",
|
||||||
@@ -120,6 +121,12 @@
|
|||||||
"node": ">= 8"
|
"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": {
|
"node_modules/array-flatten": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||||
@@ -229,6 +236,23 @@
|
|||||||
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
|
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
|
||||||
"license": "BSD-3-Clause"
|
"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": {
|
"node_modules/bytes": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||||
@@ -292,6 +316,21 @@
|
|||||||
"fsevents": "~2.3.2"
|
"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": {
|
"node_modules/content-disposition": {
|
||||||
"version": "0.5.4",
|
"version": "0.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||||
@@ -1020,6 +1059,25 @@
|
|||||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/negotiator": {
|
||||||
"version": "0.6.3",
|
"version": "0.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||||
@@ -1242,6 +1300,20 @@
|
|||||||
"node": ">= 0.8"
|
"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": {
|
"node_modules/readdirp": {
|
||||||
"version": "3.6.0",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||||
@@ -1438,6 +1510,23 @@
|
|||||||
"node": ">= 0.8"
|
"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": {
|
"node_modules/supports-color": {
|
||||||
"version": "5.5.0",
|
"version": "5.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
||||||
@@ -1496,6 +1585,12 @@
|
|||||||
"node": ">= 0.6"
|
"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": {
|
"node_modules/undefsafe": {
|
||||||
"version": "2.0.5",
|
"version": "2.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
|
||||||
@@ -1512,6 +1607,12 @@
|
|||||||
"node": ">= 0.8"
|
"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": {
|
"node_modules/utils-merge": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
"prisma:generate": "prisma generate",
|
"prisma:generate": "prisma generate",
|
||||||
"prisma:push": "prisma db push",
|
"prisma:push": "prisma db push",
|
||||||
"prisma:seed": "node prisma/seed.js",
|
"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"
|
"postinstall": "prisma generate"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
@@ -20,6 +22,7 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@prisma/client": "^5.20.0",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"cookie-parser": "^1.4.6",
|
"cookie-parser": "^1.4.6",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
@@ -28,10 +31,10 @@
|
|||||||
"helmet": "^7.1.0",
|
"helmet": "^7.1.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"@prisma/client": "^5.20.0"
|
"multer": "^2.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"nodemon": "^3.1.4",
|
"nodemon": "^3.1.4",
|
||||||
"prisma": "^5.20.0"
|
"prisma": "^5.20.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,21 @@
|
|||||||
const { PrismaClient } = require("@prisma/client");
|
const { PrismaClient } = require("@prisma/client");
|
||||||
|
const bcryptjs = require("bcryptjs");
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
|
console.log("Starting database seeding...");
|
||||||
|
|
||||||
|
// Seed categories
|
||||||
const categories = [
|
const categories = [
|
||||||
{ name: "Shoes", slug: "shoes" },
|
{ name: "Shoes", slug: "shoes" },
|
||||||
{ name: "Bags", slug: "bags" },
|
{ 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) {
|
for (const category of categories) {
|
||||||
await prisma.category.upsert({
|
await prisma.category.upsert({
|
||||||
where: { slug: category.slug },
|
where: { slug: category.slug },
|
||||||
@@ -20,12 +27,15 @@ async function main() {
|
|||||||
const shoes = await prisma.category.findUnique({ where: { slug: "shoes" } });
|
const shoes = await prisma.category.findUnique({ where: { slug: "shoes" } });
|
||||||
const bags = await prisma.category.findUnique({ where: { slug: "bags" } });
|
const bags = await prisma.category.findUnique({ where: { slug: "bags" } });
|
||||||
const accessories = await prisma.category.findUnique({ where: { slug: "accessories" } });
|
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 = [
|
const products = [
|
||||||
|
// Shoes
|
||||||
{
|
{
|
||||||
categoryId: shoes.id,
|
categoryId: shoes.id,
|
||||||
name: "Street Runner",
|
name: "Street Runner",
|
||||||
description: "Lightweight city sneaker.",
|
description: "Lightweight city sneaker perfect for daily commute.",
|
||||||
price: "18990.00",
|
price: "18990.00",
|
||||||
imageUrl: "/images/street-runner.jpg",
|
imageUrl: "/images/street-runner.jpg",
|
||||||
stock: 24
|
stock: 24
|
||||||
@@ -33,29 +43,138 @@ async function main() {
|
|||||||
{
|
{
|
||||||
categoryId: shoes.id,
|
categoryId: shoes.id,
|
||||||
name: "Trail Edge",
|
name: "Trail Edge",
|
||||||
description: "Stable shoe for outdoor tracks.",
|
description: "Stable shoe for outdoor tracks and hiking adventures.",
|
||||||
price: "24990.00",
|
price: "24990.00",
|
||||||
imageUrl: "/images/trail-edge.jpg",
|
imageUrl: "/images/trail-edge.jpg",
|
||||||
stock: 13
|
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,
|
categoryId: bags.id,
|
||||||
name: "Urban Tote",
|
name: "Urban Tote",
|
||||||
description: "Everyday tote with zipper top.",
|
description: "Everyday tote with zipper top and multiple compartments.",
|
||||||
price: "14990.00",
|
price: "14990.00",
|
||||||
imageUrl: "/images/urban-tote.jpg",
|
imageUrl: "/images/urban-tote.jpg",
|
||||||
stock: 30
|
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,
|
categoryId: accessories.id,
|
||||||
name: "Classic Cap",
|
name: "Classic Cap",
|
||||||
description: "Adjustable cotton cap.",
|
description: "Adjustable cotton cap with curved visor.",
|
||||||
price: "6990.00",
|
price: "6990.00",
|
||||||
imageUrl: "/images/classic-cap.jpg",
|
imageUrl: "/images/classic-cap.jpg",
|
||||||
stock: 42
|
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) {
|
for (const product of products) {
|
||||||
await prisma.product.upsert({
|
await prisma.product.upsert({
|
||||||
where: { name: product.name },
|
where: { name: product.name },
|
||||||
@@ -63,15 +182,37 @@ async function main() {
|
|||||||
create: product
|
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()
|
main()
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
await prisma.$disconnect();
|
await prisma.$disconnect();
|
||||||
console.log("Prisma seed completed.");
|
|
||||||
})
|
})
|
||||||
.catch(async (error) => {
|
.catch(async (error) => {
|
||||||
console.error(error);
|
console.error("Seeding error:", error);
|
||||||
await prisma.$disconnect();
|
await prisma.$disconnect();
|
||||||
process.exit(1);
|
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 "API_IMAGE=webstore-api:prod"
|
||||||
set "DB_IMAGE=postgres:16-alpine"
|
set "DB_IMAGE=postgres:16-alpine"
|
||||||
|
|
||||||
echo [1/6] Checking Docker availability...
|
echo [1/7] Checking Docker availability...
|
||||||
docker --version >nul 2>&1
|
docker --version >nul 2>&1
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
echo ERROR: Docker CLI is not available. Install Docker Desktop first.
|
echo ERROR: Docker CLI is not available. Install Docker Desktop first.
|
||||||
exit /b 1
|
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
|
pushd "%ROOT_DIR%" >nul
|
||||||
docker build -t "%API_IMAGE%" -f Dockerfile .
|
docker build -t "%API_IMAGE%" -f Dockerfile .
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
@@ -25,17 +25,33 @@ if errorlevel 1 (
|
|||||||
)
|
)
|
||||||
popd >nul
|
popd >nul
|
||||||
|
|
||||||
echo [3/6] Pulling database image: %DB_IMAGE%
|
echo [3/7] Pulling database image: %DB_IMAGE%
|
||||||
docker pull "%DB_IMAGE%"
|
docker pull "%DB_IMAGE%"
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
echo ERROR: Failed to pull database image.
|
echo ERROR: Failed to pull database image.
|
||||||
exit /b 1
|
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%"
|
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%"
|
if exist "%ARCHIVE_FILE%" del /f /q "%ARCHIVE_FILE%"
|
||||||
docker save -o "%ARCHIVE_FILE%" "%API_IMAGE%" "%DB_IMAGE%"
|
docker save -o "%ARCHIVE_FILE%" "%API_IMAGE%" "%DB_IMAGE%"
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
@@ -43,7 +59,35 @@ if errorlevel 1 (
|
|||||||
exit /b 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 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
|
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 GetCategoriesQuery = require("../../Application/Shop/Query/GetCategoriesQuery");
|
||||||
const GetProductsQuery = require("../../Application/Shop/Query/GetProductsQuery");
|
const GetProductsQuery = require("../../Application/Shop/Query/GetProductsQuery");
|
||||||
|
const GetProductByIdQuery = require("../../Application/Shop/Query/GetProductByIdQuery");
|
||||||
const CreateOrderCommand = require("../../Application/Shop/Command/CreateOrderCommand");
|
const CreateOrderCommand = require("../../Application/Shop/Command/CreateOrderCommand");
|
||||||
const CreateProductCommand = require("../../Application/Shop/Command/CreateProductCommand");
|
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 container = require("../../Infrastructure/DI/container");
|
||||||
|
|
||||||
const getCategoriesHandler = container.resolve("GetCategoriesQueryHandler");
|
const getCategoriesHandler = container.resolve("GetCategoriesQueryHandler");
|
||||||
const getProductsHandler = container.resolve("GetProductsQueryHandler");
|
const getProductsHandler = container.resolve("GetProductsQueryHandler");
|
||||||
|
const getProductByIdHandler = container.resolve("GetProductByIdQueryHandler");
|
||||||
const createOrderHandler = container.resolve("CreateOrderCommandHandler");
|
const createOrderHandler = container.resolve("CreateOrderCommandHandler");
|
||||||
const createProductHandler = container.resolve("CreateProductCommandHandler");
|
const createProductHandler = container.resolve("CreateProductCommandHandler");
|
||||||
|
const setProductImageHandler = container.resolve("SetProductImageCommandHandler");
|
||||||
|
|
||||||
const getCategories = async (req, res, next) => {
|
const getCategories = async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
@@ -22,7 +27,19 @@ const getCategories = async (req, res, next) => {
|
|||||||
const getProducts = async (req, res, next) => {
|
const getProducts = async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const query = new GetProductsQuery();
|
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 });
|
return res.json({ data });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return next(error);
|
return next(error);
|
||||||
@@ -31,8 +48,13 @@ const getProducts = async (req, res, next) => {
|
|||||||
|
|
||||||
const createProduct = async (req, res, next) => {
|
const createProduct = async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const command = new CreateProductCommand(req.body);
|
const payload = {
|
||||||
const data = await createProductHandler.handle(command);
|
...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 });
|
return res.status(201).json({ data });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return next(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 = {
|
module.exports = {
|
||||||
getCategories,
|
getCategories,
|
||||||
getProducts,
|
getProducts,
|
||||||
|
getProductById,
|
||||||
createProduct,
|
createProduct,
|
||||||
createOrder
|
createOrder,
|
||||||
|
setProductImage
|
||||||
};
|
};
|
||||||
@@ -3,6 +3,11 @@ const errorHandler = (err, req, res, next) => {
|
|||||||
return next(err);
|
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;
|
const status = err.statusCode || 500;
|
||||||
return res.status(status).json({
|
return res.status(status).json({
|
||||||
message: err.message || "Internal server error"
|
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 express = require("express");
|
||||||
const controller = require("../Controller/shop.controller");
|
const controller = require("../Controller/shop.controller");
|
||||||
|
const upload = require("../Middleware/upload.middleware");
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
router.get("/categories", controller.getCategories);
|
router.get("/categories", controller.getCategories);
|
||||||
router.get("/products", controller.getProducts);
|
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);
|
router.post("/orders", controller.createOrder);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
|
const path = require("path");
|
||||||
const helmet = require("helmet");
|
const helmet = require("helmet");
|
||||||
const cors = require("cors");
|
const cors = require("cors");
|
||||||
const morgan = require("morgan");
|
const morgan = require("morgan");
|
||||||
@@ -16,6 +17,7 @@ app.use(cors({ origin: true, credentials: true }));
|
|||||||
app.use(morgan("dev"));
|
app.use(morgan("dev"));
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
|
app.use("/images", express.static(path.resolve(__dirname, "../../images")));
|
||||||
|
|
||||||
app.get("/api/health", (req, res) => {
|
app.get("/api/health", (req, res) => {
|
||||||
res.json({ status: "ok", message: "CQRS backend running" });
|
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 GetCategoriesQueryHandler = require("../../Application/Shop/Query/GetCategoriesQueryHandler");
|
||||||
const GetProductsQueryHandler = require("../../Application/Shop/Query/GetProductsQueryHandler");
|
const GetProductsQueryHandler = require("../../Application/Shop/Query/GetProductsQueryHandler");
|
||||||
|
const GetProductByIdQueryHandler = require("../../Application/Shop/Query/GetProductByIdQueryHandler");
|
||||||
const CreateProductCommandHandler = require("../../Application/Shop/Command/CreateProductCommandHandler");
|
const CreateProductCommandHandler = require("../../Application/Shop/Command/CreateProductCommandHandler");
|
||||||
const CreateOrderCommandHandler = require("../../Application/Shop/Command/CreateOrderCommandHandler");
|
const CreateOrderCommandHandler = require("../../Application/Shop/Command/CreateOrderCommandHandler");
|
||||||
|
const SetProductImageCommandHandler = require("../../Application/Shop/Command/SetProductImageCommandHandler");
|
||||||
|
|
||||||
const container = new DIContainer();
|
const container = new DIContainer();
|
||||||
|
|
||||||
@@ -46,6 +48,10 @@ container.registerSingleton("GetProductsQueryHandler", (c) => {
|
|||||||
return new GetProductsQueryHandler(c.resolve("ShopRepository"));
|
return new GetProductsQueryHandler(c.resolve("ShopRepository"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
container.registerSingleton("GetProductByIdQueryHandler", (c) => {
|
||||||
|
return new GetProductByIdQueryHandler(c.resolve("ShopRepository"));
|
||||||
|
});
|
||||||
|
|
||||||
container.registerSingleton("CreateProductCommandHandler", (c) => {
|
container.registerSingleton("CreateProductCommandHandler", (c) => {
|
||||||
return new CreateProductCommandHandler(c.resolve("ShopRepository"));
|
return new CreateProductCommandHandler(c.resolve("ShopRepository"));
|
||||||
});
|
});
|
||||||
@@ -54,4 +60,8 @@ container.registerSingleton("CreateOrderCommandHandler", (c) => {
|
|||||||
return new CreateOrderCommandHandler(c.resolve("ShopRepository"));
|
return new CreateOrderCommandHandler(c.resolve("ShopRepository"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
container.registerSingleton("SetProductImageCommandHandler", (c) => {
|
||||||
|
return new SetProductImageCommandHandler(c.resolve("ShopRepository"));
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = container;
|
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) {
|
async createProduct(payload) {
|
||||||
const categoryId = Number(payload.category_id);
|
const categoryId = Number(payload.category_id);
|
||||||
const stock = payload.stock == null ? 0 : Number(payload.stock);
|
const stock = payload.stock == null ? 0 : Number(payload.stock);
|
||||||
@@ -121,6 +145,33 @@ class ShopPrismaRepository {
|
|||||||
return order;
|
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;
|
module.exports = ShopPrismaRepository;
|
||||||