Add initial project structure with core functionality for book and stationery uploads
- Created main application entry point in `main.go`. - Added configuration management in `config/config.go` and tests in `config/config_test.go`. - Implemented handlers for book and stationery uploads in `handlers/book.go` and `handlers/stationery.go`, including validation logic. - Established database connection and services in `services/database.go` and `services/book_service.go`. - Defined models for books and stationery in `models/book.go` and `models/stationery.go`. - Set up Firebase integration for image uploads in `services/firebase.go`. - Created migration scripts for database schema in `migrations/001_create_tables.sql` and subsequent updates. - Added CORS and error handling middleware. - Included comprehensive tests for handlers, services, and utilities. - Documented API endpoints and usage in `README.md` and migration instructions in `migrations/README.md`. - Introduced `.gitignore` to exclude unnecessary files and directories. - Added Go module support with `go.mod` and `go.sum` files. - Implemented utility functions for slug generation and validation in `utils/slug.go` and `utils/validation.go`.
This commit is contained in:
490
README.md
Normal file
490
README.md
Normal file
@@ -0,0 +1,490 @@
|
||||
# JD Book Uploader - Backend API Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
Backend API for uploading book and stationery products with images to Firebase Storage and storing metadata in PostgreSQL.
|
||||
|
||||
## Base URL
|
||||
|
||||
- **Development**: `http://localhost:8080`
|
||||
- **Production**: Configure via `FRONTEND_URL` environment variable
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### 1. Health Check
|
||||
|
||||
**Endpoint:** `GET /api/health`
|
||||
|
||||
**Description:** Check the health status of the API and its dependencies (database and Firebase).
|
||||
|
||||
**Request:**
|
||||
- **Method:** `GET`
|
||||
- **Path:** `/api/health`
|
||||
- **Headers:** None required
|
||||
- **Body:** None
|
||||
|
||||
**Response:**
|
||||
|
||||
**Success (200 OK):**
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"database": "connected",
|
||||
"firebase": "connected"
|
||||
}
|
||||
```
|
||||
|
||||
**Service Unavailable (503):**
|
||||
```json
|
||||
{
|
||||
"status": "degraded",
|
||||
"database": "disconnected",
|
||||
"firebase": "connected"
|
||||
}
|
||||
```
|
||||
|
||||
**Response Fields:**
|
||||
- `status` (string): `"ok"` if all services connected, `"degraded"` if any service unavailable
|
||||
- `database` (string): `"connected"`, `"disconnected"`, or `"not_initialized"`
|
||||
- `firebase` (string): `"connected"`, `"disconnected"`, or `"not_initialized"`
|
||||
|
||||
---
|
||||
|
||||
### 2. Upload Book
|
||||
|
||||
**Endpoint:** `POST /api/books`
|
||||
|
||||
**Description:** Upload a book product with cover image. Image is uploaded to Firebase Storage and book metadata is stored in PostgreSQL.
|
||||
|
||||
**Request:**
|
||||
- **Method:** `POST`
|
||||
- **Path:** `/api/books`
|
||||
- **Content-Type:** `multipart/form-data`
|
||||
- **Body:** FormData with the following fields:
|
||||
|
||||
**Required Parameters:**
|
||||
- `image` (File) - Book cover image file
|
||||
- **Type:** File (multipart/form-data)
|
||||
- **Allowed types:** `image/png`, `image/jpeg`, `image/jpg`, `image/webp`
|
||||
- **Max size:** 10MB
|
||||
- **Required:** Yes
|
||||
|
||||
- `book_name` (string) - Book name
|
||||
- **Type:** String
|
||||
- **Max length:** 200 characters
|
||||
- **Required:** Yes
|
||||
- **Validation:** Not empty
|
||||
|
||||
- `cost` (number) - Cost price
|
||||
- **Type:** Number (decimal)
|
||||
- **Required:** Yes
|
||||
- **Validation:** Must be a positive number
|
||||
|
||||
- `price` (number) - Selling price
|
||||
- **Type:** Number (decimal)
|
||||
- **Required:** Yes
|
||||
- **Validation:** Must be a positive number
|
||||
|
||||
- `quantity` (integer) - Stock quantity
|
||||
- **Type:** Integer
|
||||
- **Required:** Yes
|
||||
- **Validation:** Must be a positive integer
|
||||
|
||||
- `publisher_author` (string) - Publisher or author name
|
||||
- **Type:** String
|
||||
- **Max length:** 200 characters
|
||||
- **Required:** Yes
|
||||
- **Validation:** Not empty
|
||||
|
||||
- `category` (string) - Book category
|
||||
- **Type:** String
|
||||
- **Max length:** 100 characters
|
||||
- **Required:** Yes
|
||||
- **Validation:** Not empty
|
||||
|
||||
**Optional Parameters:**
|
||||
- `discount` (number) - Discount amount
|
||||
- **Type:** Number (decimal)
|
||||
- **Default:** 0
|
||||
- **Required:** No
|
||||
- **Validation:** Must be non-negative (>= 0)
|
||||
|
||||
- `description` (string) - Book description
|
||||
- **Type:** String
|
||||
- **Max length:** 1000 characters
|
||||
- **Required:** No
|
||||
|
||||
**Request Body Format:**
|
||||
```
|
||||
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary...
|
||||
|
||||
------WebKitFormBoundary...
|
||||
Content-Disposition: form-data; name="image"; filename="book-cover.png"
|
||||
Content-Type: image/png
|
||||
|
||||
[binary image data]
|
||||
------WebKitFormBoundary...
|
||||
Content-Disposition: form-data; name="book_name"
|
||||
|
||||
The Great Gatsby
|
||||
------WebKitFormBoundary...
|
||||
Content-Disposition: form-data; name="cost"
|
||||
|
||||
15.50
|
||||
------WebKitFormBoundary...
|
||||
Content-Disposition: form-data; name="price"
|
||||
|
||||
24.99
|
||||
------WebKitFormBoundary...
|
||||
Content-Disposition: form-data; name="discount"
|
||||
|
||||
0
|
||||
------WebKitFormBoundary...
|
||||
Content-Disposition: form-data; name="quantity"
|
||||
|
||||
100
|
||||
------WebKitFormBoundary...
|
||||
Content-Disposition: form-data; name="publisher_author"
|
||||
|
||||
F. Scott Fitzgerald
|
||||
------WebKitFormBoundary...
|
||||
Content-Disposition: form-data; name="category"
|
||||
|
||||
Fiction
|
||||
------WebKitFormBoundary...
|
||||
Content-Disposition: form-data; name="description"
|
||||
|
||||
A classic American novel
|
||||
------WebKitFormBoundary...--
|
||||
```
|
||||
|
||||
**Success Response (201 Created):**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"book_code": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"book_name": "The Great Gatsby",
|
||||
"image_url": "https://storage.googleapis.com/download/storage/v1/b/kaisa-341a6.appspot.com/o/images%2F2025%2F11%2F1732123456_abc123.png?generation=1732123456789&alt=media",
|
||||
"slug": "the-great-gatsby",
|
||||
"created_at": "2025-11-20T17:30:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response Fields:**
|
||||
- `success` (boolean): Always `true` on success
|
||||
- `data` (object): Contains the created book data
|
||||
- `book_code` (string): UUID generated for the book
|
||||
- `book_name` (string): Book name as provided
|
||||
- `image_url` (string): Public URL of uploaded image in Firebase Storage
|
||||
- `slug` (string): URL-friendly slug generated from book name
|
||||
- `created_at` (string): ISO 8601 timestamp of creation
|
||||
|
||||
**Error Responses:**
|
||||
|
||||
**400 Bad Request** - Validation error:
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "book_name is required"
|
||||
}
|
||||
```
|
||||
|
||||
**413 Payload Too Large** - File size exceeds limit:
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "File size exceeds 10MB limit"
|
||||
}
|
||||
```
|
||||
|
||||
**500 Internal Server Error** - Server error:
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "Failed to upload image"
|
||||
}
|
||||
```
|
||||
|
||||
**Common Error Messages:**
|
||||
- `"Image file is required"` - No image file provided
|
||||
- `"Invalid file type. Only PNG, JPEG, and WEBP are allowed"` - Unsupported file type
|
||||
- `"File size exceeds 10MB limit"` - File too large
|
||||
- `"book_name is required"` - Missing required field
|
||||
- `"Invalid cost value"` - Invalid number format or negative value
|
||||
- `"Failed to parse form data"` - Malformed request
|
||||
- `"Failed to upload image"` - Firebase upload error
|
||||
- `"Failed to create book"` - Database insert error
|
||||
|
||||
---
|
||||
|
||||
### 3. Upload Stationery
|
||||
|
||||
**Endpoint:** `POST /api/stationery`
|
||||
|
||||
**Description:** Upload a stationery product with image. Image is uploaded to Firebase Storage and stationery metadata is stored in PostgreSQL.
|
||||
|
||||
**Request:**
|
||||
- **Method:** `POST`
|
||||
- **Path:** `/api/stationery`
|
||||
- **Content-Type:** `multipart/form-data`
|
||||
- **Body:** FormData with the following fields:
|
||||
|
||||
**Required Parameters:**
|
||||
- `image` (File) - Stationery image file
|
||||
- **Type:** File (multipart/form-data)
|
||||
- **Allowed types:** `image/png`, `image/jpeg`, `image/jpg`, `image/webp`
|
||||
- **Max size:** 10MB
|
||||
- **Required:** Yes
|
||||
|
||||
- `stationery_name` (string) - Stationery name
|
||||
- **Type:** String
|
||||
- **Max length:** 200 characters
|
||||
- **Required:** Yes
|
||||
- **Validation:** Not empty
|
||||
|
||||
- `cost` (number) - Cost price
|
||||
- **Type:** Number (decimal)
|
||||
- **Required:** Yes
|
||||
- **Validation:** Must be a positive number
|
||||
|
||||
- `price` (number) - Selling price
|
||||
- **Type:** Number (decimal)
|
||||
- **Required:** Yes
|
||||
- **Validation:** Must be a positive number
|
||||
|
||||
- `quantity` (integer) - Stock quantity
|
||||
- **Type:** Integer
|
||||
- **Required:** Yes
|
||||
- **Validation:** Must be a positive integer
|
||||
|
||||
- `category` (string) - Stationery category
|
||||
- **Type:** String
|
||||
- **Max length:** 100 characters
|
||||
- **Required:** Yes
|
||||
- **Validation:** Not empty
|
||||
|
||||
**Optional Parameters:**
|
||||
- `discount` (number) - Discount amount
|
||||
- **Type:** Number (decimal)
|
||||
- **Default:** 0
|
||||
- **Required:** No
|
||||
- **Validation:** Must be non-negative (>= 0)
|
||||
|
||||
- `color` (string) - Product color
|
||||
- **Type:** String
|
||||
- **Required:** No
|
||||
|
||||
- `material` (string) - Product material
|
||||
- **Type:** String
|
||||
- **Required:** No
|
||||
|
||||
- `dimensions` (string) - Product dimensions
|
||||
- **Type:** String
|
||||
- **Required:** No
|
||||
|
||||
- `description` (string) - Product description
|
||||
- **Type:** String
|
||||
- **Max length:** 1000 characters
|
||||
- **Required:** No
|
||||
|
||||
**Request Body Format:**
|
||||
```
|
||||
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary...
|
||||
|
||||
------WebKitFormBoundary...
|
||||
Content-Disposition: form-data; name="image"; filename="pen.png"
|
||||
Content-Type: image/png
|
||||
|
||||
[binary image data]
|
||||
------WebKitFormBoundary...
|
||||
Content-Disposition: form-data; name="stationery_name"
|
||||
|
||||
Blue Ballpoint Pen
|
||||
------WebKitFormBoundary...
|
||||
Content-Disposition: form-data; name="cost"
|
||||
|
||||
2.50
|
||||
------WebKitFormBoundary...
|
||||
Content-Disposition: form-data; name="price"
|
||||
|
||||
5.99
|
||||
------WebKitFormBoundary...
|
||||
Content-Disposition: form-data; name="discount"
|
||||
|
||||
0
|
||||
------WebKitFormBoundary...
|
||||
Content-Disposition: form-data; name="quantity"
|
||||
|
||||
200
|
||||
------WebKitFormBoundary...
|
||||
Content-Disposition: form-data; name="category"
|
||||
|
||||
Writing
|
||||
------WebKitFormBoundary...
|
||||
Content-Disposition: form-data; name="color"
|
||||
|
||||
Blue
|
||||
------WebKitFormBoundary...
|
||||
Content-Disposition: form-data; name="material"
|
||||
|
||||
Plastic
|
||||
------WebKitFormBoundary...
|
||||
Content-Disposition: form-data; name="dimensions"
|
||||
|
||||
15cm
|
||||
------WebKitFormBoundary...
|
||||
Content-Disposition: form-data; name="description"
|
||||
|
||||
Smooth writing ballpoint pen
|
||||
------WebKitFormBoundary...--
|
||||
```
|
||||
|
||||
**Success Response (201 Created):**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"stationery_code": "550e8400-e29b-41d4-a716-446655440001",
|
||||
"stationery_name": "Blue Ballpoint Pen",
|
||||
"image_url": "https://storage.googleapis.com/download/storage/v1/b/kaisa-341a6.appspot.com/o/images%2F2025%2F11%2F1732123456_def456.png?generation=1732123456789&alt=media",
|
||||
"slug": "blue-ballpoint-pen",
|
||||
"created_at": "2025-11-20T17:30:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response Fields:**
|
||||
- `success` (boolean): Always `true` on success
|
||||
- `data` (object): Contains the created stationery data
|
||||
- `stationery_code` (string): UUID generated for the stationery item
|
||||
- `stationery_name` (string): Stationery name as provided
|
||||
- `image_url` (string): Public URL of uploaded image in Firebase Storage
|
||||
- `slug` (string): URL-friendly slug generated from stationery name
|
||||
- `created_at` (string): ISO 8601 timestamp of creation
|
||||
|
||||
**Error Responses:**
|
||||
|
||||
**400 Bad Request** - Validation error:
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "stationery_name is required"
|
||||
}
|
||||
```
|
||||
|
||||
**413 Payload Too Large** - File size exceeds limit:
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "File size exceeds 10MB limit"
|
||||
}
|
||||
```
|
||||
|
||||
**500 Internal Server Error** - Server error:
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "Failed to upload image"
|
||||
}
|
||||
```
|
||||
|
||||
**Common Error Messages:**
|
||||
- `"Image file is required"` - No image file provided
|
||||
- `"Invalid file type. Only PNG, JPEG, and WEBP are allowed"` - Unsupported file type
|
||||
- `"File size exceeds 10MB limit"` - File too large
|
||||
- `"stationery_name is required"` - Missing required field
|
||||
- `"Invalid cost value"` - Invalid number format or negative value
|
||||
- `"Failed to parse form data"` - Malformed request
|
||||
- `"Failed to upload image"` - Firebase upload error
|
||||
- `"Failed to create stationery"` - Database insert error
|
||||
|
||||
---
|
||||
|
||||
## Response Format
|
||||
|
||||
All API responses follow a consistent structure:
|
||||
|
||||
**Success Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
**Error Response:**
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "Error message"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## HTTP Status Codes
|
||||
|
||||
- **200 OK** - Health check successful
|
||||
- **201 Created** - Resource created successfully (book/stationery uploaded)
|
||||
- **400 Bad Request** - Validation error or invalid input
|
||||
- **413 Payload Too Large** - File size exceeds 10MB limit
|
||||
- **500 Internal Server Error** - Server error (database, Firebase, or internal error)
|
||||
- **503 Service Unavailable** - Service dependencies (database or Firebase) unavailable
|
||||
|
||||
---
|
||||
|
||||
## Field Validation Summary
|
||||
|
||||
### Book Upload Fields
|
||||
|
||||
| Field | Type | Required | Max Length | Validation Rules |
|
||||
|-------|------|----------|------------|------------------|
|
||||
| `image` | File | Yes | 10MB | PNG, JPEG, or WEBP only |
|
||||
| `book_name` | string | Yes | 200 | Not empty |
|
||||
| `cost` | number | Yes | - | Positive number (> 0) |
|
||||
| `price` | number | Yes | - | Positive number (> 0) |
|
||||
| `discount` | number | No | - | Non-negative (>= 0), default: 0 |
|
||||
| `quantity` | integer | Yes | - | Positive integer (> 0) |
|
||||
| `publisher_author` | string | Yes | 200 | Not empty |
|
||||
| `category` | string | Yes | 100 | Not empty |
|
||||
| `description` | string | No | 1000 | - |
|
||||
|
||||
### Stationery Upload Fields
|
||||
|
||||
| Field | Type | Required | Max Length | Validation Rules |
|
||||
|-------|------|----------|------------|------------------|
|
||||
| `image` | File | Yes | 10MB | PNG, JPEG, or WEBP only |
|
||||
| `stationery_name` | string | Yes | 200 | Not empty |
|
||||
| `cost` | number | Yes | - | Positive number (> 0) |
|
||||
| `price` | number | Yes | - | Positive number (> 0) |
|
||||
| `discount` | number | No | - | Non-negative (>= 0), default: 0 |
|
||||
| `quantity` | integer | Yes | - | Positive integer (> 0) |
|
||||
| `category` | string | Yes | 100 | Not empty |
|
||||
| `color` | string | No | - | - |
|
||||
| `material` | string | No | - | - |
|
||||
| `dimensions` | string | No | - | - |
|
||||
| `description` | string | No | 1000 | - |
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
1. **Content-Type Header**: For file uploads, use `multipart/form-data`. Do NOT manually set the `Content-Type` header - let the browser/client set it automatically with the proper boundary.
|
||||
|
||||
2. **Image Processing**: The backend expects images to be processed on the frontend (resized to 1000x1000, background removed) before upload. Upload the processed PNG image.
|
||||
|
||||
3. **Auto-Generated Fields**: The following fields are automatically generated by the backend:
|
||||
- `book_code` / `stationery_code`: UUID v4
|
||||
- `slug`: Generated from product name (lowercase, spaces to hyphens, special characters removed)
|
||||
- `created_at`: Current timestamp
|
||||
- `updated_at`: Current timestamp
|
||||
|
||||
4. **File Upload**: Only one image file per request. The file field name must be `image`.
|
||||
|
||||
5. **Error Messages**: Always check the `error` field in error responses for user-friendly error messages.
|
||||
|
||||
6. **CORS**: The backend is configured to accept requests from the frontend URL specified in `FRONTEND_URL` environment variable (default: `http://localhost:5173`).
|
||||
Reference in New Issue
Block a user