diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 164599f911..cebdc02163 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,7 +8,15 @@ env: IMAGE: appwrite-dev CACHE_KEY: appwrite-dev-${{ github.event.pull_request.head.sha }} -on: [ pull_request ] +on: + pull_request: + workflow_dispatch: + inputs: + response_format: + description: 'Response format version to test (e.g., 1.5.0, 1.4.0)' + required: false + type: string + default: '' jobs: check_database_changes: @@ -100,7 +108,10 @@ jobs: run: docker compose exec -T appwrite vars - name: Run Unit Tests - run: docker compose exec appwrite test /usr/src/code/tests/unit + run: | + docker compose exec \ + -e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \ + appwrite test /usr/src/code/tests/unit e2e_general_test: name: E2E General Test @@ -132,7 +143,10 @@ jobs: done - name: Run General Tests - run: docker compose exec -T appwrite test /usr/src/code/tests/e2e/General --debug + run: | + docker compose exec -T \ + -e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \ + appwrite test /usr/src/code/tests/e2e/General --debug - name: Failure Logs if: failure() @@ -208,6 +222,7 @@ jobs: docker compose exec -T \ -e _APP_DATABASE_SHARED_TABLES \ -e _APP_DATABASE_SHARED_TABLES_V1 \ + -e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \ appwrite test /usr/src/code/tests/e2e/Services/${{ matrix.service }} --debug --exclude-group devKeys,screenshots - name: Failure Logs @@ -296,6 +311,7 @@ jobs: docker compose exec -T \ -e _APP_DATABASE_SHARED_TABLES \ -e _APP_DATABASE_SHARED_TABLES_V1 \ + -e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \ appwrite test /usr/src/code/tests/e2e/Services/${{ matrix.service }} --debug --exclude-group devKeys,screenshots - name: Failure Logs @@ -337,6 +353,7 @@ jobs: docker compose exec -T \ -e _APP_DATABASE_SHARED_TABLES \ -e _APP_DATABASE_SHARED_TABLES_V1 \ + -e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \ appwrite test /usr/src/code/tests/e2e/Services/Projects --debug --group=devKeys - name: Failure Logs @@ -392,6 +409,7 @@ jobs: docker compose exec -T \ -e _APP_DATABASE_SHARED_TABLES \ -e _APP_DATABASE_SHARED_TABLES_V1 \ + -e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \ appwrite test /usr/src/code/tests/e2e/Services/Projects --debug --group=devKeys - name: Failure Logs @@ -434,6 +452,7 @@ jobs: docker compose exec -T \ -e _APP_DATABASE_SHARED_TABLES \ -e _APP_DATABASE_SHARED_TABLES_V1 \ + -e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \ appwrite test /usr/src/code/tests/e2e/Services/Sites --debug --group=screenshots - name: Failure Logs @@ -490,6 +509,7 @@ jobs: docker compose exec -T \ -e _APP_DATABASE_SHARED_TABLES \ -e _APP_DATABASE_SHARED_TABLES_V1 \ + -e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \ appwrite test /usr/src/code/tests/e2e/Services/Sites --debug --group=screenshots - name: Failure Logs @@ -498,4 +518,4 @@ jobs: echo "=== Appwrite Worker Builds Logs ===" docker compose logs appwrite-worker-builds echo "=== OpenRuntimes Executor Logs ===" - docker compose logs openruntimes-executor + docker compose logs openruntimes-executor \ No newline at end of file diff --git a/app/config/errors.php b/app/config/errors.php index 23df60f4ba..c4617f1cde 100644 --- a/app/config/errors.php +++ b/app/config/errors.php @@ -435,6 +435,11 @@ return [ 'description' => 'The requested favicon could not be found.', 'code' => 404, ], + Exception::AVATAR_SVG_SANITIZATION_FAILED => [ + 'name' => Exception::AVATAR_SVG_SANITIZATION_FAILED, + 'description' => 'SVG sanitization failed.', + 'code' => 400, + ], /** Storage */ Exception::STORAGE_FILE_ALREADY_EXISTS => [ 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 e3fd3bd04a..0841dabbd1 100644 --- a/app/config/specs/open-api3-1.8.x-client.json +++ b/app/config/specs/open-api3-1.8.x-client.json @@ -9948,17 +9948,20 @@ "type": "integer", "description": "Row automatically incrementing ID.", "x-example": 1, - "format": "int32" + "format": "int32", + "readOnly": true }, "$tableId": { "type": "string", "description": "Table ID.", - "x-example": "5e5ea5c15117e" + "x-example": "5e5ea5c15117e", + "readOnly": true }, "$databaseId": { "type": "string", "description": "Database ID.", - "x-example": "5e5ea5c15117e" + "x-example": "5e5ea5c15117e", + "readOnly": true }, "$createdAt": { "type": "string", 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 29c45692ec..31cb8e5105 100644 --- a/app/config/specs/open-api3-1.8.x-console.json +++ b/app/config/specs/open-api3-1.8.x-console.json @@ -47120,17 +47120,20 @@ "type": "integer", "description": "Row automatically incrementing ID.", "x-example": 1, - "format": "int32" + "format": "int32", + "readOnly": true }, "$tableId": { "type": "string", "description": "Table ID.", - "x-example": "5e5ea5c15117e" + "x-example": "5e5ea5c15117e", + "readOnly": true }, "$databaseId": { "type": "string", "description": "Database ID.", - "x-example": "5e5ea5c15117e" + "x-example": "5e5ea5c15117e", + "readOnly": true }, "$createdAt": { "type": "string", 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 60c07da12a..0fe1cf72ae 100644 --- a/app/config/specs/open-api3-1.8.x-server.json +++ b/app/config/specs/open-api3-1.8.x-server.json @@ -35978,17 +35978,20 @@ "type": "integer", "description": "Row automatically incrementing ID.", "x-example": 1, - "format": "int32" + "format": "int32", + "readOnly": true }, "$tableId": { "type": "string", "description": "Table ID.", - "x-example": "5e5ea5c15117e" + "x-example": "5e5ea5c15117e", + "readOnly": true }, "$databaseId": { "type": "string", "description": "Database ID.", - "x-example": "5e5ea5c15117e" + "x-example": "5e5ea5c15117e", + "readOnly": true }, "$createdAt": { "type": "string", diff --git a/app/config/specs/open-api3-latest-client.json b/app/config/specs/open-api3-latest-client.json index e3fd3bd04a..0841dabbd1 100644 --- a/app/config/specs/open-api3-latest-client.json +++ b/app/config/specs/open-api3-latest-client.json @@ -9948,17 +9948,20 @@ "type": "integer", "description": "Row automatically incrementing ID.", "x-example": 1, - "format": "int32" + "format": "int32", + "readOnly": true }, "$tableId": { "type": "string", "description": "Table ID.", - "x-example": "5e5ea5c15117e" + "x-example": "5e5ea5c15117e", + "readOnly": true }, "$databaseId": { "type": "string", "description": "Database ID.", - "x-example": "5e5ea5c15117e" + "x-example": "5e5ea5c15117e", + "readOnly": true }, "$createdAt": { "type": "string", diff --git a/app/config/specs/open-api3-latest-console.json b/app/config/specs/open-api3-latest-console.json index 29c45692ec..31cb8e5105 100644 --- a/app/config/specs/open-api3-latest-console.json +++ b/app/config/specs/open-api3-latest-console.json @@ -47120,17 +47120,20 @@ "type": "integer", "description": "Row automatically incrementing ID.", "x-example": 1, - "format": "int32" + "format": "int32", + "readOnly": true }, "$tableId": { "type": "string", "description": "Table ID.", - "x-example": "5e5ea5c15117e" + "x-example": "5e5ea5c15117e", + "readOnly": true }, "$databaseId": { "type": "string", "description": "Database ID.", - "x-example": "5e5ea5c15117e" + "x-example": "5e5ea5c15117e", + "readOnly": true }, "$createdAt": { "type": "string", diff --git a/app/config/specs/open-api3-latest-server.json b/app/config/specs/open-api3-latest-server.json index 60c07da12a..0fe1cf72ae 100644 --- a/app/config/specs/open-api3-latest-server.json +++ b/app/config/specs/open-api3-latest-server.json @@ -35978,17 +35978,20 @@ "type": "integer", "description": "Row automatically incrementing ID.", "x-example": 1, - "format": "int32" + "format": "int32", + "readOnly": true }, "$tableId": { "type": "string", "description": "Table ID.", - "x-example": "5e5ea5c15117e" + "x-example": "5e5ea5c15117e", + "readOnly": true }, "$databaseId": { "type": "string", "description": "Database ID.", - "x-example": "5e5ea5c15117e" + "x-example": "5e5ea5c15117e", + "readOnly": true }, "$createdAt": { "type": "string", diff --git a/app/config/specs/swagger2-1.8.x-client.json b/app/config/specs/swagger2-1.8.x-client.json index 3a6152b6ad..efeb167a24 100644 --- a/app/config/specs/swagger2-1.8.x-client.json +++ b/app/config/specs/swagger2-1.8.x-client.json @@ -9944,17 +9944,20 @@ "type": "integer", "description": "Row automatically incrementing ID.", "x-example": 1, - "format": "int32" + "format": "int32", + "readOnly": true }, "$tableId": { "type": "string", "description": "Table ID.", - "x-example": "5e5ea5c15117e" + "x-example": "5e5ea5c15117e", + "readOnly": true }, "$databaseId": { "type": "string", "description": "Database ID.", - "x-example": "5e5ea5c15117e" + "x-example": "5e5ea5c15117e", + "readOnly": true }, "$createdAt": { "type": "string", diff --git a/app/config/specs/swagger2-1.8.x-console.json b/app/config/specs/swagger2-1.8.x-console.json index 5e121e8840..788279b595 100644 --- a/app/config/specs/swagger2-1.8.x-console.json +++ b/app/config/specs/swagger2-1.8.x-console.json @@ -47166,17 +47166,20 @@ "type": "integer", "description": "Row automatically incrementing ID.", "x-example": 1, - "format": "int32" + "format": "int32", + "readOnly": true }, "$tableId": { "type": "string", "description": "Table ID.", - "x-example": "5e5ea5c15117e" + "x-example": "5e5ea5c15117e", + "readOnly": true }, "$databaseId": { "type": "string", "description": "Database ID.", - "x-example": "5e5ea5c15117e" + "x-example": "5e5ea5c15117e", + "readOnly": true }, "$createdAt": { "type": "string", diff --git a/app/config/specs/swagger2-1.8.x-server.json b/app/config/specs/swagger2-1.8.x-server.json index e4ffbbe973..9293ce6928 100644 --- a/app/config/specs/swagger2-1.8.x-server.json +++ b/app/config/specs/swagger2-1.8.x-server.json @@ -36115,17 +36115,20 @@ "type": "integer", "description": "Row automatically incrementing ID.", "x-example": 1, - "format": "int32" + "format": "int32", + "readOnly": true }, "$tableId": { "type": "string", "description": "Table ID.", - "x-example": "5e5ea5c15117e" + "x-example": "5e5ea5c15117e", + "readOnly": true }, "$databaseId": { "type": "string", "description": "Database ID.", - "x-example": "5e5ea5c15117e" + "x-example": "5e5ea5c15117e", + "readOnly": true }, "$createdAt": { "type": "string", diff --git a/app/config/specs/swagger2-latest-client.json b/app/config/specs/swagger2-latest-client.json index 3a6152b6ad..efeb167a24 100644 --- a/app/config/specs/swagger2-latest-client.json +++ b/app/config/specs/swagger2-latest-client.json @@ -9944,17 +9944,20 @@ "type": "integer", "description": "Row automatically incrementing ID.", "x-example": 1, - "format": "int32" + "format": "int32", + "readOnly": true }, "$tableId": { "type": "string", "description": "Table ID.", - "x-example": "5e5ea5c15117e" + "x-example": "5e5ea5c15117e", + "readOnly": true }, "$databaseId": { "type": "string", "description": "Database ID.", - "x-example": "5e5ea5c15117e" + "x-example": "5e5ea5c15117e", + "readOnly": true }, "$createdAt": { "type": "string", diff --git a/app/config/specs/swagger2-latest-console.json b/app/config/specs/swagger2-latest-console.json index 5e121e8840..788279b595 100644 --- a/app/config/specs/swagger2-latest-console.json +++ b/app/config/specs/swagger2-latest-console.json @@ -47166,17 +47166,20 @@ "type": "integer", "description": "Row automatically incrementing ID.", "x-example": 1, - "format": "int32" + "format": "int32", + "readOnly": true }, "$tableId": { "type": "string", "description": "Table ID.", - "x-example": "5e5ea5c15117e" + "x-example": "5e5ea5c15117e", + "readOnly": true }, "$databaseId": { "type": "string", "description": "Database ID.", - "x-example": "5e5ea5c15117e" + "x-example": "5e5ea5c15117e", + "readOnly": true }, "$createdAt": { "type": "string", diff --git a/app/config/specs/swagger2-latest-server.json b/app/config/specs/swagger2-latest-server.json index e4ffbbe973..9293ce6928 100644 --- a/app/config/specs/swagger2-latest-server.json +++ b/app/config/specs/swagger2-latest-server.json @@ -36115,17 +36115,20 @@ "type": "integer", "description": "Row automatically incrementing ID.", "x-example": 1, - "format": "int32" + "format": "int32", + "readOnly": true }, "$tableId": { "type": "string", "description": "Table ID.", - "x-example": "5e5ea5c15117e" + "x-example": "5e5ea5c15117e", + "readOnly": true }, "$databaseId": { "type": "string", "description": "Database ID.", - "x-example": "5e5ea5c15117e" + "x-example": "5e5ea5c15117e", + "readOnly": true }, "$createdAt": { "type": "string", diff --git a/app/controllers/api/avatars.php b/app/controllers/api/avatars.php index 785324739b..90364d997e 100644 --- a/app/controllers/api/avatars.php +++ b/app/controllers/api/avatars.php @@ -474,7 +474,7 @@ App::get('/v1/avatars/favicon') $sanitizer->minify(true); $cleanSvg = $sanitizer->sanitize($data); if ($cleanSvg === false) { - throw new \Exception('SVG sanitization failed'); + throw new Exception(Exception::AVATAR_SVG_SANITIZATION_FAILED); } $response ->addHeader('Cache-Control', 'private, max-age=2592000') // 30 days diff --git a/app/controllers/api/messaging.php b/app/controllers/api/messaging.php index abc1f9bc52..d22c5cb2c2 100644 --- a/app/controllers/api/messaging.php +++ b/app/controllers/api/messaging.php @@ -3011,7 +3011,7 @@ App::post('/v1/messaging/messages/email') ->inject('project') ->inject('queueForMessaging') ->inject('response') - ->action(function (string $messageId, string $subject, string $content, array $topics, array $users, array $targets, array $cc, array $bcc, array $attachments, bool $draft, bool $html, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response) { + ->action(function (string $messageId, string $subject, string $content, ?array $topics, ?array $users, ?array $targets, ?array $cc, ?array $bcc, ?array $attachments, bool $draft, bool $html, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response) { $messageId = $messageId == 'unique()' ? ID::unique() : $messageId; @@ -3184,7 +3184,7 @@ App::post('/v1/messaging/messages/sms') ->inject('project') ->inject('queueForMessaging') ->inject('response') - ->action(function (string $messageId, string $content, array $topics, array $users, array $targets, bool $draft, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response) { + ->action(function (string $messageId, string $content, ?array $topics, ?array $users, ?array $targets, bool $draft, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response) { $messageId = $messageId == 'unique()' ? ID::unique() : $messageId; @@ -3319,7 +3319,7 @@ App::post('/v1/messaging/messages/push') ->inject('project') ->inject('queueForMessaging') ->inject('response') - ->action(function (string $messageId, string $title, string $body, array $topics, array $users, array $targets, ?array $data, string $action, string $image, string $icon, string $sound, string $color, string $tag, int $badge, bool $draft, ?string $scheduledAt, bool $contentAvailable, bool $critical, string $priority, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response) { + ->action(function (string $messageId, string $title, string $body, ?array $topics, ?array $users, ?array $targets, ?array $data, string $action, string $image, string $icon, string $sound, string $color, string $tag, int $badge, bool $draft, ?string $scheduledAt, bool $contentAvailable, bool $critical, string $priority, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response) { $messageId = $messageId == 'unique()' ? ID::unique() : $messageId; diff --git a/app/controllers/general.php b/app/controllers/general.php index 22954afd96..40ce66b574 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -1060,26 +1060,6 @@ App::init() $response->addHeader('Access-Control-Allow-Origin', '*'); } - /** - * Deprecation Warning - */ - /** @var \Appwrite\SDK\Method $sdk */ - $sdk = $route->getLabel('sdk', false); - $deprecationWarning = 'This route is deprecated. See the updated documentation for improved compatibility and migration details.'; - $sdkItems = is_array($sdk) ? $sdk : (!empty($sdk) ? [$sdk] : []); - if (!empty($sdkItems) && count($sdkItems) > 0) { - $allDeprecated = true; - foreach ($sdkItems as $sdkItem) { - if (!$sdkItem->isDeprecated()) { - $allDeprecated = false; - break; - } - } - if ($allDeprecated) { - $warnings[] = $deprecationWarning; - } - } - if (!empty($warnings)) { $response->addHeader('X-Appwrite-Warning', implode(';', $warnings)); } diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 7742aac18f..6dcb99b56f 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -29,7 +29,6 @@ use Utopia\Database\DateTime; use Utopia\Database\Document; use Utopia\Database\Helpers\Role; use Utopia\Database\Validator\Authorization; -use Utopia\Queue\Broker\Pool as BrokerPool; use Utopia\Queue\Publisher; use Utopia\System\System; use Utopia\Telemetry\Adapter as Telemetry; @@ -416,6 +415,7 @@ App::init() ->inject('user') ->inject('publisher') ->inject('publisherFunctions') + ->inject('publisherWebhooks') ->inject('queueForEvents') ->inject('queueForMessaging') ->inject('queueForAudits') @@ -431,7 +431,7 @@ App::init() ->inject('plan') ->inject('devKey') ->inject('telemetry') - ->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Publisher $publisher, BrokerPool $publisherFunctions, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, StatsUsage $queueForStatsUsage, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry) use ($usageDatabaseListener, $eventDatabaseListener) { + ->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Publisher $publisher, Publisher $publisherFunctions, Publisher $publisherWebhooks, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, StatsUsage $queueForStatsUsage, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry) use ($usageDatabaseListener, $eventDatabaseListener) { $route = $utopia->getRoute(); @@ -544,7 +544,7 @@ App::init() // from overwriting the events that are supposed to be triggered in the shutdown hook. $queueForEventsClone = new Event($publisher); $queueForFunctions = new Func($publisherFunctions); - $queueForWebhooks = new Webhook($publisher); + $queueForWebhooks = new Webhook($publisherWebhooks); $queueForRealtime = new Realtime(); $dbForProject diff --git a/app/init/resources.php b/app/init/resources.php index 380087cf43..d4f0433447 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -84,25 +84,28 @@ App::setResource('localeCodes', function () { App::setResource('publisher', function (Group $pools) { return new BrokerPool(publisher: $pools->get('publisher')); }, ['pools']); -App::setResource('publisherDatabases', function (BrokerPool $publisher) { +App::setResource('publisherDatabases', function (Publisher $publisher) { return $publisher; }, ['publisher']); -App::setResource('publisherFunctions', function (BrokerPool $publisher) { +App::setResource('publisherFunctions', function (Publisher $publisher) { return $publisher; }, ['publisher']); -App::setResource('publisherMigrations', function (BrokerPool $publisher) { +App::setResource('publisherMigrations', function (Publisher $publisher) { return $publisher; }, ['publisher']); -App::setResource('publisherStatsUsage', function (BrokerPool $publisher) { +App::setResource('publisherStatsUsage', function (Publisher $publisher) { return $publisher; }, ['publisher']); -App::setResource('publisherMails', function (BrokerPool $publisher) { +App::setResource('publisherMails', function (Publisher $publisher) { return $publisher; }, ['publisher']); -App::setResource('publisherDeletes', function (BrokerPool $publisher) { +App::setResource('publisherDeletes', function (Publisher $publisher) { return $publisher; }, ['publisher']); -App::setResource('publisherMessaging', function (BrokerPool $publisher) { +App::setResource('publisherMessaging', function (Publisher $publisher) { + return $publisher; +}, ['publisher']); +App::setResource('publisherWebhooks', function (Publisher $publisher) { return $publisher; }, ['publisher']); App::setResource('queueForMessaging', function (Publisher $publisher) { diff --git a/composer.lock b/composer.lock index 1a776c089e..3bf17bc228 100644 --- a/composer.lock +++ b/composer.lock @@ -4109,16 +4109,16 @@ }, { "name": "utopia-php/migration", - "version": "1.0.1", + "version": "1.0.0", "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "38171023efd3abe650d2abc5ac65f5df52311da6" + "reference": "0e4499d9dd2c90c2be188cc5fb7a32d9a892b569" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/38171023efd3abe650d2abc5ac65f5df52311da6", - "reference": "38171023efd3abe650d2abc5ac65f5df52311da6", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/0e4499d9dd2c90c2be188cc5fb7a32d9a892b569", + "reference": "0e4499d9dd2c90c2be188cc5fb7a32d9a892b569", "shasum": "" }, "require": { @@ -4159,9 +4159,9 @@ ], "support": { "issues": "https://github.com/utopia-php/migration/issues", - "source": "https://github.com/utopia-php/migration/tree/1.0.1" + "source": "https://github.com/utopia-php/migration/tree/1.0.0" }, - "time": "2025-08-28T13:41:25+00:00" + "time": "2025-08-13T09:15:53+00:00" }, { "name": "utopia-php/orchestration", diff --git a/src/Appwrite/Event/StatsUsage.php b/src/Appwrite/Event/StatsUsage.php index f9e03c7c3d..f6b1d695f4 100644 --- a/src/Appwrite/Event/StatsUsage.php +++ b/src/Appwrite/Event/StatsUsage.php @@ -85,11 +85,4 @@ class StatsUsage extends Event }), ]; } - - public function reset(): Event - { - $this->metrics = []; - parent::reset(); - return $this; - } } diff --git a/src/Appwrite/Extend/Exception.php b/src/Appwrite/Extend/Exception.php index 8eded2dbe0..9849352e56 100644 --- a/src/Appwrite/Extend/Exception.php +++ b/src/Appwrite/Extend/Exception.php @@ -134,6 +134,7 @@ class Exception extends \Exception public const AVATAR_IMAGE_NOT_FOUND = 'avatar_image_not_found'; public const AVATAR_REMOTE_URL_FAILED = 'avatar_remote_url_failed'; public const AVATAR_ICON_NOT_FOUND = 'avatar_icon_not_found'; + public const AVATAR_SVG_SANITIZATION_FAILED = 'avatar_svg_sanitization_failed'; /** Storage */ public const STORAGE_FILE_ALREADY_EXISTS = 'storage_file_already_exists'; 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 78df15b0c1..d1d0738990 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 @@ -27,9 +27,18 @@ abstract class Action extends AppwriteAction $this->context = ROWS; } - // Use the same helper method to ensure consistency $contextId = '$' . $this->getCollectionsEventsContext() . 'Id'; - $this->removableAttributes = ['$databaseId', $contextId, '$sequence']; + $this->removableAttributes = [ + '*' => [ + '$sequence', + '$databaseId', + $contextId, + ], + 'privileged' => [ + '$createdAt', + '$updatedAt', + ], + ]; return parent::setHttpPath($path); } @@ -200,11 +209,19 @@ abstract class Action extends AppwriteAction * Remove configured removable attributes from a document. * Used for relationship path handling to remove API-specific attributes. */ - protected function removeReadonlyAttributes(Document $document): void - { - foreach ($this->removableAttributes as $attribute) { - $document->removeAttribute($attribute); + protected function removeReadonlyAttributes( + Document|array $document, + bool $privileged = false, + ): Document|array { + foreach ($this->removableAttributes['*'] as $attribute) { + unset($document[$attribute]); } + if (!$privileged) { + foreach ($this->removableAttributes['privileged'] ?? [] as $attribute) { + unset($document[$attribute]); + } + } + return $document; } /** diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Update.php index a9f9c3f76d..0f0ae14020 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Update.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Update.php @@ -127,8 +127,7 @@ class Update extends Action } } - // Remove sequence if set - unset($document['$sequence']); + $data = $this->removeReadonlyAttributes($data, privileged: true); $documents = []; diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Upsert.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Upsert.php index a6f27637e3..3c6e5ddc57 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Upsert.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Upsert.php @@ -100,6 +100,7 @@ class Upsert extends Action } foreach ($documents as $key => $document) { + $document = $this->removeReadonlyAttributes($document, privileged: true); $documents[$key] = new Document($document); } 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 04c90c4ec1..d274e1f128 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 @@ -254,7 +254,7 @@ class Create extends Action $operations = 0; - $checkPermissions = function (Document $collection, Document $document, string $permission) use (&$checkPermissions, $dbForProject, $database, &$operations) { + $checkPermissions = function (Document $collection, Document $document, string $permission) use ($isAPIKey, $isPrivilegedUser, &$checkPermissions, $dbForProject, $database, &$operations) { $operations++; $documentSecurity = $collection->getAttribute('documentSecurity', false); @@ -307,6 +307,8 @@ class Create extends Action $relation = new Document($relation); } if ($relation instanceof Document) { + $relation = $this->removeReadonlyAttributes($relation, $isAPIKey || $isPrivilegedUser); + $current = Authorization::skip( fn () => $dbForProject->getDocument('database_' . $database->getSequence() . '_collection_' . $relatedCollection->getSequence(), $relation->getId()) ); @@ -318,7 +320,6 @@ class Create extends Action $relation['$id'] = ID::unique(); } } else { - $this->removeReadonlyAttributes($relation); $relation->setAttribute('$collection', $relatedCollection->getId()); $type = Database::PERMISSION_UPDATE; } @@ -351,27 +352,12 @@ class Create extends Action } } - // Remove sequence if set - unset($document['$sequence']); - // Assign a unique ID if needed, otherwise use the provided ID. $document['$id'] = $sourceId === 'unique()' ? ID::unique() : $sourceId; - - // Allowing to add createdAt and updatedAt timestamps if server side(api key - if (!$isAPIKey && !$isPrivilegedUser) { - if (isset($document['$createdAt'])) { - throw new Exception($this->getInvalidStructureException(), 'Attribute "$createdAt" can not be modified. Please use a server SDK with an API key to modify server attributes.'); - } - - if (isset($document['$updatedAt'])) { - throw new Exception($this->getInvalidStructureException(), 'Attribute "$updatedAt" can not be modified. Please use a server SDK with an API key to modify server attributes.'); - } - } - + $document = $this->removeReadonlyAttributes($document, $isAPIKey || $isPrivilegedUser); $document = new Document($document); $setPermissions($document, $permissions); $checkPermissions($collection, $document, Database::PERMISSION_CREATE); - return $document; }, $documents); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php index 4ae1624f73..f34b4630c2 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php @@ -117,6 +117,7 @@ class Delete extends Action } $collectionsCache = []; + $this->processDocument( database: $database, collection: $collection, 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 334bcb8448..8382bdd5e9 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 @@ -109,16 +109,6 @@ class Update extends Action throw new Exception($this->getParentNotFoundException()); } - // Allowing to add createdAt and updatedAt timestamps if server side(api key) - if (!$isAPIKey && !$isPrivilegedUser) { - if (isset($data['$createdAt'])) { - throw new Exception($this->getInvalidStructureException(), 'Attribute "$createdAt" can not be modified. Please use a server SDK with an API key to modify server attributes.'); - } - - if (isset($data['$updatedAt'])) { - throw new Exception($this->getInvalidStructureException(), 'Attribute "$updatedAt" can not be modified. Please use a server SDK with an API key to modify server attributes.'); - } - } // Read permission should not be required for update /** @var Document $document */ $document = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $documentId)); @@ -159,17 +149,14 @@ class Update extends Action $permissions = $document->getPermissions() ?? []; } - // Remove sequence if set - unset($document['$sequence']); - $data['$id'] = $documentId; $data['$permissions'] = $permissions; + $data = $this->removeReadonlyAttributes($data, $isAPIKey || $isPrivilegedUser); $newDocument = new Document($data); $operations = 0; - $setCollection = (function (Document $collection, Document $document) use (&$setCollection, $dbForProject, $database, &$operations) { - + $setCollection = (function (Document $collection, Document $document) use ($isAPIKey, $isPrivilegedUser, &$setCollection, $dbForProject, $database, &$operations) { $operations++; $relationships = \array_filter( @@ -208,11 +195,13 @@ class Update extends Action $relation = new Document($relation); } if ($relation instanceof Document) { + $relation = $this->removeReadonlyAttributes($relation, $isAPIKey || $isPrivilegedUser); + $oldDocument = Authorization::skip(fn () => $dbForProject->getDocument( 'database_' . $database->getSequence() . '_collection_' . $relatedCollection->getSequence(), $relation->getId() )); - $this->removeReadonlyAttributes($relation); + // Attribute $collection is required for Utopia. $relation->setAttribute( '$collection', 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 6027a20c41..54b1cad950 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 @@ -121,7 +121,8 @@ class Upsert extends Action ]; $permissions = Permission::aggregate($permissions, $allowedPermissions); - // if no permission, upsert permission from the old document if present (update scenario) else add default permission (create scenario) + + // If no permission, upsert permission from the old document if present (update scenario) else add default permission (create scenario) if (\is_null($permissions)) { $oldDocument = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $documentId)); if ($oldDocument->isEmpty()) { @@ -157,24 +158,14 @@ class Upsert extends Action } } } - // Allowing to add createdAt and updatedAt timestamps if server side(api key) - if (!$isAPIKey && !$isPrivilegedUser) { - if (isset($data['$createdAt'])) { - throw new Exception($this->getInvalidStructureException(), 'Attribute "$createdAt" can not be modified. Please use a server SDK with an API key to modify server attributes.'); - } - - if (isset($data['$updatedAt'])) { - throw new Exception($this->getInvalidStructureException(), 'Attribute "$updatedAt" can not be modified. Please use a server SDK with an API key to modify server attributes.'); - } - } $data['$id'] = $documentId; $data['$permissions'] = $permissions ?? []; + $data = $this->removeReadonlyAttributes($data, $isAPIKey || $isPrivilegedUser); $newDocument = new Document($data); $operations = 0; - $setCollection = (function (Document $collection, Document $document) use (&$setCollection, $dbForProject, $database, &$operations) { - + $setCollection = (function (Document $collection, Document $document) use ($isAPIKey, $isPrivilegedUser, &$setCollection, $dbForProject, $database, &$operations) { $operations++; $relationships = \array_filter( @@ -213,11 +204,13 @@ class Upsert extends Action $relation = new Document($relation); } if ($relation instanceof Document) { + $relation = $this->removeReadonlyAttributes($relation, $isAPIKey || $isPrivilegedUser); + $oldDocument = Authorization::skip(fn () => $dbForProject->getDocument( 'database_' . $database->getSequence() . '_collection_' . $relatedCollection->getSequence(), $relation->getId() )); - $this->removeReadonlyAttributes($relation); + // Attribute $collection is required for Utopia. $relation->setAttribute( '$collection', diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php index 57ca550c18..9c8405cf18 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php @@ -176,7 +176,7 @@ class XList extends Action } // Check which removable attributes are explicitly requested - foreach ($this->removableAttributes as $attribute) { + foreach ($this->removableAttributes['*'] as $attribute) { if (\in_array($attribute, $values, true)) { $requestedAttributes[$attribute] = true; } @@ -186,7 +186,7 @@ class XList extends Action if (!$hasWildcard) { foreach ($documents as $document) { // Remove attributes that are not explicitly requested - foreach ($this->removableAttributes as $attribute) { + foreach ($this->removableAttributes['*'] as $attribute) { if (!isset($requestedAttributes[$attribute])) { $document->removeAttribute($attribute); } diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index ff25e799c1..cd7a6a1058 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -4,7 +4,6 @@ namespace Appwrite\Platform\Workers; use Ahc\Jwt\JWT; use Appwrite\Event\Realtime; -use Appwrite\Event\StatsUsage; use Exception; use Utopia\CLI\Console; use Utopia\Config\Config; @@ -14,14 +13,9 @@ use Utopia\Database\Exception\Authorization; use Utopia\Database\Exception\Conflict; use Utopia\Database\Exception\Restricted; use Utopia\Database\Exception\Structure; -use Utopia\Database\Validator\Authorization as AuthorizationValidator; use Utopia\Migration\Destination; use Utopia\Migration\Destinations\Appwrite as DestinationAppwrite; use Utopia\Migration\Exception as MigrationException; -use Utopia\Migration\Resource; -use Utopia\Migration\Resources\Database\Database as ResourceDatabase; -use Utopia\Migration\Resources\Database\Row as ResourceRow; -use Utopia\Migration\Resources\Database\Table as ResourceTable; use Utopia\Migration\Source; use Utopia\Migration\Sources\Appwrite as SourceAppwrite; use Utopia\Migration\Sources\CSV; @@ -51,7 +45,6 @@ class Migrations extends Action */ protected array $sourceReport = []; - private string $source; /** * @var callable */ @@ -76,14 +69,13 @@ class Migrations extends Action ->inject('logError') ->inject('queueForRealtime') ->inject('deviceForImports') - ->inject('queueForStatsUsage') ->callback($this->action(...)); } /** * @throws Exception */ - public function action(Message $message, Document $project, Database $dbForProject, Database $dbForPlatform, callable $logError, Realtime $queueForRealtime, Device $deviceForImports, StatsUsage $queueForStatsUsage): void + public function action(Message $message, Document $project, Database $dbForProject, Database $dbForPlatform, callable $logError, Realtime $queueForRealtime, Device $deviceForImports): void { $payload = $message->getPayload() ?? []; $this->deviceForImports = $deviceForImports; @@ -111,7 +103,7 @@ class Migrations extends Action return; } - $this->processMigration($migration, $queueForRealtime, $queueForStatsUsage); + $this->processMigration($migration, $queueForRealtime); } /** @@ -212,7 +204,6 @@ class Migrations extends Action // set the errors back without trace $clonedMigrationDocument->setAttribute('errors', $errorMessages); - /** Trigger Realtime Events */ $queueForRealtime ->setProject($project) @@ -275,7 +266,7 @@ class Migrations extends Action * @throws \Utopia\Database\Exception * @throws Exception */ - protected function processMigration(Document $migration, Realtime $queueForRealtime, StatsUsage $queueForStatsUsage): void + protected function processMigration(Document $migration, Realtime $queueForRealtime): void { $project = $this->project; $projectDocument = $this->dbForPlatform->getDocument('projects', $project->getId()); @@ -309,7 +300,6 @@ class Migrations extends Action $destination ); - $aggregatedResources = []; /** Start Transfer */ if (empty($source->getErrors())) { $migration->setAttribute('stage', 'migrating'); @@ -317,40 +307,9 @@ class Migrations extends Action $transfer->run( $migration->getAttribute('resources'), - function ($resources) use ($migration, $transfer, $projectDocument, $queueForRealtime, &$aggregatedResources) { + function () use ($migration, $transfer, $projectDocument, $queueForRealtime) { $migration->setAttribute('resourceData', json_encode($transfer->getCache())); $migration->setAttribute('statusCounters', json_encode($transfer->getStatusCounters())); - - if (!empty($resources)) { - /** - * @var Resource $resource - */ - $resource = $resources[0]; - $count = count($resources); - $databaseId = null; - $tableId = null; - switch ($resource->getName()) { - case ResourceTable::getName(): - /** @var ResourceTable $resource */ - $databaseId = $resource->getDatabase()->getSequence(); - break; - case ResourceRow::getName(): - /** @var ResourceRow $resource */ - $table = $resource->getTable(); - $databaseId = $table->getDatabase()->getSequence(); - $tableId = $table->getSequence(); - break; - default: - break; - } - $aggregatedResources[] = [ - 'name' => $resource->getName(), - 'count' => $count, - 'databaseId' => $databaseId, - 'tableId' => $tableId - ]; - - } $this->updateMigrationDocument($migration, $projectDocument, $queueForRealtime); }, $migration->getAttribute('resourceId'), @@ -452,71 +411,9 @@ class Migrations extends Action } if ($migration->getAttribute('status', '') === 'completed') { - foreach ($aggregatedResources as $resource) { - $this->processMigrationResourceStats( - $resource, - $queueForStatsUsage, - $projectDocument, - $migration->getAttribute('source'), - $migration->getAttribute('resourceId') - ); - } $destination?->success(); $source?->success(); } } } - - private function processMigrationResourceStats(array $resources, StatsUsage $queueForStatsUsage, Document $projectDocument, string $source, ?string $resourceId) - { - $resourceName = $resources['name']; - $count = $resources['count']; - $databaseInternalId = $resources['databaseId']; - $tableInternalId = $resources['tableId']; - - if ($source === CSV::getName()) { - [$databaseId, $tableId] = explode(':', $resourceId); - $database = AuthorizationValidator::skip(fn () => $this->dbForProject->getDocument('databases', $databaseId)); - $table = AuthorizationValidator::skip(fn () => $this->dbForProject->getDocument('database_' . $database->getSequence(), $tableId)); - $databaseInternalId = (int) $database->getSequence(); - $tableInternalId = (int) $table->getSequence(); - } - - switch ($resourceName) { - case ResourceDatabase::getName(): - $queueForStatsUsage->addMetric(METRIC_DATABASES, $count); - break; - - case ResourceTable::getName(): - $queueForStatsUsage - ->addMetric(METRIC_COLLECTIONS, $count) - ->addMetric( - str_replace('{databaseInternalId}', $databaseInternalId, METRIC_DATABASE_ID_COLLECTIONS), - $count - ); - break; - - case ResourceRow::getName(): - $queueForStatsUsage - ->addMetric( - str_replace( - ['{databaseInternalId}','{collectionInternalId}'], - [$databaseInternalId, $tableInternalId], - METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS - ), - $count - ) - ->addMetric( - str_replace('{databaseInternalId}', $databaseInternalId, METRIC_DATABASE_ID_DOCUMENTS), - $count - ); - break; - - default: - break; - } - - $queueForStatsUsage->setProject($projectDocument)->trigger(); - $queueForStatsUsage->reset(); - } } diff --git a/src/Appwrite/Utopia/Response/Model/Row.php b/src/Appwrite/Utopia/Response/Model/Row.php index 370d3065ba..14a9ec9cda 100644 --- a/src/Appwrite/Utopia/Response/Model/Row.php +++ b/src/Appwrite/Utopia/Response/Model/Row.php @@ -41,18 +41,21 @@ class Row extends Any 'description' => 'Row automatically incrementing ID.', 'default' => 0, 'example' => 1, + 'readOnly' => true, ]) ->addRule('$tableId', [ 'type' => self::TYPE_STRING, 'description' => 'Table ID.', 'default' => '', 'example' => '5e5ea5c15117e', + 'readOnly' => true, ]) ->addRule('$databaseId', [ 'type' => self::TYPE_STRING, 'description' => 'Database ID.', 'default' => '', 'example' => '5e5ea5c15117e', + 'readOnly' => true, ]) ->addRule('$createdAt', [ 'type' => self::TYPE_DATETIME, diff --git a/tests/e2e/Client.php b/tests/e2e/Client.php index e411b68454..6b81713654 100644 --- a/tests/e2e/Client.php +++ b/tests/e2e/Client.php @@ -108,6 +108,20 @@ class Client return $this; } + /** + * Set Response Format + * + * @param string $value + * + * @return self $this + */ + public function setResponseFormat(string $value): self + { + $this->addHeader('X-Appwrite-Response-Format', $value); + + return $this; + } + /** * @param bool $status true * @return self $this diff --git a/tests/e2e/Scopes/Scope.php b/tests/e2e/Scopes/Scope.php index 2dbeae961e..5b7f1a8771 100644 --- a/tests/e2e/Scopes/Scope.php +++ b/tests/e2e/Scopes/Scope.php @@ -7,6 +7,7 @@ use Appwrite\Tests\Retryable; use PHPUnit\Framework\TestCase; use Tests\E2E\Client; use Utopia\Database\Helpers\ID; +use Utopia\System\System; abstract class Scope extends TestCase { @@ -23,6 +24,17 @@ abstract class Scope extends TestCase { $this->client = new Client(); $this->client->setEndpoint($this->endpoint); + + $format = System::getEnv('_APP_E2E_RESPONSE_FORMAT'); + if (!empty($format)) { + if ( + !\preg_match('/^\d+\.\d+\.\d+$/', $format) || + !\version_compare($format, APP_VERSION_STABLE, '<=') + ) { + throw new \Exception('E2E response format must be ' . APP_VERSION_STABLE . ' or lower.'); + } + $this->client->setResponseFormat($format); + } } protected function tearDown(): void diff --git a/tests/e2e/Services/Databases/Legacy/DatabasesBase.php b/tests/e2e/Services/Databases/Legacy/DatabasesBase.php index bd71272537..59864255ab 100644 --- a/tests/e2e/Services/Databases/Legacy/DatabasesBase.php +++ b/tests/e2e/Services/Databases/Legacy/DatabasesBase.php @@ -1697,6 +1697,7 @@ trait DatabasesBase return $data; } + /** * @depends testCreateIndexes */ @@ -2211,6 +2212,55 @@ trait DatabasesBase $this->assertArrayHasKey('$permissions', $library3['body']); $this->assertCount(3, $library3['body']['$permissions']); $this->assertNotEmpty($library3['body']['$permissions']); + + // Readonly attributes are ignored + $personNoPerm = $this->client->call(Client::METHOD_PUT, '/databases/' . $databaseId . '/collections/' . $person['body']['$id'] . '/documents/' . $newPersonId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'data' => [ + '$id' => 'some-other-id', + '$collectionId' => 'some-other-collection', + '$databaseId' => 'some-other-database', + '$createdAt' => '2024-01-01T00:00:00Z', + '$updatedAt' => '2024-01-01T00:00:00Z', + 'library' => [ + '$id' => 'library3', + 'libraryName' => 'Library 3', + '$createdAt' => '2024-01-01T00:00:00Z', + '$updatedAt' => '2024-01-01T00:00:00Z', + ], + ], + ]); + + $update = $personNoPerm; + $update['body']['$id'] = 'random'; + $update['body']['$sequence'] = 123; + $update['body']['$databaseId'] = 'random'; + $update['body']['$collectionId'] = 'random'; + $update['body']['$createdAt'] = '2024-01-01T00:00:00.000+00:00'; + $update['body']['$updatedAt'] = '2024-01-01T00:00:00.000+00:00'; + + $upserted = $this->client->call(Client::METHOD_PUT, '/databases/' . $databaseId . '/collections/' . $person['body']['$id'] . '/documents/' . $newPersonId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'data' => $update['body'] + ]); + + $this->assertEquals(200, $upserted['headers']['status-code']); + $this->assertEquals($personNoPerm['body']['$id'], $upserted['body']['$id']); + $this->assertEquals($personNoPerm['body']['$collectionId'], $upserted['body']['$collectionId']); + $this->assertEquals($personNoPerm['body']['$databaseId'], $upserted['body']['$databaseId']); + $this->assertEquals($personNoPerm['body']['$sequence'], $upserted['body']['$sequence']); + + if ($this->getSide() === 'client') { + $this->assertEquals($personNoPerm['body']['$createdAt'], $upserted['body']['$createdAt']); + $this->assertNotEquals('2024-01-01T00:00:00.000+00:00', $upserted['body']['$updatedAt']); + } else { + $this->assertEquals('2024-01-01T00:00:00.000+00:00', $upserted['body']['$createdAt']); + $this->assertEquals('2024-01-01T00:00:00.000+00:00', $upserted['body']['$updatedAt']); + } } } @@ -3000,6 +3050,37 @@ trait DatabasesBase $this->assertEquals(200, $response['headers']['status-code']); + // Test readonly attributes are ignored + $response = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/documents/' . $id, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-timestamp' => DateTime::formatTz(DateTime::now()), + ], $this->getHeaders()), [ + 'data' => [ + '$id' => 'newId', + '$sequence' => 9999, + '$collectionId' => 'newCollectionId', + '$databaseId' => 'newDatabaseId', + '$createdAt' => '2024-01-01T00:00:00.000+00:00', + '$updatedAt' => '2024-01-01T00:00:00.000+00:00', + 'title' => 'Thor: Ragnarok', + ], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals($id, $response['body']['$id']); + $this->assertEquals($data['moviesId'], $response['body']['$collectionId']); + $this->assertEquals($databaseId, $response['body']['$databaseId']); + $this->assertNotEquals(9999, $response['body']['$sequence']); + + if ($this->getSide() === 'client') { + $this->assertNotEquals('2024-01-01T00:00:00.000+00:00', $response['body']['$createdAt']); + $this->assertNotEquals('2024-01-01T00:00:00.000+00:00', $response['body']['$updatedAt']); + } else { + $this->assertEquals('2024-01-01T00:00:00.000+00:00', $response['body']['$createdAt']); + $this->assertEquals('2024-01-01T00:00:00.000+00:00', $response['body']['$updatedAt']); + } + return []; } @@ -4260,7 +4341,9 @@ trait DatabasesBase ] ]); if ($this->getSide() === 'client') { - $this->assertEquals($document['headers']['status-code'], 400); + $this->assertEquals($document['body']['title'], 'Again Updated Date Test'); + $this->assertNotEquals($document['body']['$createdAt'], DateTime::formatTz('2022-08-01 13:09:23.040')); + $this->assertNotEquals($document['body']['$updatedAt'], DateTime::formatTz('2022-08-01 13:09:23.050')); } else { $this->assertEquals($document['body']['title'], 'Again Updated Date Test'); $this->assertEquals($document['body']['$createdAt'], DateTime::formatTz('2022-08-01 13:09:23.040')); diff --git a/tests/e2e/Services/Databases/Legacy/DatabasesCustomClientTest.php b/tests/e2e/Services/Databases/Legacy/DatabasesCustomClientTest.php index 23153e8f39..699a2b8f25 100644 --- a/tests/e2e/Services/Databases/Legacy/DatabasesCustomClientTest.php +++ b/tests/e2e/Services/Databases/Legacy/DatabasesCustomClientTest.php @@ -889,157 +889,4 @@ class DatabasesCustomClientTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); } - public function testModifyCreatedAtUpdatedAtSingleDocument(): void - { - $database = $this->client->call(Client::METHOD_POST, '/databases', [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ], [ - 'databaseId' => ID::unique(), - 'name' => 'Test Database' - ]); - - $databaseId = $database['body']['$id']; - - $table = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'collectionId' => ID::unique(), - 'name' => 'Test Table', - 'documentsecurity' => true, - 'permissions' => [ - Permission::create(Role::user($this->getUser()['$id'])), - Permission::read(Role::user($this->getUser()['$id'])), - Permission::update(Role::user($this->getUser()['$id'])), - Permission::delete(Role::user($this->getUser()['$id'])), - ], - ]); - $collectionId = $table['body']['$id']; - - // Create string column - $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'key' => 'title', - 'size' => 256, - 'required' => true, - ]); - - sleep(1); - - // Test 1: Try to create document with $createdAt - should return 400 - $response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'documentId' => ID::unique(), - 'data' => [ - 'title' => 'Test Movie', - '$createdAt' => '2000-01-01T10:00:00.000+00:00' - ] - ]); - - $this->assertEquals(400, $response['headers']['status-code']); - - // Test 2: Try to create document with $updatedAt - should return 400 - $response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'documentId' => ID::unique(), - 'data' => [ - 'title' => 'Test Movie', - '$updatedAt' => '2000-01-01T10:00:00.000+00:00' - ] - ]); - - $this->assertEquals(400, $response['headers']['status-code']); - - // Test 3: Try to create document with both $createdAt and $updatedAt - should return 400 - $response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'documentId' => ID::unique(), - 'data' => [ - 'title' => 'Test Movie', - '$createdAt' => '2000-01-01T10:00:00.000+00:00', - '$updatedAt' => '2000-01-01T10:00:00.000+00:00' - ] - ]); - - $this->assertEquals(400, $response['headers']['status-code']); - - // Test 4: Create a valid document first - $validRow = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'documentId' => ID::unique(), - 'data' => [ - 'title' => 'Valid Movie' - ] - ]); - - $this->assertEquals(201, $validRow['headers']['status-code']); - $documentId = $validRow['body']['$id']; - - // Test 5: Try to update document with $createdAt - should return 400 - $response = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $documentId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'data' => [ - 'title' => 'Updated Movie', - '$createdAt' => '2000-01-01T10:00:00.000+00:00' - ] - ]); - - $this->assertEquals(400, $response['headers']['status-code']); - - // Test 6: Try to update document with $updatedAt - should return 400 - $response = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $documentId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'data' => [ - 'title' => 'Updated Movie', - '$updatedAt' => '2000-01-01T10:00:00.000+00:00' - ] - ]); - - $this->assertEquals(400, $response['headers']['status-code']); - - // Test 7: Try to update document with both $createdAt and $updatedAt - should return 400 - $response = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $documentId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'data' => [ - 'title' => 'Updated Movie', - '$createdAt' => '2000-01-01T10:00:00.000+00:00', - '$updatedAt' => '2000-01-01T10:00:00.000+00:00' - ] - ]); - - $this->assertEquals(400, $response['headers']['status-code']); - - // Cleanup - $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/collections/' . $collectionId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); - - $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); - } } diff --git a/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php b/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php index c57421c384..c2a1ee859d 100644 --- a/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php +++ b/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php @@ -4269,9 +4269,10 @@ trait DatabasesBase ]); if ($this->getSide() === 'client') { - $this->assertEquals($row['headers']['status-code'], 400); - } else { $this->assertEquals($row['body']['title'], 'Again Updated Date Test'); + $this->assertNotEquals($row['body']['$createdAt'], DateTime::formatTz('2022-08-01 13:09:23.040')); + $this->assertNotEquals($row['body']['$updatedAt'], DateTime::formatTz('2022-08-01 13:09:23.050')); + } else { $this->assertEquals($row['body']['$createdAt'], DateTime::formatTz('2022-08-01 13:09:23.040')); $this->assertEquals($row['body']['$updatedAt'], DateTime::formatTz('2022-08-01 13:09:23.050')); diff --git a/tests/e2e/Services/Databases/TablesDB/DatabasesCustomClientTest.php b/tests/e2e/Services/Databases/TablesDB/DatabasesCustomClientTest.php index 277771e2df..f986b5dd03 100644 --- a/tests/e2e/Services/Databases/TablesDB/DatabasesCustomClientTest.php +++ b/tests/e2e/Services/Databases/TablesDB/DatabasesCustomClientTest.php @@ -890,158 +890,4 @@ class DatabasesCustomClientTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); } - - public function testModifyCreatedAtUpdatedAtSingleRow(): void - { - $database = $this->client->call(Client::METHOD_POST, '/tablesdb', [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ], [ - 'databaseId' => ID::unique(), - 'name' => 'Test Database' - ]); - - $databaseId = $database['body']['$id']; - - $table = $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' => 'Test Table', - 'rowSecurity' => true, - 'permissions' => [ - Permission::create(Role::user($this->getUser()['$id'])), - Permission::read(Role::user($this->getUser()['$id'])), - Permission::update(Role::user($this->getUser()['$id'])), - Permission::delete(Role::user($this->getUser()['$id'])), - ], - ]); - $tableId = $table['body']['$id']; - - // Create string column - $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/string', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'key' => 'title', - 'size' => 256, - 'required' => true, - ]); - - sleep(1); - - // Test 1: Try to create row with $createdAt - should return 400 - $response = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'rowId' => ID::unique(), - 'data' => [ - 'title' => 'Test Movie', - '$createdAt' => '2000-01-01T10:00:00.000+00:00' - ] - ]); - - $this->assertEquals(400, $response['headers']['status-code']); - - // Test 2: Try to create row with $updatedAt - should return 400 - $response = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'rowId' => ID::unique(), - 'data' => [ - 'title' => 'Test Movie', - '$updatedAt' => '2000-01-01T10:00:00.000+00:00' - ] - ]); - - $this->assertEquals(400, $response['headers']['status-code']); - - // Test 3: Try to create row with both $createdAt and $updatedAt - should return 400 - $response = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'rowId' => ID::unique(), - 'data' => [ - 'title' => 'Test Movie', - '$createdAt' => '2000-01-01T10:00:00.000+00:00', - '$updatedAt' => '2000-01-01T10:00:00.000+00:00' - ] - ]); - - $this->assertEquals(400, $response['headers']['status-code']); - - // Test 4: Create a valid row first - $validRow = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'rowId' => ID::unique(), - 'data' => [ - 'title' => 'Valid Movie' - ] - ]); - - $this->assertEquals(201, $validRow['headers']['status-code']); - $rowId = $validRow['body']['$id']; - - // Test 5: Try to update row with $createdAt - should return 400 - $response = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $rowId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'data' => [ - 'title' => 'Updated Movie', - '$createdAt' => '2000-01-01T10:00:00.000+00:00' - ] - ]); - - $this->assertEquals(400, $response['headers']['status-code']); - - // Test 6: Try to update row with $updatedAt - should return 400 - $response = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $rowId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'data' => [ - 'title' => 'Updated Movie', - '$updatedAt' => '2000-01-01T10:00:00.000+00:00' - ] - ]); - - $this->assertEquals(400, $response['headers']['status-code']); - - // Test 7: Try to update row with both $createdAt and $updatedAt - should return 400 - $response = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $rowId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'data' => [ - 'title' => 'Updated Movie', - '$createdAt' => '2000-01-01T10:00:00.000+00:00', - '$updatedAt' => '2000-01-01T10:00:00.000+00:00' - ] - ]); - - $this->assertEquals(400, $response['headers']['status-code']); - - // Cleanup - $this->client->call(Client::METHOD_DELETE, '/tablesdb/' . $databaseId . '/tables/' . $tableId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); - - $this->client->call(Client::METHOD_DELETE, '/tablesdb/' . $databaseId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ])); - } }