diff --git a/app/config/specs/open-api3-1.8.x-client.json b/app/config/specs/open-api3-1.8.x-client.json index 553b7ce385..807a39a79d 100644 --- a/app/config/specs/open-api3-1.8.x-client.json +++ b/app/config/specs/open-api3-1.8.x-client.json @@ -5146,7 +5146,7 @@ ] }, "put": { - "summary": "Create or update a document", + "summary": "Upsert a document", "operationId": "databasesUpsertDocument", "tags": [ "databases" @@ -7800,7 +7800,7 @@ ] }, "put": { - "summary": "Create or update a row", + "summary": "Upsert a row", "operationId": "tablesDBUpsertRow", "tags": [ "tablesDB" diff --git a/app/config/specs/open-api3-1.8.x-console.json b/app/config/specs/open-api3-1.8.x-console.json index 7d33440d01..9b17bb4307 100644 --- a/app/config/specs/open-api3-1.8.x-console.json +++ b/app/config/specs/open-api3-1.8.x-console.json @@ -9360,7 +9360,7 @@ ] }, "put": { - "summary": "Create or update a document", + "summary": "Upsert a document", "operationId": "databasesUpsertDocument", "tags": [ "databases" @@ -36595,7 +36595,7 @@ ] }, "put": { - "summary": "Create or update a row", + "summary": "Upsert a row", "operationId": "tablesDBUpsertRow", "tags": [ "tablesDB" diff --git a/app/config/specs/open-api3-1.8.x-server.json b/app/config/specs/open-api3-1.8.x-server.json index d117377a21..1bbc0c7554 100644 --- a/app/config/specs/open-api3-1.8.x-server.json +++ b/app/config/specs/open-api3-1.8.x-server.json @@ -8842,7 +8842,7 @@ ] }, "put": { - "summary": "Create or update a document", + "summary": "Upsert a document", "operationId": "databasesUpsertDocument", "tags": [ "databases" @@ -26960,7 +26960,7 @@ ] }, "put": { - "summary": "Create or update a row", + "summary": "Upsert a row", "operationId": "tablesDBUpsertRow", "tags": [ "tablesDB" diff --git a/app/config/specs/open-api3-latest-client.json b/app/config/specs/open-api3-latest-client.json index 553b7ce385..807a39a79d 100644 --- a/app/config/specs/open-api3-latest-client.json +++ b/app/config/specs/open-api3-latest-client.json @@ -5146,7 +5146,7 @@ ] }, "put": { - "summary": "Create or update a document", + "summary": "Upsert a document", "operationId": "databasesUpsertDocument", "tags": [ "databases" @@ -7800,7 +7800,7 @@ ] }, "put": { - "summary": "Create or update a row", + "summary": "Upsert a row", "operationId": "tablesDBUpsertRow", "tags": [ "tablesDB" diff --git a/app/config/specs/open-api3-latest-console.json b/app/config/specs/open-api3-latest-console.json index 7d33440d01..9b17bb4307 100644 --- a/app/config/specs/open-api3-latest-console.json +++ b/app/config/specs/open-api3-latest-console.json @@ -9360,7 +9360,7 @@ ] }, "put": { - "summary": "Create or update a document", + "summary": "Upsert a document", "operationId": "databasesUpsertDocument", "tags": [ "databases" @@ -36595,7 +36595,7 @@ ] }, "put": { - "summary": "Create or update a row", + "summary": "Upsert a row", "operationId": "tablesDBUpsertRow", "tags": [ "tablesDB" diff --git a/app/config/specs/open-api3-latest-server.json b/app/config/specs/open-api3-latest-server.json index d117377a21..1bbc0c7554 100644 --- a/app/config/specs/open-api3-latest-server.json +++ b/app/config/specs/open-api3-latest-server.json @@ -8842,7 +8842,7 @@ ] }, "put": { - "summary": "Create or update a document", + "summary": "Upsert a document", "operationId": "databasesUpsertDocument", "tags": [ "databases" @@ -26960,7 +26960,7 @@ ] }, "put": { - "summary": "Create or update a row", + "summary": "Upsert a row", "operationId": "tablesDBUpsertRow", "tags": [ "tablesDB" diff --git a/app/config/specs/swagger2-1.8.x-client.json b/app/config/specs/swagger2-1.8.x-client.json index 9b9a695896..650a74c817 100644 --- a/app/config/specs/swagger2-1.8.x-client.json +++ b/app/config/specs/swagger2-1.8.x-client.json @@ -5273,7 +5273,7 @@ ] }, "put": { - "summary": "Create or update a document", + "summary": "Upsert a document", "operationId": "databasesUpsertDocument", "consumes": [ "application\/json" @@ -7867,7 +7867,7 @@ ] }, "put": { - "summary": "Create or update a row", + "summary": "Upsert a row", "operationId": "tablesDBUpsertRow", "consumes": [ "application\/json" diff --git a/app/config/specs/swagger2-1.8.x-console.json b/app/config/specs/swagger2-1.8.x-console.json index d8fc57519f..ba78f719a8 100644 --- a/app/config/specs/swagger2-1.8.x-console.json +++ b/app/config/specs/swagger2-1.8.x-console.json @@ -9462,7 +9462,7 @@ ] }, "put": { - "summary": "Create or update a document", + "summary": "Upsert a document", "operationId": "databasesUpsertDocument", "consumes": [ "application\/json" @@ -36702,7 +36702,7 @@ ] }, "put": { - "summary": "Create or update a row", + "summary": "Upsert a row", "operationId": "tablesDBUpsertRow", "consumes": [ "application\/json" diff --git a/app/config/specs/swagger2-1.8.x-server.json b/app/config/specs/swagger2-1.8.x-server.json index f9c84e4f2d..9d3673333d 100644 --- a/app/config/specs/swagger2-1.8.x-server.json +++ b/app/config/specs/swagger2-1.8.x-server.json @@ -8934,7 +8934,7 @@ ] }, "put": { - "summary": "Create or update a document", + "summary": "Upsert a document", "operationId": "databasesUpsertDocument", "consumes": [ "application\/json" @@ -27127,7 +27127,7 @@ ] }, "put": { - "summary": "Create or update a row", + "summary": "Upsert a row", "operationId": "tablesDBUpsertRow", "consumes": [ "application\/json" diff --git a/app/config/specs/swagger2-latest-client.json b/app/config/specs/swagger2-latest-client.json index 9b9a695896..650a74c817 100644 --- a/app/config/specs/swagger2-latest-client.json +++ b/app/config/specs/swagger2-latest-client.json @@ -5273,7 +5273,7 @@ ] }, "put": { - "summary": "Create or update a document", + "summary": "Upsert a document", "operationId": "databasesUpsertDocument", "consumes": [ "application\/json" @@ -7867,7 +7867,7 @@ ] }, "put": { - "summary": "Create or update a row", + "summary": "Upsert a row", "operationId": "tablesDBUpsertRow", "consumes": [ "application\/json" diff --git a/app/config/specs/swagger2-latest-console.json b/app/config/specs/swagger2-latest-console.json index d8fc57519f..ba78f719a8 100644 --- a/app/config/specs/swagger2-latest-console.json +++ b/app/config/specs/swagger2-latest-console.json @@ -9462,7 +9462,7 @@ ] }, "put": { - "summary": "Create or update a document", + "summary": "Upsert a document", "operationId": "databasesUpsertDocument", "consumes": [ "application\/json" @@ -36702,7 +36702,7 @@ ] }, "put": { - "summary": "Create or update a row", + "summary": "Upsert a row", "operationId": "tablesDBUpsertRow", "consumes": [ "application\/json" diff --git a/app/config/specs/swagger2-latest-server.json b/app/config/specs/swagger2-latest-server.json index f9c84e4f2d..9d3673333d 100644 --- a/app/config/specs/swagger2-latest-server.json +++ b/app/config/specs/swagger2-latest-server.json @@ -8934,7 +8934,7 @@ ] }, "put": { - "summary": "Create or update a document", + "summary": "Upsert a document", "operationId": "databasesUpsertDocument", "consumes": [ "application\/json" @@ -27127,7 +27127,7 @@ ] }, "put": { - "summary": "Create or update a row", + "summary": "Upsert a row", "operationId": "tablesDBUpsertRow", "consumes": [ "application\/json" diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 40eddf574f..7742aac18f 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -810,12 +810,6 @@ App::shutdown() } if (!empty($queueForDatabase->getType())) { - Console::info("Triggering database event: \n" . \json_encode([ - 'projectId' => $project->getId(), - 'databaseId' => $queueForDatabase->getDatabase()?->getId(), - 'tableId' => $queueForDatabase->getTable()?->getId() ?? $queueForDatabase->getCollection()?->getId(), - 'rowId' => $queueForDatabase->getRow()?->getId() ?? $queueForDatabase->getDocument()?->getId(), - ])); $queueForDatabase->trigger(); } diff --git a/composer.lock b/composer.lock index 077f5204d1..3bf17bc228 100644 --- a/composer.lock +++ b/composer.lock @@ -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", diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Upsert.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Upsert.php index 8427ee6cd2..6027a20c41 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Upsert.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Upsert.php @@ -45,7 +45,7 @@ class Upsert extends Action $this ->setHttpMethod(self::HTTP_REQUEST_METHOD_PUT) ->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/documents/:documentId') - ->desc('Create or update a document') + ->desc('Upsert a document') ->groups(['api', 'database']) ->label('event', 'databases.[databaseId].collections.[collectionId].documents.[documentId].upsert') ->label('scope', 'documents.write') diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Upsert.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Upsert.php index 36ca9709e1..c2695379e3 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Upsert.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Upsert.php @@ -31,7 +31,7 @@ class Upsert extends DocumentUpsert $this ->setHttpMethod(self::HTTP_REQUEST_METHOD_PUT) ->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/rows/:rowId') - ->desc('Create or update a row') + ->desc('Upsert a row') ->groups(['api', 'database']) ->label('event', 'databases.[databaseId].tables.[tableId].rows.[rowId].upsert') ->label('scope', ['rows.write', 'documents.write']) diff --git a/src/Appwrite/Platform/Modules/Databases/Workers/Databases.php b/src/Appwrite/Platform/Modules/Databases/Workers/Databases.php index 22f1e6a2f2..9a98d77d2d 100644 --- a/src/Appwrite/Platform/Modules/Databases/Workers/Databases.php +++ b/src/Appwrite/Platform/Modules/Databases/Workers/Databases.php @@ -64,14 +64,6 @@ class Databases extends Action $collection = new Document($payload['table'] ?? $payload['collection'] ?? []); $database = new Document($payload['database'] ?? []); - Console::info("Processing database operation: \n" . \json_encode([ - 'type' => $type, - 'projectId' => $project->getId(), - 'databaseId' => $database->getId(), - 'collectionId' => $collection->getId(), - 'documentId' => $document->getId(), - ], JSON_PRETTY_PRINT)); - $log->addTag('projectId', $project->getId()); $log->addTag('type', $type); diff --git a/src/Appwrite/Utopia/Request/Filters/V20.php b/src/Appwrite/Utopia/Request/Filters/V20.php index 2683d600ef..c8622f8b7a 100644 --- a/src/Appwrite/Utopia/Request/Filters/V20.php +++ b/src/Appwrite/Utopia/Request/Filters/V20.php @@ -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'); @@ -113,6 +124,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) { return []; @@ -144,20 +162,24 @@ 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);