Merge pull request #10405 from appwrite/fix-readonly-on-write

Fix readonly attr stripping on write
This commit is contained in:
Jake Barnby 2025-08-29 21:37:28 +12:00 committed by GitHub
commit e005389ca6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 133 additions and 369 deletions

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',
@ -242,6 +231,8 @@ class Update extends Action
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, max($operations, 1))
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), $operations);
\var_dump($newDocument);
try {
$document = $dbForProject->withRequestTimestamp(
$requestTimestamp,

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

@ -212,7 +212,6 @@ class Migrations extends Action
// set the errors back without trace
$clonedMigrationDocument->setAttribute('errors', $errorMessages);
/** Trigger Realtime Events */
$queueForRealtime
->setProject($project)

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