Fix request filter for nested relationships

This commit is contained in:
Jake Barnby 2025-08-27 23:59:40 +12:00
parent 8a76979ba8
commit 3fc53b2c43
No known key found for this signature in database
GPG key ID: C437A8CC85B96E9C
2 changed files with 60 additions and 38 deletions

12
composer.lock generated
View file

@ -3557,16 +3557,16 @@
},
{
"name": "utopia-php/database",
"version": "1.2.1",
"version": "1.2.3",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/database.git",
"reference": "99beaf1dd6dc3561c8332f9893325777553644a4"
"reference": "8a536fead840d9da6ee819fe6b80e0f047997f69"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/database/zipball/99beaf1dd6dc3561c8332f9893325777553644a4",
"reference": "99beaf1dd6dc3561c8332f9893325777553644a4",
"url": "https://api.github.com/repos/utopia-php/database/zipball/8a536fead840d9da6ee819fe6b80e0f047997f69",
"reference": "8a536fead840d9da6ee819fe6b80e0f047997f69",
"shasum": ""
},
"require": {
@ -3607,9 +3607,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/database/issues",
"source": "https://github.com/utopia-php/database/tree/1.2.1"
"source": "https://github.com/utopia-php/database/tree/1.2.3"
},
"time": "2025-08-26T16:05:26+00:00"
"time": "2025-08-27T11:47:04+00:00"
},
{
"name": "utopia-php/detector",

View file

@ -32,59 +32,67 @@ class V20 extends Filter
*/
protected function manageSelectQueries(array $content): array
{
$hasWildcard = false;
if (!isset($content['queries'])) {
$hasWildcard = true;
// only query, make it json encoded!
$content['queries'] = [Query::select(['*'])->toString()];
$content['queries'] = [];
}
// Handle case where queries is an array but empty
if (\is_array($content['queries'])) {
$content['queries'] = \array_filter($content['queries'], function($q) {
if (\is_object($q) && empty((array)$q)) {
return false;
}
if (\is_string($q) && \trim($q) === '') {
return false;
}
if (empty($q)) {
return false;
}
return true;
});
}
try {
$parsed = Query::parseQueries($content['queries']);
} catch (QueryException) {
// don't crash!
return $content;
}
$selections = Query::groupByType($parsed)['selections'] ?? [];
// If there are no select queries at all, add wildcard
if (empty($selections)) {
$hasWildcard = true;
$parsed[] = Query::select(['*']);
} elseif (!$hasWildcard) {
// check if any select includes a wildcard as we added one above
// Check if we need to add wildcard + relationships
// This happens when:
// 1. No select queries exist, OR
// 2. A wildcard select exists
$needsRelationships = empty($selections);
if (!$needsRelationships) {
foreach ($selections as $select) {
if (\in_array('*', $select->getValues(), true)) {
$hasWildcard = true;
$needsRelationships = true;
break;
}
}
}
/**
* Add `keys.*` for all model types!
* Add wildcard and relationship selects for backward compatibility
*/
if ($hasWildcard) {
if ($needsRelationships) {
$relatedKeys = $this->getRelatedCollectionKeys();
$selects = \array_values(\array_unique(\array_merge(['*'], $relatedKeys)));
if (! empty($relatedKeys)) {
$selects = \array_values(\array_unique(\array_merge(['*'], $relatedKeys)));
// Remove any existing select queries
$parsed = \array_filter(
$parsed,
fn ($query) => $query->getMethod() !== Query::TYPE_SELECT
);
// remove previous select queries
$parsed = \array_filter(
$parsed,
fn ($query) => $query->getMethod() !== Query::TYPE_SELECT
);
// add wildcard + relationship(s) selects
$parsed[] = Query::select($selects);
}
// Add wildcard + relationship(s) selects
$parsed[] = Query::select($selects);
}
$resolvedQueries = [];
foreach ($parsed as $query) {
// make em json encoded!
$resolvedQueries[] = $query->toString();
}
@ -95,12 +103,15 @@ class V20 extends Filter
/**
* Returns all relationship attribute keys in `key.*` format for use with `Query::select`.
* Recursively includes nested relationships up to 3 levels deep.
* Prevents infinite loops by tracking all visited collections in the current path.
*/
private function getRelatedCollectionKeys(
?string $databaseId = null,
?string $collectionId = null,
?string $prefix = null,
int $depth = 1,
array $visited = []
): array {
$databaseId ??= $this->getParamValue('databaseId');
$collectionId ??= $this->getParamValue('collectionId');
@ -112,6 +123,13 @@ class V20 extends Filter
) {
return [];
}
// Check if we've already visited this collection in the current path to prevent cycles
if (in_array($collectionId, $visited)) {
return [];
}
$visited[] = $collectionId;
$dbForProject = $this->getDbForProject();
if ($dbForProject === null) {
@ -144,23 +162,27 @@ class V20 extends Filter
$key = $attr['key'];
$fullKey = $prefix ? $prefix . '.' . $key : $key;
$relatedCollectionId = $attr['relatedCollection'] ?? null;
// Skip this relationship entirely if it points to an already visited collection
if ($relatedCollectionId && in_array($relatedCollectionId, $visited)) {
continue;
}
// Add the wildcard select for this relationship
$relationshipKeys[] = $fullKey . '.*';
// Get the related collection for nested relationships
$relatedCollectionId = $attr['relatedCollection'] ?? null;
// Continue recursively if we have a related collection
if ($relatedCollectionId) {
// Recursively get nested relationship keys
$nestedKeys = $this->getRelatedCollectionKeys(
$databaseId,
$relatedCollectionId,
$fullKey,
$depth + 1,
$visited
);
$relationshipKeys = \array_merge($relationshipKeys, $nestedKeys);
\array_push($relationshipKeys, ...$nestedKeys);
}
}