Skip to content

Commit 2a0ff3d

Browse files
committed
fix: refactored count query builder
1 parent 40d952a commit 2a0ff3d

File tree

9 files changed

+261
-42
lines changed

9 files changed

+261
-42
lines changed

app/middlewares/Cors.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
Message
1515
};
1616

17-
class cors extends Middleware
17+
class Cors extends Middleware
1818
{
1919

2020
protected string $allowOrigin = '*';
@@ -57,7 +57,11 @@ public function getAccessControlMaxAge(): string
5757
return "{$this->accessControlMaxAge}";
5858
}
5959

60-
public function invoke(Request $request, Message $response, Headers $headers): array
60+
public function invoke(
61+
Request $request,
62+
Message $response,
63+
Headers $headers
64+
): array
6165
{
6266
$headers->set('Access-Control-Max-Age', $this->getAccessControlMaxAge());
6367
$headers->set('Access-Control-Allow-Origin', $this->getAllowOrigin());

app/middlewares/Jwt.php

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@
2222

2323
use Haku\Jwt\Token;
2424

25-
class jwt extends Middleware
25+
use function Haku\Jwt\currentToken;
26+
27+
class Jwt extends Middleware
2628
{
2729

2830
public array $requestHeaders = [];
@@ -32,7 +34,10 @@ public function __construct()
3234
$this->requestHeaders = \getallheaders();
3335
}
3436

35-
private function transformResponseMessage(Headers $headers, array $message): array
37+
private function transformResponseMessage(
38+
Headers $headers,
39+
array $message
40+
): array
3641
{
3742
$contentType = $headers->get('Content-Type');
3843

@@ -73,11 +78,18 @@ private function validAuthorizationToken(): bool
7378
return currentToken() !== null;
7479
}
7580

76-
public function invoke(Request $request, Message $response, Headers $headers): array
81+
public function invoke(
82+
Request $request,
83+
Message $response,
84+
Headers $headers
85+
): array
7786
{
7887
if (!$this->validAuthorizationHeader())
7988
{
80-
[$response, $headers] = $this->transformToErrorResponse($headers, Status::Unauthorized);
89+
[$response, $headers] = $this->transformToErrorResponse(
90+
$headers,
91+
Status::Unauthorized
92+
);
8193
}
8294

8395
if ($this->validAuthorizationToken())
@@ -86,15 +98,21 @@ public function invoke(Request $request, Message $response, Headers $headers): a
8698

8799
if ($token === null)
88100
{
89-
[$response, $headers] = $this->transformToErrorResponse($headers, Status::Unauthorized);
101+
[$response, $headers] = $this->transformToErrorResponse(
102+
$headers,
103+
Status::Unauthorized
104+
);
90105
}
91106
else
92107
{
93108
if ($token->hasExpired())
94109
{
95-
[$response, $headers] = $this->transformResponseMessage($headers, [
96-
'error' => 'Authorization token has expired'
97-
]);
110+
[$response, $headers] = $this->transformResponseMessage(
111+
$headers,
112+
[
113+
'error' => 'Authorization token has expired'
114+
]
115+
);
98116

99117
$headers->status(Status::Unauthorized);
100118
}

vendor/Haku/Database/Model.php

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ public static function findAll(
7676
offset: $offset,
7777
);
7878

79-
return $db->fetchAll($query, $parameters);
79+
return $db->fetchAll($query, $parameters) ?? [];
8080
}
8181

8282
public static function findOne(
@@ -126,6 +126,7 @@ public static function paginate(
126126
int $page = 1,
127127
int $limit = Model::DefaultFetchLimit,
128128
bool $includeDeleted = false,
129+
?string $countFieldName = null
129130
): ?array
130131
{
131132
$self = new static();
@@ -137,22 +138,27 @@ public static function paginate(
137138
$where[] = Where::null('deletedAt');
138139
}
139140

141+
if (!$countFieldName)
142+
{
143+
$countFieldName = $self->primaryKeyName;
144+
}
145+
140146
[$countQuery, $countParams] = Find::count(
141147
tableName: $self->tableName,
142-
countFieldName: $self->primaryKeyName,
148+
countFieldName: $countFieldName,
143149
aggregateFields: $self->getAggregateFields(),
144150
where: $where
145151
);
146152

147153
$numRecords = $db->fetchColumn($countQuery, $countParams);
148-
$pageCount = ceil($numRecords / $limit);
154+
$pageCount = (int) ceil($numRecords / $limit);
149155

150156
$prevPage = $page - 1;
151157
$nextPage = $page + 1;
152158

153159
$offset = $prevPage * $limit;
154160

155-
if ($prevPage <= 1)
161+
if ($prevPage <= 0)
156162
{
157163
$prevPage = null;
158164
}
@@ -177,7 +183,7 @@ public static function paginate(
177183
offset: $offset,
178184
);
179185

180-
$records = $db->fetchAll($query, $parameters);
186+
$records = $db->fetchAll($query, $parameters) ?? [];
181187
$records = array_map(fn($record) => static::from($record)->json(), $records);
182188

183189
return [
@@ -188,7 +194,9 @@ public static function paginate(
188194
'nextPage' => $nextPage,
189195
],
190196
'meta' => [
191-
'numRecords' => $numRecords,
197+
'numRecordsTotal' => $numRecords,
198+
'numRecordsPerPage' => $limit,
199+
'numRecordsOnCurrentPage' => count($records),
192200
],
193201
'records' => $records,
194202
];
@@ -304,6 +312,8 @@ public function save(
304312
bool $unmarshalBeforeSave = false,
305313
): ?static
306314
{
315+
$db = haku('db');
316+
307317
if ($ignoreValidationStatus === false && $this->isValid === false)
308318
{
309319
throw new ModelException(
@@ -318,8 +328,6 @@ public function save(
318328
);
319329
}
320330

321-
$db = haku('db');
322-
323331
$record = $this->getRecord(filterTimestamps: true);
324332

325333
if ($unmarshalBeforeSave)

vendor/Haku/Database/Query/Conditions.php

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,42 @@
1111
Filter,
1212
};
1313

14+
use Haku\Database\Query\OrWhere;
15+
1416
class Conditions
1517
{
1618

17-
public static function fromQueryString(?string $unresolved = ''): array
19+
public static function fromQueryString(
20+
?string $unresolved = '',
21+
string $searchParameterName = 'search',
22+
array $searchableFields = [],
23+
): array
1824
{
19-
$params = new Params();
25+
$params = new Params($unresolved);
2026

2127
if (!$params->has('filters'))
2228
{
2329
return [];
2430
}
2531

26-
$filter = Filter::from($params->get('filters'));
32+
$filter = Filter::from(
33+
json_decode($params->get('filters'), true)
34+
);
35+
36+
$where = static::from($filter);
2737

28-
return static::from($filter);
38+
if (count($searchableFields) > 0 && $params->has($searchParameterName))
39+
{
40+
foreach ($searchableFields as $searchable)
41+
{
42+
$where[] = OrWhere::like(
43+
$searchable,
44+
$params->get($searchParameterName)
45+
);
46+
}
47+
}
48+
49+
return $where;
2950
}
3051

3152
public static function from(Filter $filter): array

vendor/Haku/Database/Query/Find.php

Lines changed: 113 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,12 @@ public static function all(
8787
// Add ORDER BY
8888
if (count($orderBy) > 0)
8989
{
90+
$orderBy = normalizeOrderByClauses(
91+
tableName: $tableName,
92+
aggregateFields: $aggregateFields,
93+
orderBy: $orderBy,
94+
);
95+
9096
$querySegments = array_merge(
9197
$querySegments,
9298
[
@@ -127,32 +133,115 @@ public static function one(
127133
}
128134

129135
/**
130-
* Creates a count query.
136+
* Creates a count query by first building a sub query with all filtering applied and counts on that result.
131137
*/
132138
public static function count(
133139
string $tableName,
134-
string $countFieldName = '*',
140+
string $countFieldName = 'id',
135141
array $aggregateFields = [],
136142
array $where = [],
137143
): array
138144
{
139-
$fieldName = normalizeField($tableName, $countFieldName);
145+
if (count($aggregateFields) > 0)
146+
{
147+
return self::complexCount(
148+
tableName: $tableName,
149+
countFieldName: $countFieldName,
150+
aggregateFields: $aggregateFields,
151+
where: $where,
152+
);
153+
}
154+
else
155+
{
156+
return self::simpleCount(
157+
tableName: $tableName,
158+
countFieldName: $countFieldName,
159+
where: $where,
160+
);
161+
}
162+
}
163+
164+
/**
165+
* Creates a "simple" count query without aggregates.
166+
*/
167+
protected static function simpleCount(
168+
string $tableName,
169+
string $countFieldName = 'id',
170+
array $where = [],
171+
): array
172+
{
173+
// Get normalized WHERE and HAVING
174+
$conditions = (object) normalizeConditions(
175+
$tableName,
176+
$where,
177+
);
140178

141179
$querySegments = [
142180
'SELECT',
143-
"COUNT({$fieldName})",
181+
sprintf('COUNT(DISTINCT %s)', normalizeField($tableName, $countFieldName)),
144182
'FROM',
145183
$tableName,
146184
];
147185

186+
$parameters = $conditions->where->parameters;
187+
188+
// Add WHERE
189+
if (count($conditions->where->clauses) > 0)
190+
{
191+
$querySegments = array_merge(
192+
$querySegments,
193+
['WHERE'],
194+
$conditions->where->clauses
195+
);
196+
}
197+
198+
$query = implode(' ', $querySegments);
199+
200+
return [
201+
$query,
202+
$parameters
203+
];
204+
}
205+
206+
/**
207+
* Creates a "complex" count query that allows for aggregated filtering.
208+
*/
209+
protected static function complexCount(
210+
string $tableName,
211+
string $countFieldName = 'id',
212+
array $aggregateFields = [],
213+
array $where = [],
214+
): array
215+
{
148216
// Get normalized WHERE and HAVING
149217
$conditions = (object) normalizeConditions(
150218
$tableName,
151219
$where,
152220
$aggregateFields
153221
);
154222

155-
$parameters = $conditions->where->parameters;
223+
$normalizedFields = [];
224+
225+
// Normalize aggregate fields if any
226+
if (count($aggregateFields) > 0)
227+
{
228+
foreach ($aggregateFields as $field => $aggregate)
229+
{
230+
$normalizedFields[] = sprintf('%2$s AS %1$s', $field, $aggregate);
231+
}
232+
}
233+
234+
$querySegments = [
235+
'SELECT',
236+
implode(', ', $normalizedFields),
237+
'FROM',
238+
$tableName,
239+
];
240+
241+
$parameters = [
242+
...$conditions->where->parameters,
243+
...$conditions->having->parameters
244+
];
156245

157246
// Add WHERE
158247
if (count($conditions->where->clauses) > 0)
@@ -164,10 +253,27 @@ public static function count(
164253
);
165254
}
166255

167-
// @todo Add GROUP BY
256+
// Add HAVING
257+
if (count($conditions->having->clauses) > 0)
258+
{
259+
$querySegments = array_merge(
260+
$querySegments,
261+
['HAVING'],
262+
$conditions->having->clauses
263+
);
264+
}
265+
266+
$innerQuery = implode(' ', $querySegments);
267+
268+
$query = sprintf(
269+
'SELECT COUNT(DISTINCT %s) AS count FROM %s AS o, (%s) AS i',
270+
$countFieldName,
271+
$tableName,
272+
$innerQuery
273+
);
168274

169275
return [
170-
implode(' ', $querySegments),
276+
$query,
171277
$parameters
172278
];
173279
}

0 commit comments

Comments
 (0)