542 lines
14 KiB
Markdown
542 lines
14 KiB
Markdown
# Device Bulk Operations with Images - Complete Guide
|
|
|
|
## Overview
|
|
|
|
This guide explains how to use bulk create and update operations for devices with support for multiple images per device. The system allows you to upload images along with device data in a single request.
|
|
|
|
## Features
|
|
|
|
✅ **Bulk Create with Images** - Create multiple devices, each with multiple images
|
|
✅ **Bulk Update with Images** - Update multiple devices and add/replace images
|
|
✅ **Flexible Image Distribution** - Each device can have a different number of images
|
|
✅ **Transaction Safety** - All operations are database transaction-safe
|
|
✅ **Individual Validation** - Each device is validated separately
|
|
✅ **Detailed Error Reporting** - Get specific errors for failed items
|
|
✅ **Image Management** - Replace or append images to existing devices
|
|
|
|
---
|
|
|
|
## Endpoints
|
|
|
|
### 1. Bulk Create Devices (without images)
|
|
**Endpoint:** `POST /device/bulk/create`
|
|
**Content-Type:** `application/json`
|
|
|
|
```json
|
|
{
|
|
"devices": [
|
|
{
|
|
"device_code": "ODP-001",
|
|
"device_type": "ODP",
|
|
"longitude": 106.8456,
|
|
"latitude": -6.2088,
|
|
"port_amount": 8,
|
|
"status": "active",
|
|
"province": "DKI Jakarta",
|
|
"city": "Jakarta Selatan",
|
|
"district": "Kebayoran Baru",
|
|
"tower_id": "123e4567-e89b-12d3-a456-426614174000",
|
|
"olt_id": "123e4567-e89b-12d3-a456-426614174001"
|
|
},
|
|
{
|
|
"device_code": "OTB-001",
|
|
"device_type": "OTB",
|
|
"longitude": 106.8556,
|
|
"latitude": -6.2188,
|
|
"port_amount": 16,
|
|
"status": "active",
|
|
"province": "DKI Jakarta",
|
|
"city": "Jakarta Selatan"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
### 2. Bulk Create Devices (with images)
|
|
**Endpoint:** `POST /device/bulk/create`
|
|
**Content-Type:** `multipart/form-data`
|
|
|
|
**Form Fields:**
|
|
- `devices` (JSON string) - Array of device objects
|
|
- `image_indexes` (JSON array) - Number of images for each device
|
|
- `images` (files) - All image files in sequence
|
|
|
|
**Example cURL:**
|
|
```bash
|
|
curl -X POST http://localhost:8080/device/bulk/create \
|
|
-H "Authorization: Bearer YOUR_TOKEN" \
|
|
-F 'devices=[{"device_code":"ODP-001","device_type":"ODP","longitude":106.8456,"latitude":-6.2088,"port_amount":8,"status":"active","province":"DKI Jakarta","city":"Jakarta Selatan"}]' \
|
|
-F 'image_indexes=[2]' \
|
|
-F "images=@device1_photo1.jpg" \
|
|
-F "images=@device1_photo2.jpg"
|
|
```
|
|
|
|
**JavaScript/Fetch Example:**
|
|
```javascript
|
|
const formData = new FormData();
|
|
|
|
// Device data
|
|
const devices = [
|
|
{
|
|
device_code: "ODP-001",
|
|
device_type: "ODP",
|
|
longitude: 106.8456,
|
|
latitude: -6.2088,
|
|
port_amount: 8,
|
|
status: "active",
|
|
province: "DKI Jakarta",
|
|
city: "Jakarta Selatan"
|
|
},
|
|
{
|
|
device_code: "OTB-001",
|
|
device_type: "OTB",
|
|
longitude: 106.8556,
|
|
latitude: -6.2188,
|
|
port_amount: 16,
|
|
status: "active"
|
|
}
|
|
];
|
|
|
|
formData.append('devices', JSON.stringify(devices));
|
|
|
|
// Image distribution: first device has 2 images, second has 1 image
|
|
formData.append('image_indexes', JSON.stringify([2, 1]));
|
|
|
|
// Add images in order
|
|
formData.append('images', file1); // for device 0, image 1
|
|
formData.append('images', file2); // for device 0, image 2
|
|
formData.append('images', file3); // for device 1, image 1
|
|
|
|
const response = await fetch('/device/bulk/create', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': 'Bearer YOUR_TOKEN'
|
|
},
|
|
body: formData
|
|
});
|
|
```
|
|
|
|
### 3. Bulk Update Devices (without images)
|
|
**Endpoint:** `PUT /device/bulk/update`
|
|
**Content-Type:** `application/json`
|
|
|
|
```json
|
|
{
|
|
"device_ids": [
|
|
"123e4567-e89b-12d3-a456-426614174000",
|
|
"123e4567-e89b-12d3-a456-426614174001"
|
|
],
|
|
"updates": {
|
|
"status": "maintenance",
|
|
"province": "DKI Jakarta"
|
|
}
|
|
}
|
|
```
|
|
|
|
### 4. Bulk Update Devices (with images)
|
|
**Endpoint:** `PUT /device/bulk/update`
|
|
**Content-Type:** `multipart/form-data`
|
|
|
|
**Form Fields:**
|
|
- `device_ids` (JSON array) - Array of device UUIDs
|
|
- `updates` (JSON object) - Fields to update
|
|
- `image_indexes` (JSON array) - Number of images for each device
|
|
- `images` (files) - All image files in sequence
|
|
- `replace_images` (boolean string) - "true" to replace, "false" to append (default: false)
|
|
|
|
**Example cURL:**
|
|
```bash
|
|
curl -X PUT http://localhost:8080/device/bulk/update \
|
|
-H "Authorization: Bearer YOUR_TOKEN" \
|
|
-F 'device_ids=["123e4567-e89b-12d3-a456-426614174000","123e4567-e89b-12d3-a456-426614174001"]' \
|
|
-F 'updates={"status":"active"}' \
|
|
-F 'image_indexes=[1,2]' \
|
|
-F 'replace_images=false' \
|
|
-F "images=@new_image1.jpg" \
|
|
-F "images=@new_image2.jpg" \
|
|
-F "images=@new_image3.jpg"
|
|
```
|
|
|
|
**JavaScript/Fetch Example:**
|
|
```javascript
|
|
const formData = new FormData();
|
|
|
|
const deviceIds = [
|
|
"123e4567-e89b-12d3-a456-426614174000",
|
|
"123e4567-e89b-12d3-a456-426614174001"
|
|
];
|
|
|
|
const updates = {
|
|
status: "active",
|
|
province: "DKI Jakarta"
|
|
};
|
|
|
|
formData.append('device_ids', JSON.stringify(deviceIds));
|
|
formData.append('updates', JSON.stringify(updates));
|
|
formData.append('image_indexes', JSON.stringify([1, 2])); // 1 image for first device, 2 for second
|
|
formData.append('replace_images', 'false'); // Append to existing images
|
|
|
|
formData.append('images', newFile1); // for device 0
|
|
formData.append('images', newFile2); // for device 1, image 1
|
|
formData.append('images', newFile3); // for device 1, image 2
|
|
|
|
const response = await fetch('/device/bulk/update', {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Authorization': 'Bearer YOUR_TOKEN'
|
|
},
|
|
body: formData
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## Image Distribution Logic
|
|
|
|
The `image_indexes` array controls how images are distributed to devices:
|
|
|
|
```javascript
|
|
// Example 1: Create 3 devices with different image counts
|
|
devices = [device1, device2, device3]
|
|
image_indexes = [2, 0, 3] // device1: 2 images, device2: 0 images, device3: 3 images
|
|
images = [img1, img2, img3, img4, img5] // Total: 5 images (2+0+3)
|
|
|
|
// Distribution:
|
|
// device1 gets: [img1, img2]
|
|
// device2 gets: []
|
|
// device3 gets: [img3, img4, img5]
|
|
|
|
// Example 2: Update 2 devices
|
|
device_ids = [uuid1, uuid2]
|
|
image_indexes = [1, 1] // Each device gets 1 image
|
|
images = [img1, img2] // Total: 2 images (1+1)
|
|
|
|
// Distribution:
|
|
// uuid1 gets: [img1]
|
|
// uuid2 gets: [img2]
|
|
```
|
|
|
|
**Important Rules:**
|
|
- Length of `image_indexes` MUST equal number of devices/device_ids
|
|
- Sum of `image_indexes` MUST equal total number of image files
|
|
- Images are assigned in order based on the index distribution
|
|
|
|
---
|
|
|
|
## Response Format
|
|
|
|
### Success Response
|
|
```json
|
|
{
|
|
"message": "Bulk create completed: 3 successful, 1 failed out of 4 requested (with 7 images)",
|
|
"data": {
|
|
"total_requested": 4,
|
|
"successful": 3,
|
|
"failed": 1,
|
|
"errors": [
|
|
{
|
|
"index": 2,
|
|
"error": "Tower not found",
|
|
"details": "123e4567-e89b-12d3-a456-426614174000"
|
|
}
|
|
],
|
|
"results": [
|
|
{
|
|
"id": "123e4567-e89b-12d3-a456-426614174100",
|
|
"device_code": "ODP-001",
|
|
"device_type": "ODP",
|
|
"longitude": 106.8456,
|
|
"latitude": -6.2088,
|
|
"port_amount": 8,
|
|
"status": "active",
|
|
"address": "Jakarta Selatan, DKI Jakarta",
|
|
"province": "DKI Jakarta",
|
|
"city": "Jakarta Selatan",
|
|
"district": "Kebayoran Baru",
|
|
"image_url": "/uploads/devices/abc123.jpg",
|
|
"image_urls": [
|
|
"/uploads/devices/abc123.jpg",
|
|
"/uploads/devices/def456.jpg"
|
|
],
|
|
"created_at": "2025-10-10T10:30:00Z",
|
|
"updated_at": "2025-10-10T10:30:00Z"
|
|
}
|
|
],
|
|
"execution_time": "245ms"
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Validation Rules
|
|
|
|
### Device Validation
|
|
1. **Device Type**: Must be "ODP", "OTB", or "closure"
|
|
2. **Port Amount**: Required and > 0 for ODP and OTB devices
|
|
3. **Status**: Must be "active", "inactive", or "maintenance"
|
|
4. **OLT Assignment**: Only ODP devices can be assigned to OLT
|
|
5. **Tower**: Must exist in the database if provided
|
|
6. **OLT**: Must exist in the database if provided
|
|
|
|
### Image Validation
|
|
1. **File Size**: Maximum 5MB per image
|
|
2. **File Type**: Only .jpg, .jpeg, .png, .webp
|
|
3. **Image Distribution**: Must match the formula: `sum(image_indexes) == len(images)`
|
|
4. **Index Length**: `len(image_indexes) == len(devices)` or `len(device_ids)`
|
|
|
|
---
|
|
|
|
## Image Handling
|
|
|
|
### Primary Image
|
|
- The **first image** for each device becomes the **primary image** (`image_url`)
|
|
- Primary image is displayed as the main device photo
|
|
|
|
### Multiple Images
|
|
- All images are stored in `image_urls` array (JSONB field)
|
|
- Images are saved in `/uploads/devices/` directory
|
|
- Filenames are auto-generated with UUID + timestamp
|
|
|
|
### Replace vs Append (Update only)
|
|
- **Replace (`replace_images=true`)**: Remove all existing images, add new ones
|
|
- **Append (`replace_images=false`)**: Keep existing images, add new ones
|
|
|
|
---
|
|
|
|
## Complete Examples
|
|
|
|
### Example 1: Create 2 Devices with Images
|
|
|
|
```javascript
|
|
const formData = new FormData();
|
|
|
|
// Device 1: ODP with 3 images
|
|
// Device 2: OTB with 1 image
|
|
const devices = [
|
|
{
|
|
device_code: "ODP-BULK-001",
|
|
device_type: "ODP",
|
|
longitude: 106.8456,
|
|
latitude: -6.2088,
|
|
port_amount: 8,
|
|
status: "active",
|
|
province: "DKI Jakarta",
|
|
city: "Jakarta Selatan",
|
|
olt_id: "550e8400-e29b-41d4-a716-446655440000"
|
|
},
|
|
{
|
|
device_code: "OTB-BULK-001",
|
|
device_type: "OTB",
|
|
longitude: 106.8556,
|
|
latitude: -6.2188,
|
|
port_amount: 16,
|
|
status: "active",
|
|
tower_id: "550e8400-e29b-41d4-a716-446655440001"
|
|
}
|
|
];
|
|
|
|
formData.append('devices', JSON.stringify(devices));
|
|
formData.append('image_indexes', JSON.stringify([3, 1])); // 3 images for first, 1 for second
|
|
|
|
// Add 4 total images (3+1)
|
|
formData.append('images', odpImage1);
|
|
formData.append('images', odpImage2);
|
|
formData.append('images', odpImage3);
|
|
formData.append('images', otbImage1);
|
|
|
|
fetch('/device/bulk/create', {
|
|
method: 'POST',
|
|
headers: { 'Authorization': 'Bearer YOUR_TOKEN' },
|
|
body: formData
|
|
})
|
|
.then(res => res.json())
|
|
.then(data => console.log(data));
|
|
```
|
|
|
|
### Example 2: Update Devices and Add Images
|
|
|
|
```javascript
|
|
const formData = new FormData();
|
|
|
|
const deviceIds = [
|
|
"550e8400-e29b-41d4-a716-446655440010",
|
|
"550e8400-e29b-41d4-a716-446655440011"
|
|
];
|
|
|
|
const updates = {
|
|
status: "maintenance"
|
|
};
|
|
|
|
formData.append('device_ids', JSON.stringify(deviceIds));
|
|
formData.append('updates', JSON.stringify(updates));
|
|
formData.append('image_indexes', JSON.stringify([2, 0])); // Add 2 images to first device, 0 to second
|
|
formData.append('replace_images', 'false'); // Append to existing images
|
|
|
|
formData.append('images', newImage1);
|
|
formData.append('images', newImage2);
|
|
|
|
fetch('/device/bulk/update', {
|
|
method: 'PUT',
|
|
headers: { 'Authorization': 'Bearer YOUR_TOKEN' },
|
|
body: formData
|
|
})
|
|
.then(res => res.json())
|
|
.then(data => console.log(data));
|
|
```
|
|
|
|
### Example 3: No Images (JSON only)
|
|
|
|
For operations without images, use regular JSON:
|
|
|
|
```javascript
|
|
// Create without images
|
|
fetch('/device/bulk/create', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': 'Bearer YOUR_TOKEN',
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
devices: [
|
|
{
|
|
device_code: "ODP-NO-IMG-001",
|
|
device_type: "ODP",
|
|
longitude: 106.8456,
|
|
latitude: -6.2088,
|
|
port_amount: 8,
|
|
status: "active"
|
|
}
|
|
]
|
|
})
|
|
});
|
|
|
|
// Update without images
|
|
fetch('/device/bulk/update', {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Authorization': 'Bearer YOUR_TOKEN',
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
device_ids: ["550e8400-e29b-41d4-a716-446655440010"],
|
|
updates: {
|
|
status: "active"
|
|
}
|
|
})
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## Error Handling
|
|
|
|
### Common Errors
|
|
|
|
1. **Invalid image_indexes length**
|
|
```json
|
|
{
|
|
"error": "image_indexes length (3) must match devices length (2)"
|
|
}
|
|
```
|
|
|
|
2. **Image count mismatch**
|
|
```json
|
|
{
|
|
"error": "total images (5) doesn't match sum of image_indexes (4)"
|
|
}
|
|
```
|
|
|
|
3. **Invalid device type**
|
|
```json
|
|
{
|
|
"data": {
|
|
"errors": [
|
|
{
|
|
"index": 0,
|
|
"error": "Invalid device type",
|
|
"details": "Type must be ODP, OTB, or closure, got: INVALID"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
4. **Image upload failed**
|
|
```json
|
|
{
|
|
"data": {
|
|
"errors": [
|
|
{
|
|
"index": 1,
|
|
"error": "Failed to save images",
|
|
"details": "file 0: file size exceeds 5MB limit"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Performance
|
|
|
|
- **Batch Processing**: 50 devices per database batch
|
|
- **Transaction Safety**: All creates/updates in transactions
|
|
- **Typical Performance**: 4-10x faster than individual requests
|
|
- **Max File Size**: 5MB per image
|
|
- **Max Request Size**: 100MB total (for bulk operations)
|
|
|
|
---
|
|
|
|
## Authorization
|
|
|
|
All bulk operations require authentication and proper role:
|
|
- **Roles Allowed**: Teknisi, Admin, Super Admin
|
|
- **Header**: `Authorization: Bearer YOUR_JWT_TOKEN`
|
|
|
|
---
|
|
|
|
## Best Practices
|
|
|
|
1. **Limit Batch Size**: Keep requests under 50 devices for optimal performance
|
|
2. **Validate Before Upload**: Verify device data before sending
|
|
3. **Use Proper Image Formats**: Stick to JPG/PNG for best compatibility
|
|
4. **Handle Partial Failures**: Check the `errors` array in response
|
|
5. **Image Naming**: Use descriptive filenames before upload (for debugging)
|
|
6. **Test Image Distribution**: Verify `image_indexes` sum matches image count
|
|
|
|
---
|
|
|
|
## Testing with Postman
|
|
|
|
### Setup
|
|
1. Set URL: `http://localhost:8080/device/bulk/create`
|
|
2. Method: `POST`
|
|
3. Authorization: Bearer Token
|
|
4. Body: `form-data`
|
|
|
|
### Form Data Fields
|
|
| Key | Type | Value |
|
|
|-----|------|-------|
|
|
| devices | Text | `[{"device_code":"TEST-001","device_type":"ODP","longitude":106.8,"latitude":-6.2,"port_amount":8,"status":"active"}]` |
|
|
| image_indexes | Text | `[2]` |
|
|
| images | File | Select image 1 |
|
|
| images | File | Select image 2 |
|
|
|
|
### Send Request
|
|
Click "Send" and verify response contains uploaded image URLs.
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
✅ **Two modes**: JSON (no images) or multipart/form-data (with images)
|
|
✅ **Flexible distribution**: Each device can have 0-N images
|
|
✅ **Replace or append**: Control image update behavior
|
|
✅ **Transaction safe**: Rollback on failures
|
|
✅ **Detailed errors**: Know exactly what failed and why
|
|
✅ **High performance**: Batch processing with GORM
|
|
|
|
Use bulk operations with images to efficiently manage large numbers of devices with their photos in a single request! 🚀
|