Skip to content

Commit d649a00

Browse files
CopilotBitsHost
andcommitted
Add count endpoint for analytics and record counting
Co-authored-by: BitsHost <23263143+BitsHost@users.noreply.github.com>
1 parent c06596b commit d649a00

File tree

5 files changed

+192
-0
lines changed

5 files changed

+192
-0
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
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+
- **Count Endpoint**: New `count` action to get record counts with optional filtering (no pagination overhead)
89
- **Bulk Operations**:
910
- `bulk_create` - Create multiple records in a single transaction
1011
- `bulk_delete` - Delete multiple records by IDs in a single query
@@ -36,6 +37,7 @@
3637
- Field selection: `/index.php?action=list&table=users&fields=id,name,email`
3738
- Advanced filtering: `/index.php?action=list&table=users&filter=age:gt:18,status:eq:active`
3839
- IN operator: `/index.php?action=list&table=orders&filter=status:in:pending|processing|shipped`
40+
- Count records: `/index.php?action=count&table=users&filter=status:eq:active`
3941
- Bulk create: `POST /index.php?action=bulk_create&table=users` with JSON array
4042
- Bulk delete: `POST /index.php?action=bulk_delete&table=users` with `{"ids":[1,2,3]}`
4143

README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ All requests go through `public/index.php` with `action` parameter.
9898
| tables | GET | `/index.php?action=tables` |
9999
| columns | GET | `/index.php?action=columns&table=users` |
100100
| list | GET | `/index.php?action=list&table=users` |
101+
| count | GET | `/index.php?action=count&table=users` |
101102
| read | GET | `/index.php?action=read&table=users&id=1` |
102103
| create | POST | `/index.php?action=create&table=users` (form POST or JSON) |
103104
| update | POST | `/index.php?action=update&table=users&id=1` (form POST or JSON) |
@@ -195,6 +196,40 @@ Delete multiple records by their IDs in a single query.
195196

196197
---
197198

199+
### 📊 Count Records
200+
201+
Get the total count of records in a table with optional filtering. This is useful for analytics and doesn't include pagination overhead.
202+
203+
**Endpoint:** `GET /index.php?action=count&table=users`
204+
205+
**Query Parameters:**
206+
- `filter` - (Optional) Same filter syntax as the list endpoint
207+
208+
**Examples:**
209+
210+
```sh
211+
# Count all users
212+
curl "http://localhost/index.php?action=count&table=users"
213+
214+
# Count active users
215+
curl "http://localhost/index.php?action=count&table=users&filter=status:eq:active"
216+
217+
# Count users over 18
218+
curl "http://localhost/index.php?action=count&table=users&filter=age:gt:18"
219+
220+
# Count with multiple filters
221+
curl "http://localhost/index.php?action=count&table=users&filter=status:eq:active,age:gte:18"
222+
```
223+
224+
**Response:**
225+
```json
226+
{
227+
"count": 42
228+
}
229+
```
230+
231+
---
232+
198233

199234
### 🔄 Advanced Query Features (Filtering, Sorting, Pagination, Field Selection)
200235

