Skip to content

Commit c06596b

Browse files
CopilotBitsHost
andcommitted
Add bulk operations and Response helper class
Co-authored-by: BitsHost <23263143+BitsHost@users.noreply.github.com>
1 parent 783e672 commit c06596b

File tree

5 files changed

+289
-12
lines changed

5 files changed

+289
-12
lines changed

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
11
# Changelog
22

3-
## 1.1.0 - Enhanced Query Capabilities
3+
## 1.1.0 - Enhanced Query Capabilities and Bulk Operations
44

55
### New Features
66
- **Advanced Filter Operators**: Support for comparison operators (eq, neq, gt, gte, lt, lte, like, in, notin, null, notnull)
77
- **Field Selection**: Select specific fields in list queries using the `fields` parameter
8+
- **Bulk Operations**:
9+
- `bulk_create` - Create multiple records in a single transaction
10+
- `bulk_delete` - Delete multiple records by IDs in a single query
811
- **Input Validation**: Added comprehensive input validation for table names, column names, IDs, and query parameters
12+
- **Response Helper**: Added Response class for standardized API responses (for future use)
913
- **Backward Compatibility**: Old filter format (`col:value`) still works alongside new format (`col:op:value`)
1014

1115
### Improvements
1216
- Fixed SQL injection vulnerability in filter parameter by using parameterized queries with unique parameter names
1317
- Added Validator class for centralized input validation and sanitization
1418
- Improved error messages with proper HTTP status codes
1519
- Enhanced documentation with detailed examples of new features
20+
- Transaction support for bulk create operations
1621

1722
### Filter Operators
1823
- `eq` - Equals
@@ -31,6 +36,8 @@
3136
- Field selection: `/index.php?action=list&table=users&fields=id,name,email`
3237
- Advanced filtering: `/index.php?action=list&table=users&filter=age:gt:18,status:eq:active`
3338
- IN operator: `/index.php?action=list&table=orders&filter=status:in:pending|processing|shipped`
39+
- Bulk create: `POST /index.php?action=bulk_create&table=users` with JSON array
40+
- Bulk delete: `POST /index.php?action=bulk_delete&table=users` with `{"ids":[1,2,3]}`
3441

3542
## 1.0.0
3643

README.md

Lines changed: 88 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ OpenAPI (Swagger) docs, and zero code generation.
1010

