Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 13 additions & 13 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion src/Database/Adapter/Mongo.php
Original file line number Diff line number Diff line change
Expand Up @@ -1401,7 +1401,9 @@ public function castingAfter(Document $collection, Document $document): Document
private function convertStdClassToArray(mixed $value): mixed
{
if (is_object($value) && get_class($value) === stdClass::class) {
return array_map($this->convertStdClassToArray(...), get_object_vars($value));
$properties = get_object_vars($value);

return $properties === [] ? $value : array_map($this->convertStdClassToArray(...), $properties);
}

if (is_array($value)) {
Expand Down
58 changes: 53 additions & 5 deletions src/Database/Database.php
Original file line number Diff line number Diff line change
Expand Up @@ -669,7 +669,7 @@ function (mixed $value) {
},
/**
* @param string|null $value
* @return array|null
* @return mixed
*/
function (?string $value) {
if (is_null($value)) {
Expand All @@ -690,7 +690,7 @@ function (?string $value) {
* @return mixed
*/
function (mixed $value) {
if (!\is_array($value)) {
if (!\is_array($value) && !$value instanceof \stdClass) {
return $value;
}

Expand All @@ -708,12 +708,60 @@ function (mixed $value) {
if (!is_string($value)) {
return $value;
}
$decoded = json_decode($value, true);
return is_array($decoded) ? $decoded : $value;
$decoded = self::decodeObject($value);

return is_array($decoded) || $decoded instanceof \stdClass ? $decoded : $value;
}
);
}

private static function decodeObject(string $value): mixed
{
if (preg_match('/\{\s*\}/', $value) === 0) {
return json_decode($value, true);
}

return self::toAssociative(json_decode($value));
}

private static function toAssociative(mixed $value): mixed
{
if ($value instanceof \stdClass) {
$properties = (array)$value;

return $properties === [] ? $value : array_map(self::toAssociative(...), $properties);
}

if (is_array($value)) {
return array_map(self::toAssociative(...), $value);
}

return $value;
}

Comment thread
coderabbitai[bot] marked this conversation as resolved.
private static function valuesEqual(mixed $value, mixed $old): bool
{
if ($value instanceof \stdClass && $old instanceof \stdClass) {
return self::valuesEqual((array)$value, (array)$old);
}

if (is_array($value) && is_array($old)) {
if (array_keys($value) !== array_keys($old)) {
return false;
}

foreach ($value as $key => $item) {
if (!self::valuesEqual($item, $old[$key])) {
return false;
}
}

return true;
}

return $value === $old;
}

/**
* Add listener to events
* Passing a null $callback will remove the listener
Expand Down Expand Up @@ -6360,7 +6408,7 @@ public function updateDocument(string $collection, string $id, Document $documen

$oldValue = $old->getAttribute($key);

if ($value !== $oldValue) {
if (!self::valuesEqual($value, $oldValue)) {
$shouldUpdate = true;
break;
}
Expand Down
4 changes: 4 additions & 0 deletions src/Database/Validator/ObjectValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ public function isValid(mixed $value): bool
return json_last_error() === JSON_ERROR_NONE;
}

if ($value instanceof \stdClass) {
return true;
}

// Allow empty or associative arrays (non-list)
return empty($value) || (is_array($value) && !array_is_list($value));
}
Expand Down
100 changes: 100 additions & 0 deletions tests/e2e/Adapter/Scopes/ObjectAttributeTests.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Exception;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Authorization as AuthorizationException;
use Utopia\Database\Exception\Duplicate as DuplicateException;
use Utopia\Database\Exception\Index as IndexException;
use Utopia\Database\Exception\Query as QueryException;
Expand Down Expand Up @@ -963,6 +964,105 @@ public function testObjectAttributeDefaults(): void
$database->deleteCollection($collectionId);
}

public function testObjectAttributeEmptyObject(): void
{
/** @var Database $database */
$database = static::getDatabase();

if (!$database->getAdapter()->getSupportForObject()) {
$this->markTestSkipped('Adapter does not support object attributes');
}

$collectionId = ID::unique();
$database->createCollection($collectionId);
$this->createAttribute($database, $collectionId, 'meta', Database::VAR_OBJECT, 0, false);

$created = $database->createDocument($collectionId, new Document([
'$id' => 'emptyObject',
'$permissions' => [Permission::read(Role::any())],
'meta' => new \stdClass(),
]));

$this->assertSame('{}', json_encode($created->getAttribute('meta')));

$database->purgeCachedDocument($collectionId, 'emptyObject');
$read = $database->getDocument($collectionId, 'emptyObject');

$this->assertSame('{}', json_encode($read->getAttribute('meta')));
$cached = $database->getDocument($collectionId, 'emptyObject');
$this->assertSame('{}', json_encode($cached->getAttribute('meta')));

$updated = $database->updateDocument($collectionId, 'emptyObject', new Document([
'meta' => new \stdClass(),
]));
$this->assertSame($cached->getUpdatedAt(), $updated->getUpdatedAt());
$this->assertSame('{}', json_encode($updated->getAttribute('meta')));

try {
$database->updateDocument($collectionId, 'emptyObject', new Document([
'meta' => [],
]));
$this->fail('Changing an empty object to an empty array must require update permission');
} catch (AuthorizationException) {
$this->addToAssertionCount(1);
}

$database->deleteCollection($collectionId);
}

public function testObjectAttributeNestedEmptyObjects(): void
{
/** @var Database $database */
$database = static::getDatabase();

if (!$database->getAdapter()->getSupportForObject()) {
$this->markTestSkipped('Adapter does not support object attributes');
}

$collectionId = ID::unique();
$database->createCollection($collectionId);
$this->createAttribute($database, $collectionId, 'meta', Database::VAR_OBJECT, 0, false);

$created = $database->createDocument($collectionId, new Document([
'$id' => 'nestedEmptyObjects',
'$permissions' => [Permission::read(Role::any())],
'meta' => [
'inner' => new \stdClass(),
'arr' => [new \stdClass(), ['x' => 1]],
'emptyArray' => [],
],
]));
$createdMeta = $created->getAttribute('meta');
$this->assertSame('{}', json_encode($createdMeta['inner']));
$this->assertSame('{}', json_encode($createdMeta['arr'][0]));
$this->assertSame('{"x":1}', json_encode($createdMeta['arr'][1]));
$this->assertSame('[]', json_encode($createdMeta['emptyArray']));

$database->purgeCachedDocument($collectionId, 'nestedEmptyObjects');
$readMeta = $database->getDocument($collectionId, 'nestedEmptyObjects')->getAttribute('meta');
$this->assertSame('{}', json_encode($readMeta['inner']));
$this->assertSame('{}', json_encode($readMeta['arr'][0]));
$this->assertSame('{"x":1}', json_encode($readMeta['arr'][1]));
$this->assertSame('[]', json_encode($readMeta['emptyArray']));

$cached = $database->getDocument($collectionId, 'nestedEmptyObjects');
$cachedMeta = $cached->getAttribute('meta');
$this->assertSame('{}', json_encode($cachedMeta['inner']));
$this->assertSame('{}', json_encode($cachedMeta['arr'][0]));
$this->assertSame('{"x":1}', json_encode($cachedMeta['arr'][1]));
$this->assertSame('[]', json_encode($cachedMeta['emptyArray']));

$updatedMeta = $cachedMeta;
$updatedMeta['inner'] = new \stdClass();
$updatedMeta['arr'][0] = new \stdClass();
$updated = $database->updateDocument($collectionId, 'nestedEmptyObjects', new Document([
'meta' => $updatedMeta,
]));
$this->assertSame($cached->getUpdatedAt(), $updated->getUpdatedAt());

$database->deleteCollection($collectionId);
}

public function testMetadataWithVector(): void
{
/** @var Database $database */
Expand Down
1 change: 1 addition & 0 deletions tests/unit/Validator/ObjectTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ public function testEmptyCases(): void
$validator = new ObjectValidator();

$this->assertTrue($validator->isValid([]));
$this->assertTrue($validator->isValid(new \stdClass()));

$this->assertFalse($validator->isValid('sldfjsdlfj'));
}
Expand Down
Loading