diff --git a/app/config/collections/platform.php b/app/config/collections/platform.php index 73c9eea870..2fb3168c5b 100644 --- a/app/config/collections/platform.php +++ b/app/config/collections/platform.php @@ -632,30 +632,6 @@ $platformCollections = [ '$id' => ID::custom('keys'), 'name' => 'keys', 'attributes' => [ - // Delete eventuelly, when removing dual-write too - [ - '$id' => ID::custom('projectInternalId'), - 'type' => Database::VAR_STRING, - 'format' => '', - 'size' => Database::LENGTH_KEY, - 'signed' => true, - 'required' => false, - 'default' => null, - 'array' => false, - 'filters' => [], - ], - // Delete eventuelly, when removing dual-write too - [ - '$id' => ID::custom('projectId'), - 'type' => Database::VAR_STRING, - 'format' => '', - 'size' => Database::LENGTH_KEY, - 'signed' => true, - 'required' => false, - 'default' => 0, - 'array' => false, - 'filters' => [], - ], [ '$id' => ID::custom('resourceType'), 'type' => Database::VAR_STRING, diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index 1e03c861d1..57ad3030d9 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -1502,9 +1502,6 @@ App::post('/v1/projects/:projectId/keys') Permission::update(Role::any()), Permission::delete(Role::any()), ], - // TODO: @hmacr Remove `projectInternalId` and `projectId` column writes before deleting the column. - 'projectInternalId' => $project->getSequence(), - 'projectId' => $project->getId(), 'resourceInternalId' => $project->getSequence(), 'resourceId' => $project->getId(), 'resourceType' => 'projects', diff --git a/app/controllers/general.php b/app/controllers/general.php index eaa01495b9..40bcdf4ea3 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -1228,7 +1228,7 @@ App::error() } switch ($class) { - case 'Utopia\Exception': + case Utopia\Exception::class: $error = new AppwriteException(AppwriteException::GENERAL_UNKNOWN, $message, $code, $error); switch ($code) { case 400: @@ -1239,10 +1239,10 @@ App::error() break; } break; - case 'Utopia\Database\Exception\Authorization': + case Utopia\Database\Exception\Authorization::class: $error = new AppwriteException(AppwriteException::USER_UNAUTHORIZED); break; - case 'Utopia\Database\Exception\Timeout': + case Utopia\Database\Exception\Timeout::class: $error = new AppwriteException(AppwriteException::DATABASE_TIMEOUT, previous: $error); break; } diff --git a/app/controllers/mock.php b/app/controllers/mock.php index 16d6d72de7..42b300e410 100644 --- a/app/controllers/mock.php +++ b/app/controllers/mock.php @@ -200,9 +200,6 @@ App::post('/v1/mock/api-key-unprefixed') Permission::update(Role::any()), Permission::delete(Role::any()), ], - // TODO: @hmacr Remove `projectInternalId` and `projectId` column writes before deleting the column. - 'projectInternalId' => $project->getSequence(), - 'projectId' => $project->getId(), 'resourceInternalId' => $project->getSequence(), 'resourceId' => $project->getId(), 'resourceType' => 'projects', diff --git a/composer.lock b/composer.lock index 812b86bb55..76b183c953 100644 --- a/composer.lock +++ b/composer.lock @@ -2066,16 +2066,16 @@ }, { "name": "phpseclib/phpseclib", - "version": "3.0.48", + "version": "3.0.49", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "64065a5679c50acb886e82c07aa139b0f757bb89" + "reference": "6233a1e12584754e6b5daa69fe1289b47775c1b9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/64065a5679c50acb886e82c07aa139b0f757bb89", - "reference": "64065a5679c50acb886e82c07aa139b0f757bb89", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/6233a1e12584754e6b5daa69fe1289b47775c1b9", + "reference": "6233a1e12584754e6b5daa69fe1289b47775c1b9", "shasum": "" }, "require": { @@ -2156,7 +2156,7 @@ ], "support": { "issues": "https://github.com/phpseclib/phpseclib/issues", - "source": "https://github.com/phpseclib/phpseclib/tree/3.0.48" + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.49" }, "funding": [ { @@ -2172,7 +2172,7 @@ "type": "tidelift" } ], - "time": "2025-12-15T11:51:42+00:00" + "time": "2026-01-27T09:17:28+00:00" }, { "name": "psr/container", @@ -8200,16 +8200,16 @@ }, { "name": "symfony/finder", - "version": "v8.0.4", + "version": "v8.0.5", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "42e48eb02e07d5f3771d194d67da117eb824c8c1" + "reference": "8bd576e97c67d45941365bf824e18dc8538e6eb0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/42e48eb02e07d5f3771d194d67da117eb824c8c1", - "reference": "42e48eb02e07d5f3771d194d67da117eb824c8c1", + "url": "https://api.github.com/repos/symfony/finder/zipball/8bd576e97c67d45941365bf824e18dc8538e6eb0", + "reference": "8bd576e97c67d45941365bf824e18dc8538e6eb0", "shasum": "" }, "require": { @@ -8244,7 +8244,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v8.0.4" + "source": "https://github.com/symfony/finder/tree/v8.0.5" }, "funding": [ { @@ -8264,7 +8264,7 @@ "type": "tidelift" } ], - "time": "2026-01-12T12:37:40+00:00" + "time": "2026-01-26T15:08:38+00:00" }, { "name": "symfony/options-resolver", @@ -8669,16 +8669,16 @@ }, { "name": "symfony/process", - "version": "v8.0.4", + "version": "v8.0.5", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "10df72602d88c0a3fa685b822976a052611dd607" + "reference": "b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/10df72602d88c0a3fa685b822976a052611dd607", - "reference": "10df72602d88c0a3fa685b822976a052611dd607", + "url": "https://api.github.com/repos/symfony/process/zipball/b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674", + "reference": "b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674", "shasum": "" }, "require": { @@ -8710,7 +8710,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v8.0.4" + "source": "https://github.com/symfony/process/tree/v8.0.5" }, "funding": [ { @@ -8730,7 +8730,7 @@ "type": "tidelift" } ], - "time": "2026-01-23T11:07:10+00:00" + "time": "2026-01-26T15:08:38+00:00" }, { "name": "symfony/string", diff --git a/src/Appwrite/GraphQL/Types/Mapper.php b/src/Appwrite/GraphQL/Types/Mapper.php index c9ae84f1c3..8935e67c0b 100644 --- a/src/Appwrite/GraphQL/Types/Mapper.php +++ b/src/Appwrite/GraphQL/Types/Mapper.php @@ -269,59 +269,59 @@ class Mapper } switch ((!empty($validator)) ? $validator::class : '') { - case 'Appwrite\Auth\Validator\Password': - case 'Appwrite\Event\Validator\Event': - case 'Appwrite\Event\Validator\FunctionEvent': - case 'Appwrite\Network\Validator\CNAME': - case 'Appwrite\Network\Validator\Email': - case 'Appwrite\Network\Validator\Redirect': - case 'Appwrite\Network\Validator\DNS': - case 'Appwrite\Network\Validator\Origin': - case 'Appwrite\Task\Validator\Cron': - case 'Appwrite\Utopia\Database\Validator\CustomId': - case 'Utopia\Database\Validator\Key': - case 'Utopia\Database\Validator\UID': - case 'Utopia\Validator\Domain': - case 'Utopia\Validator\HexColor': - case 'Utopia\Validator\Host': - case 'Utopia\Validator\IP': - case 'Utopia\Validator\Origin': - case 'Utopia\Validator\Text': - case 'Utopia\Validator\URL': - case 'Utopia\Validator\WhiteList': + case \Appwrite\Auth\Validator\Password::class: + case \Appwrite\Event\Validator\Event::class: + case \Appwrite\Event\Validator\FunctionEvent::class: + case \Appwrite\Network\Validator\CNAME::class: + case \Appwrite\Network\Validator\Email::class: + case \Appwrite\Network\Validator\Redirect::class: + case \Appwrite\Network\Validator\DNS::class: + case \Appwrite\Network\Validator\Origin::class: + case \Appwrite\Task\Validator\Cron::class: + case \Appwrite\Utopia\Database\Validator\CustomId::class: + case \Utopia\Database\Validator\Key::class: + case \Utopia\Database\Validator\UID::class: + case \Utopia\Validator\Domain::class: + case \Utopia\Validator\HexColor::class: + case \Utopia\Validator\Host::class: + case \Utopia\Validator\IP::class: + case \Utopia\Validator\Origin::class: + case \Utopia\Validator\Text::class: + case \Utopia\Validator\URL::class: + case \Utopia\Validator\WhiteList::class: default: $type = Type::string(); break; - case 'Appwrite\Utopia\Database\Validator\Queries\Attributes': - case 'Appwrite\Utopia\Database\Validator\Queries\Base': - case 'Appwrite\Utopia\Database\Validator\Queries\Buckets': - case 'Appwrite\Utopia\Database\Validator\Queries\Tables': - case 'Appwrite\Utopia\Database\Validator\Queries\Collections': - case 'Appwrite\Utopia\Database\Validator\Queries\Columns': - case 'Appwrite\Utopia\Database\Validator\Queries\Databases': - case 'Appwrite\Utopia\Database\Validator\Queries\Deployments': - case 'Appwrite\Utopia\Database\Validator\Queries\Executions': - case 'Appwrite\Utopia\Database\Validator\Queries\Files': - case 'Appwrite\Utopia\Database\Validator\Queries\Functions': - case 'Appwrite\Utopia\Database\Validator\Queries\Indexes': - case 'Appwrite\Utopia\Database\Validator\Queries\Installations': - case 'Appwrite\Utopia\Database\Validator\Queries\Memberships': - case 'Appwrite\Utopia\Database\Validator\Queries\Projects': - case 'Appwrite\Utopia\Database\Validator\Queries\Rules': - case 'Appwrite\Utopia\Database\Validator\Queries\Teams': - case 'Appwrite\Utopia\Database\Validator\Queries\Users': - case 'Appwrite\Utopia\Database\Validator\Queries\Variables': - case 'Utopia\Database\Validator\Authorization': - case 'Utopia\Database\Validator\Permissions': - case 'Utopia\Database\Validator\Queries': - case 'Utopia\Database\Validator\Queries\Documents': - case 'Utopia\Database\Validator\Roles': + case \Appwrite\Utopia\Database\Validator\Queries\Attributes::class: + case \Appwrite\Utopia\Database\Validator\Queries\Base::class: + case \Appwrite\Utopia\Database\Validator\Queries\Buckets::class: + case \Appwrite\Utopia\Database\Validator\Queries\Tables::class: + case \Appwrite\Utopia\Database\Validator\Queries\Collections::class: + case \Appwrite\Utopia\Database\Validator\Queries\Columns::class: + case \Appwrite\Utopia\Database\Validator\Queries\Databases::class: + case \Appwrite\Utopia\Database\Validator\Queries\Deployments::class: + case \Appwrite\Utopia\Database\Validator\Queries\Executions::class: + case \Appwrite\Utopia\Database\Validator\Queries\Files::class: + case \Appwrite\Utopia\Database\Validator\Queries\Functions::class: + case \Appwrite\Utopia\Database\Validator\Queries\Indexes::class: + case \Appwrite\Utopia\Database\Validator\Queries\Installations::class: + case \Appwrite\Utopia\Database\Validator\Queries\Memberships::class: + case \Appwrite\Utopia\Database\Validator\Queries\Projects::class: + case \Appwrite\Utopia\Database\Validator\Queries\Rules::class: + case \Appwrite\Utopia\Database\Validator\Queries\Teams::class: + case \Appwrite\Utopia\Database\Validator\Queries\Users::class: + case \Appwrite\Utopia\Database\Validator\Queries\Variables::class: + case \Utopia\Database\Validator\Authorization::class: + case \Utopia\Database\Validator\Permissions::class: + case \Utopia\Database\Validator\Queries::class: + case \Utopia\Database\Validator\Queries\Documents::class: + case \Utopia\Database\Validator\Roles::class: $type = Type::listOf(Type::string()); break; - case 'Utopia\Validator\Boolean': + case \Utopia\Validator\Boolean::class: $type = Type::boolean(); break; - case 'Utopia\Validator\ArrayList': + case \Utopia\Validator\ArrayList::class: $type = Type::listOf(self::param( $utopia, $validator->getValidator(), @@ -329,11 +329,11 @@ class Mapper $injections )); break; - case 'Utopia\Validator\Integer': - case 'Utopia\Validator\Numeric': + case \Utopia\Validator\Integer::class: + case \Utopia\Validator\Numeric::class: $type = Type::int(); break; - case 'Utopia\Validator\Range': + case \Utopia\Validator\Range::class: // Check if the Range validator is for float or integer if ($validator instanceof \Utopia\Validator\Range && $validator->getType() === \Utopia\Validator\Range::TYPE_FLOAT) { $type = Type::float(); @@ -341,16 +341,16 @@ class Mapper $type = Type::int(); } break; - case 'Utopia\Validator\FloatValidator': + case \Utopia\Validator\FloatValidator::class: $type = Type::float(); break; - case 'Utopia\Validator\Assoc': + case \Utopia\Validator\Assoc::class: $type = Types::assoc(); break; - case 'Utopia\Validator\JSON': + case \Utopia\Validator\JSON::class: $type = Types::json(); break; - case 'Utopia\Storage\Validator\File': + case \Utopia\Storage\Validator\File::class: $type = Types::inputFile(); break; } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php index 4df180ee0b..3159eed5e3 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php @@ -6,6 +6,7 @@ use Appwrite\Event\Event; use Appwrite\Extend\Exception; use Appwrite\Functions\EventProcessor; use Appwrite\Platform\Modules\Databases\Http\Databases\Action as DatabasesAction; +use Appwrite\Utopia\Database\Validator\CustomId; use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Validator\Authorization; @@ -250,6 +251,35 @@ abstract class Action extends DatabasesAction return $document; } + /** + * Validate relationship values. + * Handles Document objects, ID strings, and associative arrays. + */ + protected function validateRelationship(mixed $relation): void + { + $relationId = null; + + if ($relation instanceof Document) { + $relationId = $relation->getId(); + } elseif (\is_string($relation)) { + $relationId = $relation; + } elseif (\is_array($relation) && !\array_is_list($relation)) { + $relationId = $relation['$id'] ?? null; + } else { + throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, 'Relationship value must be an object, document ID string, or associative array'); + } + + if ($relationId !== null) { + if (!\is_string($relationId)) { + throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, 'Relationship $id must be a string'); + } + $validator = new CustomId(); + if (!$validator->isValid($relationId)) { + throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, $validator->getDescription()); + } + } + } + /** * Resolves relationships in a document and attaches metadata. */ diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php index 15811db92e..624d3c48ff 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php @@ -319,6 +319,9 @@ class Create extends Action $relation['$id'] = ID::unique(); $relation = new Document($relation); } + + $this->validateRelationship($relation); + if ($relation instanceof Document) { $relation = $this->removeReadonlyAttributes($relation, $isAPIKey || $isPrivilegedUser); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php index a92d8ec180..ff3ab6e23c 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php @@ -210,6 +210,9 @@ class Update extends Action $relation['$id'] = ID::unique(); $relation = new Document($relation); } + + $this->validateRelationship($relation); + if ($relation instanceof Document) { $relation = $this->removeReadonlyAttributes($relation, $isAPIKey || $isPrivilegedUser); 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 62e59dd010..d0536b65ef 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 @@ -220,6 +220,9 @@ class Upsert extends Action $relation['$id'] = ID::unique(); $relation = new Document($relation); } + + $this->validateRelationship($relation); + if ($relation instanceof Document) { $relation = $this->removeReadonlyAttributes($relation, $isAPIKey || $isPrivilegedUser); diff --git a/src/Appwrite/SDK/Specification/Format/OpenAPI3.php b/src/Appwrite/SDK/Specification/Format/OpenAPI3.php index 8171a45db4..8e710cd0ac 100644 --- a/src/Appwrite/SDK/Specification/Format/OpenAPI3.php +++ b/src/Appwrite/SDK/Specification/Format/OpenAPI3.php @@ -392,51 +392,51 @@ class OpenAPI3 extends Format : ''; switch ($base) { - case 'Appwrite\Utopia\Database\Validator\Queries\Base': + case \Appwrite\Utopia\Database\Validator\Queries\Base::class: $class = $base; break; } - if ($class === 'Utopia\Validator\AnyOf') { + if ($class === \Utopia\Validator\AnyOf::class) { $validator = $param['validator']->getValidators()[0]; $class = \get_class($validator); } $array = false; - if ($class === 'Utopia\Validator\ArrayList') { + if ($class === \Utopia\Validator\ArrayList::class) { $array = true; $subclass = \get_class($validator->getValidator()); switch ($subclass) { - case 'Appwrite\Utopia\Database\Validator\Operation': - case 'Utopia\Validator\WhiteList': + case \Appwrite\Utopia\Database\Validator\Operation::class: + case \Utopia\Validator\WhiteList::class: $class = $subclass; break; } } switch ($class) { - case 'Utopia\Database\Validator\UID': - case 'Utopia\Validator\Text': + case \Utopia\Database\Validator\UID::class: + case \Utopia\Validator\Text::class: $node['schema']['type'] = $validator->getType(); $node['schema']['x-example'] = ($param['example'] ?? '') ?: '<' . \strtoupper(Template::fromCamelCaseToSnake($node['name'])) . '>'; break; - case 'Utopia\Validator\Boolean': + case \Utopia\Validator\Boolean::class: $node['schema']['type'] = $validator->getType(); $node['schema']['x-example'] = ($param['example'] ?? '') ?: false; break; - case 'Appwrite\Utopia\Database\Validator\CustomId': + case \Appwrite\Utopia\Database\Validator\CustomId::class: if ($sdk->getType() === MethodType::UPLOAD) { $node['schema']['x-upload-id'] = true; } $node['schema']['type'] = $validator->getType(); $node['schema']['x-example'] = ($param['example'] ?? '') ?: '<' . \strtoupper(Template::fromCamelCaseToSnake($node['name'])) . '>'; break; - case 'Utopia\Database\Validator\DatetimeValidator': + case \Utopia\Database\Validator\DatetimeValidator::class: $node['schema']['type'] = $validator->getType(); $node['schema']['format'] = 'datetime'; $node['schema']['x-example'] = ($param['example'] ?? '') ?: Model::TYPE_DATETIME_EXAMPLE; break; - case 'Utopia\Database\Validator\Spatial': + case \Utopia\Database\Validator\Spatial::class: /** @var Spatial $validator */ $node['schema']['type'] = 'array'; $node['schema']['items'] = [ @@ -450,31 +450,31 @@ class OpenAPI3 extends Format Database::VAR_POLYGON => '[[[1, 2], [3, 4], [5, 6], [1, 2]]]', }; break; - case 'Appwrite\Network\Validator\Email': + case \Appwrite\Network\Validator\Email::class: $node['schema']['type'] = $validator->getType(); $node['schema']['format'] = 'email'; $node['schema']['x-example'] = ($param['example'] ?? '') ?: 'email@example.com'; break; - case 'Utopia\Validator\Host': - case 'Utopia\Validator\URL': - case 'Appwrite\Network\Validator\Redirect': + case \Utopia\Validator\Host::class: + case \Utopia\Validator\URL::class: + case \Appwrite\Network\Validator\Redirect::class: $node['schema']['type'] = $validator->getType(); $node['schema']['format'] = 'url'; $node['schema']['x-example'] = ($param['example'] ?? '') ?: 'https://example.com'; break; - case 'Utopia\Validator\JSON': - case 'Utopia\Validator\Mock': - case 'Utopia\Validator\Assoc': + case \Utopia\Validator\JSON::class: + case \Utopia\Validator\Mock::class: + case \Utopia\Validator\Assoc::class: $param['default'] = (empty($param['default'])) ? new \stdClass() : $param['default']; $node['schema']['type'] = 'object'; $node['schema']['x-example'] = ($param['example'] ?? '') ?: '{}'; break; - case 'Utopia\Storage\Validator\File': + case \Utopia\Storage\Validator\File::class: $consumes = ['multipart/form-data']; $node['schema']['type'] = $validator->getType(); $node['schema']['format'] = 'binary'; break; - case 'Utopia\Validator\ArrayList': + case \Utopia\Validator\ArrayList::class: /** @var ArrayList $validator */ $node['schema']['type'] = 'array'; $node['schema']['items'] = [ @@ -484,92 +484,92 @@ class OpenAPI3 extends Format $node['schema']['x-example'] = $param['example']; } break; - case 'Appwrite\Utopia\Database\Validator\Queries\Base': - case 'Appwrite\Utopia\Database\Validator\Queries\Columns': - case 'Appwrite\Utopia\Database\Validator\Queries\Attributes': - case 'Appwrite\Utopia\Database\Validator\Queries\Buckets': - case 'Appwrite\Utopia\Database\Validator\Queries\Tables': - case 'Appwrite\Utopia\Database\Validator\Queries\Collections': - case 'Appwrite\Utopia\Database\Validator\Queries\Databases': - case 'Appwrite\Utopia\Database\Validator\Queries\Deployments': - case 'Appwrite\Utopia\Database\Validator\Queries\Executions': - case 'Appwrite\Utopia\Database\Validator\Queries\Files': - case 'Appwrite\Utopia\Database\Validator\Queries\Functions': - case 'Appwrite\Utopia\Database\Validator\Queries\Identities': - case 'Appwrite\Utopia\Database\Validator\Queries\Indexes': - case 'Appwrite\Utopia\Database\Validator\Queries\Installations': - case 'Appwrite\Utopia\Database\Validator\Queries\Memberships': - case 'Appwrite\Utopia\Database\Validator\Queries\Messages': - case 'Appwrite\Utopia\Database\Validator\Queries\Migrations': - case 'Appwrite\Utopia\Database\Validator\Queries\Projects': - case 'Appwrite\Utopia\Database\Validator\Queries\Providers': - case 'Appwrite\Utopia\Database\Validator\Queries\Rules': - case 'Appwrite\Utopia\Database\Validator\Queries\Subscribers': - case 'Appwrite\Utopia\Database\Validator\Queries\Targets': - case 'Appwrite\Utopia\Database\Validator\Queries\Teams': - case 'Appwrite\Utopia\Database\Validator\Queries\Topics': - case 'Appwrite\Utopia\Database\Validator\Queries\Users': - case 'Appwrite\Utopia\Database\Validator\Queries\Variables': - case 'Utopia\Database\Validator\Queries': - case 'Utopia\Database\Validator\Queries\Document': - case 'Utopia\Database\Validator\Queries\Documents': + case \Appwrite\Utopia\Database\Validator\Queries\Base::class: + case \Appwrite\Utopia\Database\Validator\Queries\Columns::class: + case \Appwrite\Utopia\Database\Validator\Queries\Attributes::class: + case \Appwrite\Utopia\Database\Validator\Queries\Buckets::class: + case \Appwrite\Utopia\Database\Validator\Queries\Tables::class: + case \Appwrite\Utopia\Database\Validator\Queries\Collections::class: + case \Appwrite\Utopia\Database\Validator\Queries\Databases::class: + case \Appwrite\Utopia\Database\Validator\Queries\Deployments::class: + case \Appwrite\Utopia\Database\Validator\Queries\Executions::class: + case \Appwrite\Utopia\Database\Validator\Queries\Files::class: + case \Appwrite\Utopia\Database\Validator\Queries\Functions::class: + case \Appwrite\Utopia\Database\Validator\Queries\Identities::class: + case \Appwrite\Utopia\Database\Validator\Queries\Indexes::class: + case \Appwrite\Utopia\Database\Validator\Queries\Installations::class: + case \Appwrite\Utopia\Database\Validator\Queries\Memberships::class: + case \Appwrite\Utopia\Database\Validator\Queries\Messages::class: + case \Appwrite\Utopia\Database\Validator\Queries\Migrations::class: + case \Appwrite\Utopia\Database\Validator\Queries\Projects::class: + case \Appwrite\Utopia\Database\Validator\Queries\Providers::class: + case \Appwrite\Utopia\Database\Validator\Queries\Rules::class: + case \Appwrite\Utopia\Database\Validator\Queries\Subscribers::class: + case \Appwrite\Utopia\Database\Validator\Queries\Targets::class: + case \Appwrite\Utopia\Database\Validator\Queries\Teams::class: + case \Appwrite\Utopia\Database\Validator\Queries\Topics::class: + case \Appwrite\Utopia\Database\Validator\Queries\Users::class: + case \Appwrite\Utopia\Database\Validator\Queries\Variables::class: + case \Utopia\Database\Validator\Queries::class: + case \Utopia\Database\Validator\Queries\Document::class: + case \Utopia\Database\Validator\Queries\Documents::class: $node['schema']['type'] = 'array'; $node['schema']['items'] = [ 'type' => 'string', ]; break; - case 'Utopia\Database\Validator\Permissions': + case \Utopia\Database\Validator\Permissions::class: $node['schema']['type'] = $validator->getType(); $node['schema']['items'] = [ 'type' => 'string', ]; $node['schema']['x-example'] = ($param['example'] ?? '') ?: '["' . Permission::read(Role::any()) . '"]'; break; - case 'Utopia\Database\Validator\Roles': + case \Utopia\Database\Validator\Roles::class: $node['schema']['type'] = $validator->getType(); $node['schema']['items'] = [ 'type' => 'string', ]; $node['schema']['x-example'] = ($param['example'] ?? '') ?: '["' . Role::any()->toString() . '"]'; break; - case 'Appwrite\Auth\Validator\Password': + case \Appwrite\Auth\Validator\Password::class: $node['schema']['type'] = $validator->getType(); $node['schema']['format'] = 'password'; $node['schema']['x-example'] = ($param['example'] ?? '') ?: 'password'; break; - case 'Appwrite\Auth\Validator\Phone': + case \Appwrite\Auth\Validator\Phone::class: $node['schema']['type'] = $validator->getType(); $node['schema']['format'] = 'phone'; $node['schema']['x-example'] = ($param['example'] ?? '') ?: '+12065550100'; // In the US, 555 is reserved like example.com break; - case 'Utopia\Validator\Range': + case \Utopia\Validator\Range::class: /** @var Range $validator */ $node['schema']['type'] = $validator->getType() === Validator::TYPE_FLOAT ? 'number' : $validator->getType(); $node['schema']['format'] = $validator->getType() == Validator::TYPE_INTEGER ? 'int32' : 'float'; $node['schema']['x-example'] = ($param['example'] ?? '') ?: $validator->getMin(); break; - case 'Utopia\Validator\Integer': + case \Utopia\Validator\Integer::class: $node['schema']['type'] = $validator->getType(); $node['schema']['format'] = $validator->getFormat(); if (!empty($param['example'])) { $node['schema']['x-example'] = $param['example']; } break; - case 'Utopia\Validator\Numeric': - case 'Utopia\Validator\FloatValidator': + case \Utopia\Validator\Numeric::class: + case \Utopia\Validator\FloatValidator::class: $node['schema']['type'] = 'number'; $node['schema']['format'] = 'float'; if (!empty($param['example'])) { $node['schema']['x-example'] = $param['example']; } break; - case 'Utopia\Validator\Length': + case \Utopia\Validator\Length::class: $node['schema']['type'] = $validator->getType(); if (!empty($param['example'])) { $node['schema']['x-example'] = $param['example']; } break; - case 'Utopia\Validator\WhiteList': + case \Utopia\Validator\WhiteList::class: if ($array) { $validator = $validator->getValidator(); @@ -687,11 +687,11 @@ class OpenAPI3 extends Format } } break; - case 'Appwrite\Utopia\Database\Validator\CompoundUID': + case \Appwrite\Utopia\Database\Validator\CompoundUID::class: $node['schema']['type'] = $validator->getType(); $node['schema']['x-example'] = ($param['example'] ?? '') ?: ''; break; - case 'Appwrite\Utopia\Database\Validator\Operation': + case \Appwrite\Utopia\Database\Validator\Operation::class: if ($array) { $validator = $validator->getValidator(); } diff --git a/src/Appwrite/SDK/Specification/Format/Swagger2.php b/src/Appwrite/SDK/Specification/Format/Swagger2.php index 990c456851..0d66e4d725 100644 --- a/src/Appwrite/SDK/Specification/Format/Swagger2.php +++ b/src/Appwrite/SDK/Specification/Format/Swagger2.php @@ -397,51 +397,51 @@ class Swagger2 extends Format : ''; switch ($base) { - case 'Appwrite\Utopia\Database\Validator\Queries\Base': + case \Appwrite\Utopia\Database\Validator\Queries\Base::class: $class = $base; break; } - if ($class === 'Utopia\Validator\AnyOf') { + if ($class === \Utopia\Validator\AnyOf::class) { $validator = $param['validator']->getValidators()[0]; $class = \get_class($validator); } $array = false; - if ($class === 'Utopia\Validator\ArrayList') { + if ($class === \Utopia\Validator\ArrayList::class) { $array = true; $subclass = \get_class($validator->getValidator()); switch ($subclass) { - case 'Appwrite\Utopia\Database\Validator\Operation': - case 'Utopia\Validator\WhiteList': + case \Appwrite\Utopia\Database\Validator\Operation::class: + case \Utopia\Validator\WhiteList::class: $class = $subclass; break; } } switch ($class) { - case 'Utopia\Validator\Text': - case 'Utopia\Database\Validator\UID': + case \Utopia\Validator\Text::class: + case \Utopia\Database\Validator\UID::class: $node['type'] = $validator->getType(); $node['x-example'] = ($param['example'] ?? '') ?: '<' . \strtoupper(Template::fromCamelCaseToSnake($node['name'])) . '>'; break; - case 'Utopia\Validator\Boolean': + case \Utopia\Validator\Boolean::class: $node['type'] = $validator->getType(); $node['x-example'] = ($param['example'] ?? '') ?: false; break; - case 'Appwrite\Utopia\Database\Validator\CustomId': + case \Appwrite\Utopia\Database\Validator\CustomId::class: if ($sdk->getType() === MethodType::UPLOAD) { $node['x-upload-id'] = true; } $node['type'] = $validator->getType(); $node['x-example'] = ($param['example'] ?? '') ?: '<' . \strtoupper(Template::fromCamelCaseToSnake($node['name'])) . '>'; break; - case 'Utopia\Database\Validator\DatetimeValidator': + case \Utopia\Database\Validator\DatetimeValidator::class: $node['type'] = $validator->getType(); $node['format'] = 'datetime'; $node['x-example'] = ($param['example'] ?? '') ?: Model::TYPE_DATETIME_EXAMPLE; break; - case 'Utopia\Database\Validator\Spatial': + case \Utopia\Database\Validator\Spatial::class: /** @var Spatial $validator */ $node['type'] = 'array'; $node['schema']['items'] = [ @@ -455,19 +455,19 @@ class Swagger2 extends Format Database::VAR_POLYGON => '[[[1, 2], [3, 4], [5, 6], [1, 2]]]', }; break; - case 'Appwrite\Network\Validator\Email': + case \Appwrite\Network\Validator\Email::class: $node['type'] = $validator->getType(); $node['format'] = 'email'; $node['x-example'] = ($param['example'] ?? '') ?: 'email@example.com'; break; - case 'Utopia\Validator\Host': - case 'Utopia\Validator\URL': - case 'Appwrite\Network\Validator\Redirect': + case \Utopia\Validator\Host::class: + case \Utopia\Validator\URL::class: + case \Appwrite\Network\Validator\Redirect::class: $node['type'] = $validator->getType(); $node['format'] = 'url'; $node['x-example'] = ($param['example'] ?? '') ?: 'https://example.com'; break; - case 'Utopia\Validator\ArrayList': + case \Utopia\Validator\ArrayList::class: /** @var ArrayList $validator */ $node['type'] = 'array'; $node['collectionFormat'] = 'multi'; @@ -478,34 +478,34 @@ class Swagger2 extends Format $node['x-example'] = $param['example']; } break; - case 'Utopia\Validator\JSON': - case 'Utopia\Validator\Mock': - case 'Utopia\Validator\Assoc': + case \Utopia\Validator\JSON::class: + case \Utopia\Validator\Mock::class: + case \Utopia\Validator\Assoc::class: $node['type'] = 'object'; $node['default'] = (empty($param['default'])) ? new \stdClass() : $param['default']; $node['x-example'] = ($param['example'] ?? '') ?: '{}'; break; - case 'Utopia\Storage\Validator\File': + case \Utopia\Storage\Validator\File::class: $consumes = ['multipart/form-data']; $node['type'] = 'file'; break; - case 'Appwrite\Functions\Validator\Payload': + case \Appwrite\Functions\Validator\Payload::class: $consumes = ['multipart/form-data']; $node['type'] = 'payload'; break; - case 'Appwrite\Utopia\Database\Validator\Queries\Base': - case 'Utopia\Database\Validator\Queries': - case 'Utopia\Database\Validator\Queries\Document': - case 'Utopia\Database\Validator\Queries\Documents': - case 'Appwrite\Utopia\Database\Validator\Queries\Columns': - case 'Appwrite\Utopia\Database\Validator\Queries\Tables': + case \Appwrite\Utopia\Database\Validator\Queries\Base::class: + case \Utopia\Database\Validator\Queries::class: + case \Utopia\Database\Validator\Queries\Document::class: + case \Utopia\Database\Validator\Queries\Documents::class: + case \Appwrite\Utopia\Database\Validator\Queries\Columns::class: + case \Appwrite\Utopia\Database\Validator\Queries\Tables::class: $node['type'] = 'array'; $node['collectionFormat'] = 'multi'; $node['items'] = [ 'type' => 'string', ]; break; - case 'Utopia\Database\Validator\Permissions': + case \Utopia\Database\Validator\Permissions::class: $node['type'] = $validator->getType(); $node['collectionFormat'] = 'multi'; $node['items'] = [ @@ -513,7 +513,7 @@ class Swagger2 extends Format ]; $node['x-example'] = ($param['example'] ?? '') ?: '["' . Permission::read(Role::any()) . '"]'; break; - case 'Utopia\Database\Validator\Roles': + case \Utopia\Database\Validator\Roles::class: $node['type'] = $validator->getType(); $node['collectionFormat'] = 'multi'; $node['items'] = [ @@ -521,44 +521,44 @@ class Swagger2 extends Format ]; $node['x-example'] = ($param['example'] ?? '') ?: '["' . Role::any()->toString() . '"]'; break; - case 'Appwrite\Auth\Validator\Password': + case \Appwrite\Auth\Validator\Password::class: $node['type'] = $validator->getType(); $node['format'] = 'password'; $node['x-example'] = ($param['example'] ?? '') ?: 'password'; break; - case 'Appwrite\Auth\Validator\Phone': + case \Appwrite\Auth\Validator\Phone::class: $node['type'] = $validator->getType(); $node['format'] = 'phone'; $node['x-example'] = ($param['example'] ?? '') ?: '+12065550100'; break; - case 'Utopia\Validator\Range': + case \Utopia\Validator\Range::class: /** @var Range $validator */ $node['type'] = $validator->getType() === Validator::TYPE_FLOAT ? 'number' : $validator->getType(); $node['format'] = $validator->getType() == Validator::TYPE_INTEGER ? 'int32' : 'float'; $node['x-example'] = ($param['example'] ?? '') ?: $validator->getMin(); break; - case 'Utopia\Validator\Integer': + case \Utopia\Validator\Integer::class: $node['type'] = $validator->getType(); $node['format'] = $validator->getFormat(); if (!empty($param['example'])) { $node['x-example'] = $param['example']; } break; - case 'Utopia\Validator\Numeric': - case 'Utopia\Validator\FloatValidator': + case \Utopia\Validator\Numeric::class: + case \Utopia\Validator\FloatValidator::class: $node['type'] = 'number'; $node['format'] = 'float'; if (!empty($param['example'])) { $node['x-example'] = $param['example']; } break; - case 'Utopia\Validator\Length': + case \Utopia\Validator\Length::class: $node['type'] = $validator->getType(); if (!empty($param['example'])) { $node['x-example'] = $param['example']; } break; - case 'Utopia\Validator\WhiteList': + case \Utopia\Validator\WhiteList::class: if ($array) { $validator = $validator->getValidator(); @@ -665,11 +665,11 @@ class Swagger2 extends Format } } break; - case 'Appwrite\Utopia\Database\Validator\CompoundUID': + case \Appwrite\Utopia\Database\Validator\CompoundUID::class: $node['type'] = $validator->getType(); $node['x-example'] = ($param['example'] ?? '') ?: ''; break; - case 'Appwrite\Utopia\Database\Validator\Operation': + case \Appwrite\Utopia\Database\Validator\Operation::class: if ($array) { $validator = $validator->getValidator(); } diff --git a/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php b/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php index bcb87e92d5..58bb92f643 100644 --- a/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php +++ b/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php @@ -7626,6 +7626,267 @@ trait DatabasesBase $this->assertEquals(200, $update['headers']['status-code']); } + /** + * @depends testCreateDatabase + */ + public function testInvalidRelationshipDocumentId(array $data): void + { + $databaseId = $data['databaseId']; + + // Create parent table + $parentTable = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'tableId' => ID::unique(), + 'name' => 'ParentTable', + ]); + $this->assertEquals(201, $parentTable['headers']['status-code']); + $parentTableId = $parentTable['body']['$id']; + + // Create child table + $childTable = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'tableId' => ID::unique(), + 'name' => 'ChildTable', + ]); + $this->assertEquals(201, $childTable['headers']['status-code']); + $childTableId = $childTable['body']['$id']; + + // Add string column to parent + $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $parentTableId . '/columns/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'name', + 'size' => 255, + 'required' => false, + ]); + + // Add string column to child + $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $childTableId . '/columns/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'title', + 'size' => 255, + 'required' => false, + ]); + + // Create one-to-many relationship + $relationship = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $parentTableId . '/columns/relationship', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'relatedTableId' => $childTableId, + 'type' => Database::RELATION_ONE_TO_MANY, + 'twoWay' => false, + 'key' => 'children', + ]); + $this->assertEquals(202, $relationship['headers']['status-code']); + + // Wait for relationship column to be available + $this->assertEventually(function () use ($databaseId, $parentTableId) { + $columns = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $parentTableId . '/columns', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + $columnKeys = array_column($columns['body']['columns'], 'key'); + $this->assertContains('children', $columnKeys, "Relationship column 'children' not found in table {$parentTableId} of database {$databaseId}"); + }, 2000, 200); + + // ID too long (>36 chars) should fail + $response = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $parentTableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'rowId' => ID::unique(), + 'data' => [ + 'name' => 'Parent 1', + 'children' => [ + [ + '$id' => 'this_id_is_way_too_long_and_should_fail_validation_check', + 'title' => 'Child 1', + ], + ], + ], + ]); + $this->assertEquals(400, $response['headers']['status-code']); + + // ID with invalid characters should fail + $response = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $parentTableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'rowId' => ID::unique(), + 'data' => [ + 'name' => 'Parent 2', + 'children' => [ + [ + '$id' => 'invalid@id#with$special%chars', + 'title' => 'Child 2', + ], + ], + ], + ]); + $this->assertEquals(400, $response['headers']['status-code']); + + // ID starting with underscore should fail + $response = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $parentTableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'rowId' => ID::unique(), + 'data' => [ + 'name' => 'Parent 3', + 'children' => [ + [ + '$id' => '_startsWithUnderscore', + 'title' => 'Child 3', + ], + ], + ], + ]); + $this->assertEquals(400, $response['headers']['status-code']); + + // Valid ID should succeed + $response = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $parentTableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'rowId' => ID::unique(), + 'data' => [ + 'name' => 'Parent 4', + 'children' => [ + [ + '$id' => 'valid-id-123', + 'title' => 'Child 4', + ], + ], + ], + ]); + $this->assertEquals(201, $response['headers']['status-code']); + $parentRowId = $response['body']['$id']; + + // Update with invalid relationship ID should fail + $response = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/' . $parentTableId . '/rows/' . $parentRowId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'data' => [ + 'children' => [ + [ + '$id' => 'another@invalid#id', + 'title' => 'Child 5', + ], + ], + ], + ]); + $this->assertEquals(400, $response['headers']['status-code']); + + // Invalid string relation ID should fail + $response = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $parentTableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'rowId' => ID::unique(), + 'data' => [ + 'name' => 'Parent 6', + 'children' => [ + 'invalid@string#id', + ], + ], + ]); + $this->assertEquals(400, $response['headers']['status-code']); + + // Integer as relation value should fail + $response = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $parentTableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'rowId' => ID::unique(), + 'data' => [ + 'name' => 'Parent 7', + 'children' => [ + 12345, + ], + ], + ]); + $this->assertEquals(400, $response['headers']['status-code']); + + // unique() as $id should succeed + $response = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $parentTableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'rowId' => ID::unique(), + 'data' => [ + 'name' => 'Parent 8', + 'children' => [ + [ + '$id' => 'unique()', + 'title' => 'Child 8', + ], + ], + ], + ]); + $this->assertEquals(201, $response['headers']['status-code']); + + // Empty string as $id should fail + $response = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $parentTableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'rowId' => ID::unique(), + 'data' => [ + 'name' => 'Parent 9', + 'children' => [ + [ + '$id' => '', + 'title' => 'Child 9', + ], + ], + ], + ]); + $this->assertEquals(400, $response['headers']['status-code']); + + // Valid ID with allowed special chars (hyphen, period) should succeed + $response = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $parentTableId . '/rows', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'rowId' => ID::unique(), + 'data' => [ + 'name' => 'Parent 10', + 'children' => [ + [ + '$id' => 'valid.id-with_chars', + 'title' => 'Child 10', + ], + ], + ], + ]); + $this->assertEquals(201, $response['headers']['status-code']); + } + /** * @depends testCreateDatabase */