1111
- Auto-discovers tables and columns
1212
- Full CRUD endpoints for any table
13+
- **Bulk operations** - Create or delete multiple records efficiently
1314
- Configurable authentication (API Key, Basic Auth, JWT, or none)
1415
- **Advanced query features:**
1516
- **Field selection** - Choose specific columns to return
@@ -92,28 +93,104 @@ return [
9293

9394
All requests go through `public/index.php` with `action` parameter.
9495

95-
| Action | Method | Usage Example |
96-
|-----------|--------|------------------------------------------------------------|
97-
| tables | GET | `/index.php?action=tables` |
98-
| columns | GET | `/index.php?action=columns&table=users` |
99-
| list | GET | `/index.php?action=list&table=users` |
100-
| read | GET | `/index.php?action=read&table=users&id=1` |
101-
| create | POST | `/index.php?action=create&table=users` (form POST) |
102-
| update | POST | `/index.php?action=update&table=users&id=1` (form POST) |
103-
| delete | POST | `/index.php?action=delete&table=users&id=1` |
104-
| openapi | GET | `/index.php?action=openapi` |
105-
| login | POST | `/index.php?action=login` (JWT only) |
96+
| Action | Method | Usage Example |
97+
|--------------|--------|-------------------------------------------------------------|
98+
| tables | GET | `/index.php?action=tables` |
99+
| columns | GET | `/index.php?action=columns&table=users` |
100+
| list | GET | `/index.php?action=list&table=users` |
101+
| read | GET | `/index.php?action=read&table=users&id=1` |
102+
| create | POST | `/index.php?action=create&table=users` (form POST or JSON) |
103+
| update | POST | `/index.php?action=update&table=users&id=1` (form POST or JSON) |
104+
| delete | POST | `/index.php?action=delete&table=users&id=1` |
105+
| bulk_create | POST | `/index.php?action=bulk_create&table=users` (JSON array) |
106+
| bulk_delete | POST | `/index.php?action=bulk_delete&table=users` (JSON with ids) |
107+
| openapi | GET | `/index.php?action=openapi` |
108+
| login | POST | `/index.php?action=login` (JWT only) |
106109

107110
---
108111

109112
## 🤖 Example `curl` Commands
110113

111114
```sh
115+
# List tables
112116
curl http://localhost/index.php?action=tables
117+
118+
# List users with API key
113119
curl -H "X-API-Key: changeme123" "http://localhost/index.php?action=list&table=users"
120+
121+
# JWT login
114122
curl -X POST -d "username=admin&password=secret" http://localhost/index.php?action=login
123+
124+
# List with JWT token
115125
curl -H "Authorization: Bearer <token>" "http://localhost/index.php?action=list&table=users"
126+
127+
# Basic auth
116128
curl -u admin:secret "http://localhost/index.php?action=list&table=users"
129+
130+
# Bulk create
131+
curl -X POST -H "Content-Type: application/json" \
132+
-d '[{"name":"Alice","email":"alice@example.com"},{"name":"Bob","email":"bob@example.com"}]' \
133+
"http://localhost/index.php?action=bulk_create&table=users"
134+
135+
# Bulk delete
136+
curl -X POST -H "Content-Type: application/json" \
137+
-d '{"ids":[1,2,3]}' \
138+
"http://localhost/index.php?action=bulk_delete&table=users"
139+
```
140+
141+
---
142+
143+
### 💪 Bulk Operations
144+
145+
The API supports bulk operations for efficient handling of multiple records:
146+
147+
#### Bulk Create
148+
149+
Create multiple records in a single transaction. If any record fails, the entire operation is rolled back.
150+
151+
**Endpoint:** `POST /index.php?action=bulk_create&table=users`
152+
153+
**Request Body (JSON array):**
154+
```json
155+
[
156+
{"name": "Alice", "email": "alice@example.com", "age": 25},
157+
{"name": "Bob", "email": "bob@example.com", "age": 30},
158+
{"name": "Charlie", "email": "charlie@example.com", "age": 35}
159+
]
160+
```
161+
162+
**Response:**
163+
```json
164+
{
165+
"success": true,
166+
"created": 3,
167+
"data": [
168+
{"id": 1, "name": "Alice", "email": "alice@example.com", "age": 25},
169+
{"id": 2, "name": "Bob", "email": "bob@example.com", "age": 30},
170+
{"id": 3, "name": "Charlie", "email": "charlie@example.com", "age": 35}
171+
]
172+
}
173+
```
174+
175+
#### Bulk Delete
176+
177+
Delete multiple records by their IDs in a single query.
178+
179+
**Endpoint:** `POST /index.php?action=bulk_delete&table=users`
180+
181+
**Request Body (JSON):**
182+
```json
183+
{
184+
"ids": [1, 2, 3, 4, 5]
185+
}
186+
```
187+
188+
**Response:**
189+
```json
190+
{
191+
"success": true,
192+
"deleted": 5
193+
}
117194
```
118195

119196
---

src/ApiGenerator.php

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,4 +261,60 @@ public function delete(string $table, $id): array
261261
}
262262
return ['success' => true];
263263
}
264+
265+
/**
266+
* Bulk create multiple records
267+
*/
268+
public function bulkCreate(string $table, array $records): array
269+
{
270+
if (empty($records)) {
271+
return ['error' => 'No records provided for bulk create'];
272+
}
273+
274+
$this->pdo->beginTransaction();
275+
try {
276+
$created = [];
277+
foreach ($records as $data) {
278+
$created[] = $this->create($table, $data);
279+
}
280+
$this->pdo->commit();
281+
return [
282+
'success' => true,
283+
'created' => count($created),
284+
'data' => $created
285+
];
286+
} catch (\Exception $e) {
287+
$this->pdo->rollBack();
288+
return ['error' => 'Bulk create failed: ' . $e->getMessage()];
289+
}
290+
}
291+
292+
/**
293+
* Bulk delete multiple records by IDs
294+
*/
295+
public function bulkDelete(string $table, array $ids): array
296+
{
297+
if (empty($ids)) {
298+
return ['error' => 'No IDs provided for bulk delete'];
299+
}
300+
301+
$pk = $this->inspector->getPrimaryKey($table);
302+
$placeholders = [];
303+
$params = [];
304+
305+
foreach ($ids as $i => $id) {
306+
$key = "id_$i";
307+
$placeholders[] = ":$key";
308+
$params[$key] = $id;
309+
}
310+
311+
$sql = "DELETE FROM `$table` WHERE `$pk` IN (" . implode(',', $placeholders) . ")";
312+
$stmt = $this->pdo->prepare($sql);
313+
$stmt->execute($params);
314+
315+
return [
316+
'success' => true,
317+
'deleted' => $stmt->rowCount()
318+
];
319+
}
264320
}

