Merge branch '1.8.x' into feat-update-preview-url-in-vcs-controller

This commit is contained in:
Khushboo Verma 2025-09-02 13:40:48 +05:30 committed by GitHub
commit 44950eac0b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 289 additions and 562 deletions

View file

@ -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

View file

@ -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 => [

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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

View file

@ -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;

View file

@ -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));
}

View file

@ -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

View file

@ -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) {

12
composer.lock generated
View file

@ -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",

View file

@ -85,11 +85,4 @@ class StatsUsage extends Event
}),
];
}
public function reset(): Event
{
$this->metrics = [];
parent::reset();
return $this;
}
}

View file

@ -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';

View file

@ -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;
}
/**

View file

@ -127,8 +127,7 @@ class Update extends Action
}
}
// Remove sequence if set
unset($document['$sequence']);
$data = $this->removeReadonlyAttributes($data, privileged: true);
$documents = [];

View file

@ -100,6 +100,7 @@ class Upsert extends Action
}
foreach ($documents as $key => $document) {
$document = $this->removeReadonlyAttributes($document, privileged: true);
$documents[$key] = new Document($document);
}

View file

@ -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);

View file

@ -117,6 +117,7 @@ class Delete extends Action
}
$collectionsCache = [];
$this->processDocument(
database: $database,
collection: $collection,

View file

@ -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',

View file

@ -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',

View file

@ -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);
}

View file

@ -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();
}
}

View file

@ -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,

View file

@ -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

View file

@ -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

View file

@ -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'));

View file

@ -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']
]));
}
}

View file

@ -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'));

View file

@ -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']
]));
}
}