src/ApiGenerator.php

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,4 +317,119 @@ public function bulkDelete(string $table, array $ids): array
317317
'deleted' => $stmt->rowCount()
318318
];
319319
}
320+
321+
/**
322+
* Count records with optional filtering
323+
*/
324+
public function count(string $table, array $opts = []): array
325+
{
326+
$columns = $this->inspector->getColumns($table);
327+
$colNames = array_column($columns, 'Field');
328+
329+
// --- Filtering (same as list method) ---
330+
$where = [];
331+
$params = [];
332+
$paramCounter = 0;
333+
if (!empty($opts['filter'])) {
334+
$filters = explode(',', $opts['filter']);
335+
foreach ($filters as $f) {
336+
$parts = explode(':', $f, 3);
337+
if (count($parts) === 2) {
338+
$col = $parts[0];
339+
$val = $parts[1];
340+
if (in_array($col, $colNames, true)) {
341+
if (str_contains($val, '%')) {
342+
$paramKey = "{$col}_{$paramCounter}";
343+
$where[] = "`$col` LIKE :$paramKey";
344+
$params[$paramKey] = $val;
345+
$paramCounter++;
346+
} else {
347+
$paramKey = "{$col}_{$paramCounter}";
348+
$where[] = "`$col` = :$paramKey";
349+
$params[$paramKey] = $val;
350+
$paramCounter++;
351+
}
352+
}
353+
} elseif (count($parts) === 3 && in_array($parts[0], $colNames, true)) {
354+
$col = $parts[0];
355+
$operator = strtolower($parts[1]);
356+
$val = $parts[2];
357+
$paramKey = "{$col}_{$paramCounter}";
358+
359+
switch ($operator) {
360+
case 'eq':
361+
$where[] = "`$col` = :$paramKey";
362+
$params[$paramKey] = $val;
363+
break;
364+
case 'neq':
365+
case 'ne':
366+
$where[] = "`$col` != :$paramKey";
367+
$params[$paramKey] = $val;
368+
break;
369+
case 'gt':
370+
$where[] = "`$col` > :$paramKey";
371+
$params[$paramKey] = $val;
372+
break;
373+
case 'gte':
374+
case 'ge':
375+
$where[] = "`$col` >= :$paramKey";
376+
$params[$paramKey] = $val;
377+
break;
378+
case 'lt':
379+
$where[] = "`$col` < :$paramKey";
380+
$params[$paramKey] = $val;
381+
break;
382+
case 'lte':
383+
case 'le':
384+
$where[] = "`$col` <= :$paramKey";
385+
$params[$paramKey] = $val;
386+
break;
387+
case 'like':
388+
$where[] = "`$col` LIKE :$paramKey";
389+
$params[$paramKey] = $val;
390+
break;
391+
case 'in':
392+
$values = explode('|', $val);
393+
$placeholders = [];
394+
foreach ($values as $i => $v) {
395+
$inParamKey = "{$paramKey}_in_{$i}";
396+
$placeholders[] = ":$inParamKey";
397+
$params[$inParamKey] = $v;
398+
}
399+
$where[] = "`$col` IN (" . implode(',', $placeholders) . ")";
400+
break;
401+
case 'notin':
402+
case 'nin':
403+
$values = explode('|', $val);
404+
$placeholders = [];
405+
foreach ($values as $i => $v) {
406+
$inParamKey = "{$paramKey}_nin_{$i}";
407+
$placeholders[] = ":$inParamKey";
408+
$params[$inParamKey] = $v;
409+
}
410+
$where[] = "`$col` NOT IN (" . implode(',', $placeholders) . ")";
411+
break;
412+
case 'null':
413+
$where[] = "`$col` IS NULL";
414+
break;
415+
case 'notnull':
416+
$where[] = "`$col` IS NOT NULL";
417+
break;
418+
}
419+
$paramCounter++;
420+
}
421+
}
422+
}
423+
424+
$sql = "SELECT COUNT(*) FROM `$table`";
425+
if ($where) {
426+
$sql .= ' WHERE ' . implode(' AND ', $where);
427+
}
428+
429+
$stmt = $this->pdo->prepare($sql);
430+
$stmt->execute($params);
431+
$count = (int)$stmt->fetchColumn();
432+
433+
return ['count' => $count];
434+
}
320435
}

src/Router.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,24 @@ public function route(array $query)
124124
}
125125
break;
126126

127+
case 'count':
128+
if (isset($query['table'])) {
129+
if (!Validator::validateTableName($query['table'])) {
130+
http_response_code(400);
131+
echo json_encode(['error' => 'Invalid table name']);
132+
break;
133+
}
134+
$this->enforceRbac('list', $query['table']); // Use 'list' permission for count
135+
$opts = [
136+
'filter' => $query['filter'] ?? null,
137+
];
138+
echo json_encode($this->api->count($query['table'], $opts));
139+
} else {
140+
http_response_code(400);
141+
echo json_encode(['error' => 'Missing table parameter']);
142+
}
143+
break;
144+
127145
case 'read':
128146
if (isset($query['table'], $query['id'])) {
129147
if (!Validator::validateTableName($query['table'])) {

tests/AdvancedFilterTest.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,4 +154,26 @@ public function testBackwardCompatibility()
154154
$this->assertEquals(1, count($result['data']));
155155
$this->assertEquals('Alice', $result['data'][0]['name']);
156156
}
157+
158+
public function testCount()
159+
{
160+
$result = $this->api->count($this->table);
161+
$this->assertIsArray($result);
162+
$this->assertArrayHasKey('count', $result);
163+
$this->assertEquals(5, $result['count']); // We inserted 5 records
164+
}
165+
166+
public function testCountWithFilter()
167+
{
168+
$result = $this->api->count($this->table, ['filter' => 'status:eq:active']);
169+
$this->assertIsArray($result);
170+
$this->assertEquals(3, $result['count']); // Alice, Bob, Eve are active
171+
}
172+
173+
public function testCountWithMultipleFilters()
174+
{
175+
$result = $this->api->count($this->table, ['filter' => 'age:gte:25,status:eq:active']);
176+
$this->assertIsArray($result);
177+
$this->assertGreaterThanOrEqual(2, $result['count']); // At least Alice (25) and Bob (30)
178+
}
157179
}

0 commit comments

Comments
 (0)