src/Response.php

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<?php
2+
3+
namespace App;
4+
5+
class Response
6+
{
7+
/**
8+
* Send a success response
9+
*/
10+
public static function success($data, int $statusCode = 200): void
11+
{
12+
http_response_code($statusCode);
13+
header('Content-Type: application/json');
14+
echo json_encode($data);
15+
}
16+
17+
/**
18+
* Send an error response
19+
*/
20+
public static function error(string $message, int $statusCode = 400, array $details = []): void
21+
{
22+
http_response_code($statusCode);
23+
header('Content-Type: application/json');
24+
$response = ['error' => $message];
25+
if (!empty($details)) {
26+
$response['details'] = $details;
27+
}
28+
echo json_encode($response);
29+
}
30+
31+
/**
32+
* Send a created response (201)
33+
*/
34+
public static function created($data): void
35+
{
36+
self::success($data, 201);
37+
}
38+
39+
/**
40+
* Send a no content response (204)
41+
*/
42+
public static function noContent(): void
43+
{
44+
http_response_code(204);
45+
header('Content-Type: application/json');
46+
}
47+
48+
/**
49+
* Send a not found response (404)
50+
*/
51+
public static function notFound(string $message = 'Resource not found'): void
52+
{
53+
self::error($message, 404);
54+
}
55+
56+
/**
57+
* Send an unauthorized response (401)
58+
*/
59+
public static function unauthorized(string $message = 'Unauthorized'): void
60+
{
61+
self::error($message, 401);
62+
}
63+
64+
/**
65+
* Send a forbidden response (403)
66+
*/
67+
public static function forbidden(string $message = 'Forbidden'): void
68+
{
69+
self::error($message, 403);
70+
}
71+
72+
/**
73+
* Send a method not allowed response (405)
74+
*/
75+
public static function methodNotAllowed(string $message = 'Method Not Allowed'): void
76+
{
77+
self::error($message, 405);
78+
}
79+
80+
/**
81+
* Send a server error response (500)
82+
*/
83+
public static function serverError(string $message = 'Internal Server Error'): void
84+
{
85+
self::error($message, 500);
86+
}
87+
88+
/**
89+
* Send a validation error response (422)
90+
*/
91+
public static function validationError(string $message, array $errors = []): void
92+
{
93+
self::error($message, 422, $errors);
94+
}
95+
}

src/Router.php

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,48 @@ public function route(array $query)
207207
}
208208
break;
209209

210+
case 'bulk_create':
211+
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
212+
http_response_code(405);
213+
echo json_encode(['error' => 'Method Not Allowed']);
214+
break;
215+
}
216+
if (!isset($query['table']) || !Validator::validateTableName($query['table'])) {
217+
http_response_code(400);
218+
echo json_encode(['error' => 'Invalid or missing table parameter']);
219+
break;
220+
}
221+
$this->enforceRbac('create', $query['table']);
222+
$data = json_decode(file_get_contents('php://input'), true) ?? [];
223+
if (!is_array($data) || empty($data)) {
224+
http_response_code(400);
225+
echo json_encode(['error' => 'Invalid or empty JSON array']);
226+
break;
227+
}
228+
echo json_encode($this->api->bulkCreate($query['table'], $data));
229+
break;
230+
231+
case 'bulk_delete':
232+
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
233+
http_response_code(405);
234+
echo json_encode(['error' => 'Method Not Allowed']);
235+
break;
236+
}
237+
if (!isset($query['table']) || !Validator::validateTableName($query['table'])) {
238+
http_response_code(400);
239+
echo json_encode(['error' => 'Invalid or missing table parameter']);
240+
break;
241+
}
242+
$this->enforceRbac('delete', $query['table']);
243+
$data = json_decode(file_get_contents('php://input'), true) ?? [];
244+
if (!isset($data['ids']) || !is_array($data['ids']) || empty($data['ids'])) {
245+
http_response_code(400);
246+
echo json_encode(['error' => 'Invalid or empty ids array. Send JSON with "ids" field.']);
247+
break;
248+
}
249+
echo json_encode($this->api->bulkDelete($query['table'], $data['ids']));
250+
break;
251+
210252
case 'openapi':
211253
// No per-table RBAC needed by default
212254
echo json_encode(OpenApiGenerator::generate(

0 commit comments

Comments
 (0)