Merge pull request #10244 from appwrite/dat-588

Allow modifying createdAt and updatedAt with server side sdk and api key
This commit is contained in:
Jake Barnby 2025-08-01 21:37:09 +12:00 committed by GitHub
commit e6f0804272
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 2282 additions and 62 deletions

24
composer.lock generated
View file

@ -69,16 +69,16 @@
},
{
"name": "appwrite/appwrite",
"version": "15.0.0",
"version": "15.1.0",
"source": {
"type": "git",
"url": "https://github.com/appwrite/sdk-for-php.git",
"reference": "deb97b62e0abed8a4fd5c5d48e77365cf89867cf"
"reference": "c438b3885071ac7c0329199dce5e6f6a24dd215b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/deb97b62e0abed8a4fd5c5d48e77365cf89867cf",
"reference": "deb97b62e0abed8a4fd5c5d48e77365cf89867cf",
"url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/c438b3885071ac7c0329199dce5e6f6a24dd215b",
"reference": "c438b3885071ac7c0329199dce5e6f6a24dd215b",
"shasum": ""
},
"require": {
@ -104,10 +104,10 @@
"support": {
"email": "team@appwrite.io",
"issues": "https://github.com/appwrite/sdk-for-php/issues",
"source": "https://github.com/appwrite/sdk-for-php/tree/15.0.0",
"source": "https://github.com/appwrite/sdk-for-php/tree/15.1.0",
"url": "https://appwrite.io/support"
},
"time": "2025-05-18T09:47:10+00:00"
"time": "2025-08-01T04:50:51+00:00"
},
{
"name": "appwrite/php-clamav",
@ -4814,16 +4814,16 @@
"packages-dev": [
{
"name": "appwrite/sdk-generator",
"version": "0.41.26",
"version": "0.41.27",
"source": {
"type": "git",
"url": "https://github.com/appwrite/sdk-generator.git",
"reference": "5a13191a5a4bdec8fe1b1180ff67f75c4ff6ac0b"
"reference": "083fd2e8163d6a4e59ee971ac6cb97277d831dd5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/5a13191a5a4bdec8fe1b1180ff67f75c4ff6ac0b",
"reference": "5a13191a5a4bdec8fe1b1180ff67f75c4ff6ac0b",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/083fd2e8163d6a4e59ee971ac6cb97277d831dd5",
"reference": "083fd2e8163d6a4e59ee971ac6cb97277d831dd5",
"shasum": ""
},
"require": {
@ -4859,9 +4859,9 @@
"description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms",
"support": {
"issues": "https://github.com/appwrite/sdk-generator/issues",
"source": "https://github.com/appwrite/sdk-generator/tree/0.41.26"
"source": "https://github.com/appwrite/sdk-generator/tree/0.41.27"
},
"time": "2025-07-30T06:53:12+00:00"
"time": "2025-07-31T10:20:46+00:00"
},
{
"name": "doctrine/annotations",

View file

@ -125,16 +125,18 @@ class Update extends Action
$documents = [];
try {
$modified = $dbForProject->updateDocuments(
'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(),
new Document($data),
$queries,
onNext: function (Document $document) use ($plan, &$documents) {
if (\count($documents) < ($plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH)) {
$documents[] = $document;
}
},
);
$modified = $dbForProject->withPreserveDates(function () use ($plan, &$documents, $dbForProject, $database, $collection, $data, $queries) {
return $dbForProject->updateDocuments(
'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(),
new Document($data),
$queries,
onNext: function (Document $document) use ($plan, &$documents) {
if (\count($documents) < ($plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH)) {
$documents[] = $document;
}
},
);
});
} catch (ConflictException) {
throw new Exception($this->getConflictException());
} catch (RelationshipException $e) {

View file

@ -106,15 +106,17 @@ class Upsert extends Action
$upserted = [];
try {
$modified = $dbForProject->createOrUpdateDocuments(
'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(),
$documents,
onNext: function (Document $document) use ($plan, &$upserted) {
if (\count($upserted) < ($plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH)) {
$upserted[] = $document;
}
},
);
$modified = $dbForProject->withPreserveDates(function () use ($dbForProject, $database, $collection, $documents, $plan, &$upserted) {
return $dbForProject->createOrUpdateDocuments(
'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(),
$documents,
onNext: function (Document $document) use ($plan, &$upserted) {
if (\count($upserted) < ($plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH)) {
$upserted[] = $document;
}
},
);
});
} catch (ConflictException) {
throw new Exception($this->getConflictException());
} catch (DuplicateException) {

View file

@ -332,7 +332,7 @@ class Create extends Action
}
};
$documents = \array_map(function ($document) use ($collection, $permissions, $checkPermissions, $isBulk, $documentId, $setPermissions) {
$documents = \array_map(function ($document) use ($collection, $permissions, $checkPermissions, $isBulk, $documentId, $setPermissions, $isAPIKey, $isPrivilegedUser) {
$document['$collection'] = $collection->getId();
// Determine the source ID depending on whether it's a bulk operation.
@ -350,6 +350,18 @@ class Create extends Action
// 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 = new Document($document);
$setPermissions($document, $permissions);
$checkPermissions($collection, $document, Database::PERMISSION_CREATE);
@ -358,9 +370,11 @@ class Create extends Action
}, $documents);
try {
$dbForProject->createDocuments(
'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(),
$documents
$dbForProject->withPreserveDates(
fn () => $dbForProject->createDocuments(
'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(),
$documents,
)
);
} catch (DuplicateException) {
throw new Exception($this->getDuplicateException());

View file

@ -109,6 +109,16 @@ 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));
@ -233,11 +243,11 @@ class Update extends Action
try {
$document = $dbForProject->withRequestTimestamp(
$requestTimestamp,
fn () => $dbForProject->updateDocument(
fn () => $dbForProject->withPreserveDates(fn () => $dbForProject->updateDocument(
'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(),
$document->getId(),
$newDocument
)
))
);
} catch (ConflictException) {
throw new Exception($this->getConflictException());

View file

@ -153,6 +153,16 @@ 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 ?? [];
@ -236,13 +246,15 @@ class Upsert extends Action
$upserted = [];
try {
$dbForProject->createOrUpdateDocuments(
'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(),
[$newDocument],
onNext: function (Document $document) use (&$upserted) {
$upserted[] = $document;
},
);
$dbForProject->withPreserveDates(function () use (&$upserted, $dbForProject, $database, $collection, $newDocument) {
return $dbForProject->createOrUpdateDocuments(
'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(),
[$newDocument],
onNext: function (Document $document) use (&$upserted) {
$upserted[] = $document;
},
);
});
} catch (ConflictException) {
throw new Exception($this->getConflictException());
} catch (DuplicateException) {

View file

@ -2850,7 +2850,6 @@ trait DatabasesBase
'releaseYear' => 2017,
'birthDay' => '1976-06-12 14:12:55',
'actors' => [],
'$createdAt' => 5 // Should be ignored
],
'permissions' => [
Permission::read(Role::user($this->getUser()['$id'])),
@ -4215,17 +4214,19 @@ trait DatabasesBase
$row = $this->client->call(Client::METHOD_PATCH, '/databases/' . $data['databaseId'] . '/grids/tables/' . $data['moviesId'] . '/rows/' . $rowId, $headers, [
'data' => [
'title' => 'Again Updated Date Test',
'$createdAt' => '2022-08-01 13:09:23.040', // $createdAt is not updatable
'$updatedAt' => '2022-08-01 13:09:23.050' // system will update it not api
'$createdAt' => '2022-08-01 13:09:23.040',
'$updatedAt' => '2022-08-01 13:09:23.050'
]
]);
$this->assertEquals($row['body']['title'], 'Again Updated Date Test');
$this->assertEquals($row['body']['$createdAt'], $createdAt);
$this->assertNotEquals($row['body']['$createdAt'], '2022-08-01 13:09:23.040');
$this->assertNotEquals($row['body']['$updatedAt'], $updatedAt);
$this->assertNotEquals($row['body']['$updatedAt'], $updatedAtSecond);
$this->assertNotEquals($row['body']['$updatedAt'], '2022-08-01 13:09:23.050');
if ($this->getSide() === 'client') {
$this->assertEquals($row['headers']['status-code'], 400);
} else {
$this->assertEquals($row['body']['title'], 'Again Updated Date Test');
$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'));
}
return $data;
}

View file

@ -890,4 +890,158 @@ class DatabasesCustomClientTest extends Scope
$this->assertEquals(200, $response['headers']['status-code']);
}
public function testModifyCreatedAtUpdatedAtSingleRow(): 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 . '/grids/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, '/databases/' . $databaseId . '/grids/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, '/databases/' . $databaseId . '/grids/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, '/databases/' . $databaseId . '/grids/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, '/databases/' . $databaseId . '/grids/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, '/databases/' . $databaseId . '/grids/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, '/databases/' . $databaseId . '/grids/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, '/databases/' . $databaseId . '/grids/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, '/databases/' . $databaseId . '/grids/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, '/databases/' . $databaseId . '/grids/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, '/databases/' . $databaseId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
}
}

View file

@ -5189,4 +5189,947 @@ class DatabasesCustomServerTest extends Scope
$this->assertEquals(400, $response['headers']['status-code']);
}
public function testDateTimeRow(): void
{
$databaseId = $this->client->call(Client::METHOD_POST, '/databases', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'databaseId' => ID::unique(),
'name' => 'DateTime Test Database',
]);
$this->assertEquals(201, $databaseId['headers']['status-code']);
$databaseId = $databaseId['body']['$id'];
$table = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'tableId' => ID::unique(),
'name' => 'create_modify_dates',
'rowSecurity' => true,
'permissions' => [],
]);
$this->assertEquals(201, $table['headers']['status-code']);
$tableId = $table['body']['$id'];
// Create string column
$this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/columns/string', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'key' => 'string',
'size' => 128,
'required' => false,
]);
// Create datetime column
$this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/columns/datetime', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'key' => 'datetime',
'required' => false,
'format' => 'datetime',
]);
sleep(1);
$date = '2000-01-01T10:00:00.000+00:00';
// Test - default behaviour of external datetime column not changed
$row = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'rowId' => 'row1',
'data' => [
'datetime' => ''
],
'permissions' => [
Permission::read(Role::any()),
Permission::write(Role::any()),
Permission::update(Role::any()),
]
]);
$this->assertEquals(201, $row['headers']['status-code']);
$this->assertNotEmpty($row['body']['datetime']);
$this->assertNotEmpty($row['body']['$createdAt']);
$this->assertNotEmpty($row['body']['$updatedAt']);
$row = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows/row1', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $row['headers']['status-code']);
$this->assertNotEmpty($row['body']['datetime']);
$this->assertNotEmpty($row['body']['$createdAt']);
$this->assertNotEmpty($row['body']['$updatedAt']);
// Test - modifying $createdAt and $updatedAt
$row = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'rowId' => 'row2',
'data' => [
'$createdAt' => $date
],
'permissions' => [
Permission::read(Role::any()),
Permission::write(Role::any()),
Permission::update(Role::any()),
]
]);
$this->assertEquals(201, $row['headers']['status-code']);
$this->assertEquals($row['body']['$createdAt'], $date);
$this->assertNotEmpty($row['body']['$updatedAt']);
$this->assertNotEquals($row['body']['$updatedAt'], $date);
$row = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows/row2', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $row['headers']['status-code']);
$this->assertEquals($row['body']['$createdAt'], $date);
$this->assertNotEmpty($row['body']['$updatedAt']);
$this->assertNotEquals($row['body']['$updatedAt'], $date);
// Cleanup
$this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/grids/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, '/databases/' . $databaseId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
}
public function testSingleRowDateOperations(): void
{
$databaseId = $this->client->call(Client::METHOD_POST, '/databases', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'databaseId' => ID::unique(),
'name' => 'Single Date Operations Database',
]);
$this->assertEquals(201, $databaseId['headers']['status-code']);
$databaseId = $databaseId['body']['$id'];
$table = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'tableId' => ID::unique(),
'name' => 'normal_date_operations',
'rowSecurity' => true,
'permissions' => [],
]);
$this->assertEquals(201, $table['headers']['status-code']);
$tableId = $table['body']['$id'];
// Create string column
$this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/columns/string', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'key' => 'string',
'size' => 128,
'required' => false,
]);
sleep(1);
$createDate = '2000-01-01T10:00:00.000+00:00';
$updateDate = '2000-02-01T15:30:00.000+00:00';
$date1 = '2000-01-01T10:00:00.000+00:00';
$date2 = '2000-02-01T15:30:00.000+00:00';
$date3 = '2000-03-01T20:45:00.000+00:00';
// Test 1: Create with custom createdAt, then update with custom updatedAt
$row = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'rowId' => 'row1',
'data' => [
'string' => 'initial',
'$createdAt' => $createDate
],
'permissions' => [
Permission::read(Role::any()),
Permission::write(Role::any()),
Permission::update(Role::any()),
]
]);
$this->assertEquals(201, $row['headers']['status-code']);
$this->assertEquals($createDate, $row['body']['$createdAt']);
$this->assertNotEquals($createDate, $row['body']['$updatedAt']);
// Update with custom updatedAt
$updatedRow = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows/row1', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'data' => [
'string' => 'updated',
'$updatedAt' => $updateDate
]
]);
$this->assertEquals(200, $updatedRow['headers']['status-code']);
$this->assertEquals($createDate, $updatedRow['body']['$createdAt']);
$this->assertEquals($updateDate, $updatedRow['body']['$updatedAt']);
// Test 2: Create with both custom dates
$row2 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'rowId' => 'row2',
'data' => [
'string' => 'both_dates',
'$createdAt' => $createDate,
'$updatedAt' => $updateDate
],
'permissions' => [
Permission::read(Role::any()),
Permission::write(Role::any()),
Permission::update(Role::any()),
]
]);
$this->assertEquals(201, $row2['headers']['status-code']);
$this->assertEquals($createDate, $row2['body']['$createdAt']);
$this->assertEquals($updateDate, $row2['body']['$updatedAt']);
// Test 3: Create without dates, then update with custom dates
$row3 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'rowId' => 'row3',
'data' => [
'string' => 'no_dates'
],
'permissions' => [
Permission::read(Role::any()),
Permission::write(Role::any()),
Permission::update(Role::any()),
]
]);
$this->assertEquals(201, $row3['headers']['status-code']);
$updatedRow3 = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows/row3', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'data' => [
'string' => 'updated_no_dates',
'$createdAt' => $createDate,
'$updatedAt' => $updateDate
]
]);
$this->assertEquals(200, $updatedRow3['headers']['status-code']);
$this->assertEquals($createDate, $updatedRow3['body']['$createdAt']);
$this->assertEquals($updateDate, $updatedRow3['body']['$updatedAt']);
// Test 4: Update only createdAt
$row4 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'rowId' => 'row4',
'data' => [
'string' => 'initial'
],
'permissions' => [
Permission::read(Role::any()),
Permission::write(Role::any()),
Permission::update(Role::any()),
]
]);
$this->assertEquals(201, $row4['headers']['status-code']);
$originalCreatedAt4 = $row4['body']['$createdAt'];
$originalUpdatedAt4 = $row4['body']['$updatedAt'];
$updatedRow4 = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows/row4', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'data' => [
'string' => 'updated',
'$updatedAt' => null,
'$createdAt' => null
],
]);
$this->assertEquals(200, $updatedRow4['headers']['status-code']);
$this->assertEquals($originalCreatedAt4, $updatedRow4['body']['$createdAt']);
$this->assertNotEquals($originalUpdatedAt4, $updatedRow4['body']['$updatedAt']);
// Test 5: Update only updatedAt
$finalRow4 = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows/row4', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'data' => [
'string' => 'final',
'$updatedAt' => $updateDate,
'$createdAt' => $createDate
]
]);
$this->assertEquals(200, $finalRow4['headers']['status-code']);
$this->assertEquals($createDate, $finalRow4['body']['$createdAt']);
$this->assertEquals($updateDate, $finalRow4['body']['$updatedAt']);
// Test 6: Create with updatedAt, update with createdAt
$row5 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'rowId' => 'row5',
'data' => [
'string' => 'row5',
'$updatedAt' => $date2
],
'permissions' => [
Permission::read(Role::any()),
Permission::write(Role::any()),
Permission::update(Role::any()),
]
]);
$this->assertEquals(201, $row5['headers']['status-code']);
$this->assertNotEquals($date2, $row5['body']['$createdAt']);
$this->assertEquals($date2, $row5['body']['$updatedAt']);
$updatedRow5 = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows/row5', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'data' => [
'string' => 'row5_updated',
'$createdAt' => $date1
]
]);
$this->assertEquals(200, $updatedRow5['headers']['status-code']);
$this->assertEquals($date1, $updatedRow5['body']['$createdAt']);
$this->assertNotEquals($date2, $updatedRow5['body']['$updatedAt']);
// Test 7: Create with both dates, update with different dates
$row6 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'rowId' => 'row6',
'data' => [
'string' => 'row6',
'$createdAt' => $date1,
'$updatedAt' => $date2
],
'permissions' => [
Permission::read(Role::any()),
Permission::write(Role::any()),
Permission::update(Role::any()),
]
]);
$this->assertEquals(201, $row6['headers']['status-code']);
$this->assertEquals($date1, $row6['body']['$createdAt']);
$this->assertEquals($date2, $row6['body']['$updatedAt']);
$updatedRow6 = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows/row6', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'data' => [
'string' => 'row6_updated',
'$createdAt' => $date3,
'$updatedAt' => $date3
]
]);
$this->assertEquals(200, $updatedRow6['headers']['status-code']);
$this->assertEquals($date3, $updatedRow6['body']['$createdAt']);
$this->assertEquals($date3, $updatedRow6['body']['$updatedAt']);
// Cleanup
$this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/grids/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, '/databases/' . $databaseId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
}
public function testBulkRowDateOperations(): void
{
$databaseId = $this->client->call(Client::METHOD_POST, '/databases', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'databaseId' => ID::unique(),
'name' => 'Bulk Date Operations Database',
]);
$this->assertEquals(201, $databaseId['headers']['status-code']);
$databaseId = $databaseId['body']['$id'];
$table = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'tableId' => ID::unique(),
'name' => 'bulk_date_operations',
'rowSecurity' => true,
'permissions' => [],
]);
$this->assertEquals(201, $table['headers']['status-code']);
$tableId = $table['body']['$id'];
// Create string column
$this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/columns/string', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'key' => 'string',
'size' => 128,
'required' => false,
]);
sleep(1);
$createDate = '2000-01-01T10:00:00.000+00:00';
$updateDate = '2000-02-01T15:30:00.000+00:00';
// Test 1: Bulk create with different date configurations
$rows = [
[
'$id' => 'row1',
'string' => 'row1',
'$createdAt' => $createDate,
'$permissions' => [
Permission::read(Role::user($this->getUser()['$id'])),
Permission::update(Role::user($this->getUser()['$id'])),
Permission::delete(Role::user($this->getUser()['$id'])),
]
],
[
'$id' => 'row2',
'string' => 'row2',
'$updatedAt' => $updateDate,
'$permissions' => [
Permission::read(Role::user($this->getUser()['$id'])),
Permission::update(Role::user($this->getUser()['$id'])),
Permission::delete(Role::user($this->getUser()['$id'])),
]
],
[
'$id' => 'row3',
'string' => 'row3',
'$createdAt' => $createDate,
'$updatedAt' => $updateDate,
'$permissions' => [
Permission::read(Role::user($this->getUser()['$id'])),
Permission::update(Role::user($this->getUser()['$id'])),
Permission::delete(Role::user($this->getUser()['$id'])),
]
],
[
'$id' => 'row4',
'string' => 'row4',
'$permissions' => [
Permission::read(Role::user($this->getUser()['$id'])),
Permission::update(Role::user($this->getUser()['$id'])),
Permission::delete(Role::user($this->getUser()['$id'])),
]
],
[
'$id' => 'row5',
'string' => 'row5',
'$createdAt' => null,
'$permissions' => [
Permission::read(Role::user($this->getUser()['$id'])),
Permission::update(Role::user($this->getUser()['$id'])),
Permission::delete(Role::user($this->getUser()['$id'])),
]
],
[
'$id' => 'row6',
'string' => 'row6',
'$updatedAt' => null,
'$permissions' => [
Permission::read(Role::user($this->getUser()['$id'])),
Permission::update(Role::user($this->getUser()['$id'])),
Permission::delete(Role::user($this->getUser()['$id'])),
]
]
];
// Create all rows in one bulk operation
$bulkCreateResponse = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'rows' => $rows
]);
$this->assertEquals(201, $bulkCreateResponse['headers']['status-code']);
$this->assertCount(count($rows), $bulkCreateResponse['body']['rows']);
// Verify initial state
foreach (['row1', 'row3'] as $id) {
$row = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $row['headers']['status-code']);
$this->assertEquals($createDate, $row['body']['$createdAt'], "createdAt mismatch for $id");
}
foreach (['row2', 'row3'] as $id) {
$row = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $row['headers']['status-code']);
$this->assertEquals($updateDate, $row['body']['$updatedAt'], "updatedAt mismatch for $id");
}
foreach (['row4', 'row5', 'row6'] as $id) {
$row = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $row['headers']['status-code']);
$this->assertNotEmpty($row['body']['$createdAt'], "createdAt missing for $id");
$this->assertNotEmpty($row['body']['$updatedAt'], "updatedAt missing for $id");
}
// Test 2: Bulk update with custom dates
$updateData = [
'data' => [
'string' => 'updated',
'$createdAt' => $createDate,
'$updatedAt' => $updateDate,
'$permissions' => [
Permission::read(Role::user($this->getUser()['$id'])),
Permission::update(Role::user($this->getUser()['$id'])),
Permission::delete(Role::user($this->getUser()['$id'])),
]
],
];
// Use bulk update instead of individual updates
$response = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), $updateData);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertCount(6, $response['body']['rows']);
// Verify updated state
foreach (['row1', 'row3'] as $id) {
$row = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $row['headers']['status-code']);
$this->assertEquals($createDate, $row['body']['$createdAt'], "createdAt mismatch for $id");
$this->assertEquals($updateDate, $row['body']['$updatedAt'], "updatedAt mismatch for $id");
$this->assertEquals('updated', $row['body']['string'], "string mismatch for $id");
}
foreach (['row2', 'row4', 'row5', 'row6'] as $id) {
$row = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $row['headers']['status-code']);
$this->assertEquals($updateDate, $row['body']['$updatedAt'], "updatedAt mismatch for $id");
$this->assertEquals('updated', $row['body']['string'], "string mismatch for $id");
}
$newDate = '2000-03-01T20:45:00.000+00:00';
$updateDataEnabled = [
'data' => [
'string' => 'enabled_update',
'$createdAt' => $newDate,
'$updatedAt' => $newDate
],
];
// Use bulk update instead of individual updates
$response = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), $updateDataEnabled);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertCount(6, $response['body']['rows']);
// Verify final state
foreach (['row1', 'row2', 'row3', 'row4', 'row5', 'row6'] as $id) {
$row = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $row['headers']['status-code']);
$this->assertEquals($newDate, $row['body']['$createdAt'], "createdAt mismatch for $id");
$this->assertEquals($newDate, $row['body']['$updatedAt'], "updatedAt mismatch for $id");
$this->assertEquals('enabled_update', $row['body']['string'], "string mismatch for $id");
}
// Cleanup
$this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/grids/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, '/databases/' . $databaseId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
}
public function testUpsertRowDateOperations(): void
{
$databaseId = $this->client->call(Client::METHOD_POST, '/databases', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'databaseId' => ID::unique(),
'name' => 'Upsert Date Operations Database',
]);
$this->assertEquals(201, $databaseId['headers']['status-code']);
$databaseId = $databaseId['body']['$id'];
$table = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'tableId' => ID::unique(),
'name' => 'upsert_date_operations',
'rowSecurity' => true,
'permissions' => [],
]);
$this->assertEquals(201, $table['headers']['status-code']);
$tableId = $table['body']['$id'];
// Create string column
$this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/columns/string', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'key' => 'string',
'size' => 128,
'required' => false,
]);
sleep(1);
$createDate = '2000-01-01T10:00:00.000+00:00';
$updateDate = '2000-02-01T15:30:00.000+00:00';
$date1 = '2000-01-01T10:00:00.000+00:00';
$date2 = '2000-02-01T15:30:00.000+00:00';
$date3 = '2000-03-01T20:45:00.000+00:00';
// Test 1: Upsert new row with custom createdAt
$upsertRow1 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'rowId' => 'upsert1',
'data' => [
'string' => 'upsert1_initial',
'$permissions' => [
Permission::read(Role::any()),
Permission::write(Role::any()),
Permission::update(Role::any()),
],
'$createdAt' => $createDate
],
]);
$this->assertEquals(201, $upsertRow1['headers']['status-code']);
$this->assertEquals($createDate, $upsertRow1['body']['$createdAt']);
$this->assertNotEquals($createDate, $upsertRow1['body']['$updatedAt']);
// Test 2: Upsert existing row with custom updatedAt
$updatedUpsertRow1 = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows/upsert1', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'data' => [
'string' => 'upsert1_updated',
'$updatedAt' => $updateDate
],
]);
$this->assertEquals(200, $updatedUpsertRow1['headers']['status-code']);
$this->assertEquals($createDate, $updatedUpsertRow1['body']['$createdAt']);
$this->assertEquals($updateDate, $updatedUpsertRow1['body']['$updatedAt']);
// Test 3: Upsert new row with both custom dates
$upsertRow2 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'rowId' => 'upsert2',
'data' => [
'string' => 'upsert2_both_dates',
'$permissions' => [
Permission::read(Role::any()),
Permission::write(Role::any()),
Permission::update(Role::any()),
],
'$createdAt' => $createDate,
'$updatedAt' => $updateDate
],
]);
$this->assertEquals(201, $upsertRow2['headers']['status-code']);
$this->assertEquals($createDate, $upsertRow2['body']['$createdAt']);
$this->assertEquals($updateDate, $upsertRow2['body']['$updatedAt']);
// Test 4: Upsert existing row with different dates
$updatedUpsertRow2 = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows/upsert2', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'data' => [
'string' => 'upsert2_updated',
'$createdAt' => $date3,
'$updatedAt' => $date3,
'$permissions' => [
Permission::read(Role::any()),
Permission::write(Role::any()),
Permission::update(Role::any()),
],
]
]);
$this->assertEquals(200, $updatedUpsertRow2['headers']['status-code']);
$this->assertEquals($date3, $updatedUpsertRow2['body']['$createdAt']);
$this->assertEquals($date3, $updatedUpsertRow2['body']['$updatedAt']);
// Test 5: Bulk upsert operations with custom dates
$upsertRows = [
[
'$id' => 'bulk_upsert1',
'string' => 'bulk_upsert1_initial',
'$createdAt' => $createDate
],
[
'$id' => 'bulk_upsert2',
'string' => 'bulk_upsert2_initial',
'$updatedAt' => $updateDate
],
[
'$id' => 'bulk_upsert3',
'string' => 'bulk_upsert3_initial',
'$createdAt' => $createDate,
'$updatedAt' => $updateDate
],
[
'$id' => 'bulk_upsert4',
'string' => 'bulk_upsert4_initial'
]
];
// Create rows using bulk upsert
$response = $this->client->call(Client::METHOD_PUT, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'rows' => $upsertRows
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertCount(4, $response['body']['rows']);
// Test 7: Verify initial bulk upsert state
foreach (['bulk_upsert1', 'bulk_upsert3'] as $id) {
$row = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $row['headers']['status-code']);
$this->assertEquals($createDate, $row['body']['$createdAt'], "createdAt mismatch for $id");
}
foreach (['bulk_upsert2', 'bulk_upsert3'] as $id) {
$row = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $row['headers']['status-code']);
$this->assertEquals($updateDate, $row['body']['$updatedAt'], "updatedAt mismatch for $id");
}
foreach (['bulk_upsert4'] as $id) {
$row = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $row['headers']['status-code']);
$this->assertNotEmpty($row['body']['$createdAt'], "createdAt missing for $id");
$this->assertNotEmpty($row['body']['$updatedAt'], "updatedAt missing for $id");
}
// Test 8: Bulk upsert update with custom dates
$newDate = '2000-04-01T12:00:00.000+00:00';
$updateUpsertData = [
'data' => [
'string' => 'bulk_upsert_updated',
'$createdAt' => $newDate,
'$updatedAt' => $newDate
],
'queries' => [Query::equal('$id', ['bulk_upsert1','bulk_upsert2','bulk_upsert3','bulk_upsert4'])->toString()]
];
$upsertIds = ['bulk_upsert1', 'bulk_upsert2', 'bulk_upsert3', 'bulk_upsert4'];
// Use bulk update instead of individual updates
$response = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), $updateUpsertData);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertCount(4, $response['body']['rows']);
// Verify updated state
foreach ($upsertIds as $id) {
$row = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $row['headers']['status-code']);
$this->assertEquals($newDate, $row['body']['$createdAt'], "createdAt mismatch for $id");
$this->assertEquals($newDate, $row['body']['$updatedAt'], "updatedAt mismatch for $id");
$this->assertEquals('bulk_upsert_updated', $row['body']['string'], "string mismatch for $id");
}
// Test 9: checking by passing null to each
$updateUpsertDataNull = [
'data' => [
'string' => 'bulk_upsert_null_test',
'$createdAt' => null,
'$updatedAt' => null
],
'queries' => [Query::equal('$id', ['bulk_upsert1','bulk_upsert2','bulk_upsert3','bulk_upsert4'])->toString()]
];
// Use bulk update instead of individual updates
$response = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), $updateUpsertDataNull);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertCount(4, $response['body']['rows']);
// Verify null handling
foreach ($upsertIds as $id) {
$row = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $row['headers']['status-code']);
$this->assertNotEmpty($row['body']['$createdAt'], "createdAt missing for $id");
$this->assertNotEmpty($row['body']['$updatedAt'], "updatedAt missing for $id");
}
// Cleanup
$this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/grids/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, '/databases/' . $databaseId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
}
}

View file

@ -2877,7 +2877,6 @@ trait DatabasesBase
'releaseYear' => 2017,
'birthDay' => '1976-06-12 14:12:55',
'actors' => [],
'$createdAt' => 5 // Should be ignored
],
'permissions' => [
Permission::read(Role::user($this->getUser()['$id'])),
@ -4207,17 +4206,18 @@ trait DatabasesBase
$document = $this->client->call(Client::METHOD_PATCH, '/databases/' . $data['databaseId'] . '/collections/' . $data['moviesId'] . '/documents/' . $documentId, $headers, [
'data' => [
'title' => 'Again Updated Date Test',
'$createdAt' => '2022-08-01 13:09:23.040', // $createdAt is not updatable
'$updatedAt' => '2022-08-01 13:09:23.050' // system will update it not api
'$createdAt' => '2022-08-01 13:09:23.040',
'$updatedAt' => '2022-08-01 13:09:23.050'
]
]);
if ($this->getSide() === 'client') {
$this->assertEquals($document['headers']['status-code'], 400);
} else {
$this->assertEquals($document['body']['title'], 'Again Updated Date Test');
$this->assertEquals($document['body']['$createdAt'], DateTime::formatTz('2022-08-01 13:09:23.040'));
$this->assertEquals($document['body']['$updatedAt'], DateTime::formatTz('2022-08-01 13:09:23.050'));
$this->assertEquals($document['body']['title'], 'Again Updated Date Test');
$this->assertEquals($document['body']['$createdAt'], $createdAt);
$this->assertNotEquals($document['body']['$createdAt'], '2022-08-01 13:09:23.040');
$this->assertNotEquals($document['body']['$updatedAt'], $updatedAt);
$this->assertNotEquals($document['body']['$updatedAt'], $updatedAtSecond);
$this->assertNotEquals($document['body']['$updatedAt'], '2022-08-01 13:09:23.050');
}
return $data;
}

View file

@ -889,4 +889,157 @@ 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

@ -5243,4 +5243,933 @@ class DatabasesCustomServerTest extends Scope
$this->assertEquals(400, $response['headers']['status-code']);
}
public function testDateTimeDocument(): void
{
$databaseId = $this->client->call(Client::METHOD_POST, '/databases', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'databaseId' => ID::unique(),
'name' => 'DateTime Test Database',
]);
$this->assertEquals(201, $databaseId['headers']['status-code']);
$databaseId = $databaseId['body']['$id'];
$collection = $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' => 'create_modify_dates',
'documentSecurity' => true,
'permissions' => [],
]);
$this->assertEquals(201, $collection['headers']['status-code']);
$collectionId = $collection['body']['$id'];
// Create string attribute
$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' => 'string',
'size' => 128,
'required' => false,
]);
// Create datetime attribute
$this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/datetime', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'key' => 'datetime',
'required' => false,
'format' => 'datetime',
]);
sleep(1);
$date = '2000-01-01T10:00:00.000+00:00';
// Test - default behaviour of external datetime attribute not changed
$doc = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'documentId' => 'doc1',
'data' => [
'datetime' => ''
],
'permissions' => [
Permission::read(Role::any()),
Permission::write(Role::any()),
Permission::update(Role::any()),
]
]);
$this->assertEquals(201, $doc['headers']['status-code']);
$this->assertNotEmpty($doc['body']['datetime']);
$this->assertNotEmpty($doc['body']['$createdAt']);
$this->assertNotEmpty($doc['body']['$updatedAt']);
$doc = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/doc1', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $doc['headers']['status-code']);
$this->assertNotEmpty($doc['body']['datetime']);
$this->assertNotEmpty($doc['body']['$createdAt']);
$this->assertNotEmpty($doc['body']['$updatedAt']);
// Test - modifying $createdAt and $updatedAt
$doc = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'documentId' => 'doc2',
'data' => [
'$createdAt' => $date
],
'permissions' => [
Permission::read(Role::any()),
Permission::write(Role::any()),
Permission::update(Role::any()),
]
]);
$this->assertEquals(201, $doc['headers']['status-code']);
$this->assertEquals($doc['body']['$createdAt'], $date);
$this->assertNotEmpty($doc['body']['$updatedAt']);
$this->assertNotEquals($doc['body']['$updatedAt'], $date);
$doc = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/doc2', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $doc['headers']['status-code']);
$this->assertEquals($doc['body']['$createdAt'], $date);
$this->assertNotEmpty($doc['body']['$updatedAt']);
$this->assertNotEquals($doc['body']['$updatedAt'], $date);
// 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']
]));
}
public function testSingleDocumentDateOperations(): void
{
$databaseId = $this->client->call(Client::METHOD_POST, '/databases', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'databaseId' => ID::unique(),
'name' => 'Single Date Operations Database',
]);
$this->assertEquals(201, $databaseId['headers']['status-code']);
$databaseId = $databaseId['body']['$id'];
$collection = $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' => 'normal_date_operations',
'documentSecurity' => true,
'permissions' => [],
]);
$this->assertEquals(201, $collection['headers']['status-code']);
$collectionId = $collection['body']['$id'];
// Create string attribute
$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' => 'string',
'size' => 128,
'required' => false,
]);
sleep(1);
$createDate = '2000-01-01T10:00:00.000+00:00';
$updateDate = '2000-02-01T15:30:00.000+00:00';
$date1 = '2000-01-01T10:00:00.000+00:00';
$date2 = '2000-02-01T15:30:00.000+00:00';
$date3 = '2000-03-01T20:45:00.000+00:00';
// Test 1: Create with custom createdAt, then update with custom updatedAt
$doc = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'documentId' => 'doc1',
'data' => [
'string' => 'initial',
'$createdAt' => $createDate
],
'permissions' => [
Permission::read(Role::any()),
Permission::write(Role::any()),
Permission::update(Role::any()),
]
]);
$this->assertEquals(201, $doc['headers']['status-code']);
$this->assertEquals($createDate, $doc['body']['$createdAt']);
$this->assertNotEquals($createDate, $doc['body']['$updatedAt']);
// Update with custom updatedAt
$updatedDoc = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/doc1', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'data' => [
'string' => 'updated',
'$updatedAt' => $updateDate
]
]);
$this->assertEquals(200, $updatedDoc['headers']['status-code']);
$this->assertEquals($createDate, $updatedDoc['body']['$createdAt']);
$this->assertEquals($updateDate, $updatedDoc['body']['$updatedAt']);
// Test 2: Create with both custom dates
$doc2 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'documentId' => 'doc2',
'data' => [
'string' => 'both_dates',
'$createdAt' => $createDate,
'$updatedAt' => $updateDate
],
'permissions' => [
Permission::read(Role::any()),
Permission::write(Role::any()),
Permission::update(Role::any()),
]
]);
$this->assertEquals(201, $doc2['headers']['status-code']);
$this->assertEquals($createDate, $doc2['body']['$createdAt']);
$this->assertEquals($updateDate, $doc2['body']['$updatedAt']);
// Test 3: Create without dates, then update with custom dates
$doc3 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'documentId' => 'doc3',
'data' => [
'string' => 'no_dates'
],
'permissions' => [
Permission::read(Role::any()),
Permission::write(Role::any()),
Permission::update(Role::any()),
]
]);
$this->assertEquals(201, $doc3['headers']['status-code']);
$updatedDoc3 = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/doc3', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'data' => [
'string' => 'updated_no_dates',
'$createdAt' => $createDate,
'$updatedAt' => $updateDate
]
]);
$this->assertEquals(200, $updatedDoc3['headers']['status-code']);
$this->assertEquals($createDate, $updatedDoc3['body']['$createdAt']);
$this->assertEquals($updateDate, $updatedDoc3['body']['$updatedAt']);
// Test 4: Update only createdAt
$doc4 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'documentId' => 'doc4',
'data' => [
'string' => 'initial'
],
'permissions' => [
Permission::read(Role::any()),
Permission::write(Role::any()),
Permission::update(Role::any()),
]
]);
$this->assertEquals(201, $doc4['headers']['status-code']);
$originalCreatedAt4 = $doc4['body']['$createdAt'];
$originalUpdatedAt4 = $doc4['body']['$updatedAt'];
$updatedDoc4 = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/doc4', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'data' => [
'string' => 'updated',
'$updatedAt' => null,
'$createdAt' => null
],
]);
$this->assertEquals(200, $updatedDoc4['headers']['status-code']);
$this->assertEquals($originalCreatedAt4, $updatedDoc4['body']['$createdAt']);
$this->assertNotEquals($originalUpdatedAt4, $updatedDoc4['body']['$updatedAt']);
// Test 5: Update only updatedAt
$finalDoc4 = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/doc4', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'data' => [
'string' => 'final',
'$updatedAt' => $updateDate,
'$createdAt' => $createDate
]
]);
$this->assertEquals(200, $finalDoc4['headers']['status-code']);
$this->assertEquals($createDate, $finalDoc4['body']['$createdAt']);
$this->assertEquals($updateDate, $finalDoc4['body']['$updatedAt']);
// Test 6: Create with updatedAt, update with createdAt
$doc5 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'documentId' => 'doc5',
'data' => [
'string' => 'doc5',
'$updatedAt' => $date2
],
'permissions' => [
Permission::read(Role::any()),
Permission::write(Role::any()),
Permission::update(Role::any()),
]
]);
$this->assertEquals(201, $doc5['headers']['status-code']);
$this->assertNotEquals($date2, $doc5['body']['$createdAt']);
$this->assertEquals($date2, $doc5['body']['$updatedAt']);
$updatedDoc5 = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/doc5', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'data' => [
'string' => 'doc5_updated',
'$createdAt' => $date1
]
]);
$this->assertEquals(200, $updatedDoc5['headers']['status-code']);
$this->assertEquals($date1, $updatedDoc5['body']['$createdAt']);
$this->assertNotEquals($date2, $updatedDoc5['body']['$updatedAt']);
// Test 7: Create with both dates, update with different dates
$doc6 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'documentId' => 'doc6',
'data' => [
'string' => 'doc6',
'$createdAt' => $date1,
'$updatedAt' => $date2
],
'permissions' => [
Permission::read(Role::any()),
Permission::write(Role::any()),
Permission::update(Role::any()),
]
]);
$this->assertEquals(201, $doc6['headers']['status-code']);
$this->assertEquals($date1, $doc6['body']['$createdAt']);
$this->assertEquals($date2, $doc6['body']['$updatedAt']);
$updatedDoc6 = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/doc6', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'data' => [
'string' => 'doc6_updated',
'$createdAt' => $date3,
'$updatedAt' => $date3
]
]);
$this->assertEquals(200, $updatedDoc6['headers']['status-code']);
$this->assertEquals($date3, $updatedDoc6['body']['$createdAt']);
$this->assertEquals($date3, $updatedDoc6['body']['$updatedAt']);
// 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']
]));
}
public function testBulkDocumentDateOperations(): void
{
$databaseId = $this->client->call(Client::METHOD_POST, '/databases', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'databaseId' => ID::unique(),
'name' => 'Bulk Date Operations Database',
]);
$this->assertEquals(201, $databaseId['headers']['status-code']);
$databaseId = $databaseId['body']['$id'];
$collection = $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' => 'bulk_date_operations',
'documentSecurity' => true,
'permissions' => [],
]);
$this->assertEquals(201, $collection['headers']['status-code']);
$collectionId = $collection['body']['$id'];
// Create string attribute
$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' => 'string',
'size' => 128,
'required' => false,
]);
sleep(1);
$createDate = '2000-01-01T10:00:00.000+00:00';
$updateDate = '2000-02-01T15:30:00.000+00:00';
// Test 1: Bulk create with different date configurations
$documents = [
[
'$id' => 'doc1',
'string' => 'doc1',
'$createdAt' => $createDate,
'$permissions' => [ Permission::read(Role::user($this->getUser()['$id'])),
Permission::update(Role::user($this->getUser()['$id'])),
Permission::delete(Role::user($this->getUser()['$id'])),]
],
[
'$id' => 'doc2',
'string' => 'doc2',
'$updatedAt' => $updateDate,
'$permissions' => [ Permission::read(Role::user($this->getUser()['$id'])),
Permission::update(Role::user($this->getUser()['$id'])),
Permission::delete(Role::user($this->getUser()['$id'])),]
],
[
'$id' => 'doc3',
'string' => 'doc3',
'$createdAt' => $createDate,
'$updatedAt' => $updateDate,
'$permissions' => [ Permission::read(Role::user($this->getUser()['$id'])),
Permission::update(Role::user($this->getUser()['$id'])),
Permission::delete(Role::user($this->getUser()['$id'])),]
],
[
'$id' => 'doc4',
'string' => 'doc4',
'$permissions' => [ Permission::read(Role::user($this->getUser()['$id'])),
Permission::update(Role::user($this->getUser()['$id'])),
Permission::delete(Role::user($this->getUser()['$id'])),]
],
[
'$id' => 'doc5',
'string' => 'doc5',
'$createdAt' => null,
'$permissions' => [ Permission::read(Role::user($this->getUser()['$id'])),
Permission::update(Role::user($this->getUser()['$id'])),
Permission::delete(Role::user($this->getUser()['$id'])),]
],
[
'$id' => 'doc6',
'string' => 'doc6',
'$updatedAt' => null,
'$permissions' => [ Permission::read(Role::user($this->getUser()['$id'])),
Permission::update(Role::user($this->getUser()['$id'])),
Permission::delete(Role::user($this->getUser()['$id'])),]
]
];
// Create all documents in one bulk operation
$bulkCreateResponse = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'documents' => $documents
]);
$this->assertEquals(201, $bulkCreateResponse['headers']['status-code']);
$this->assertCount(count($documents), $bulkCreateResponse['body']['documents']);
// Verify initial state
foreach (['doc1', 'doc3'] as $id) {
$doc = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $doc['headers']['status-code']);
$this->assertEquals($createDate, $doc['body']['$createdAt'], "createdAt mismatch for $id");
}
foreach (['doc2', 'doc3'] as $id) {
$doc = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $doc['headers']['status-code']);
$this->assertEquals($updateDate, $doc['body']['$updatedAt'], "updatedAt mismatch for $id");
}
foreach (['doc4', 'doc5', 'doc6'] as $id) {
$doc = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $doc['headers']['status-code']);
$this->assertNotEmpty($doc['body']['$createdAt'], "createdAt missing for $id");
$this->assertNotEmpty($doc['body']['$updatedAt'], "updatedAt missing for $id");
}
// Test 2: Bulk update with custom dates
$updateData = [
'data' => [
'string' => 'updated',
'$createdAt' => $createDate,
'$updatedAt' => $updateDate,
'$permissions' => [ Permission::read(Role::user($this->getUser()['$id'])),
Permission::update(Role::user($this->getUser()['$id'])),
Permission::delete(Role::user($this->getUser()['$id'])),]
],
];
// Use bulk update instead of individual updates
$response = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), $updateData);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertCount(6, $response['body']['documents']);
// Verify updated state
foreach (['doc1', 'doc3'] as $id) {
$doc = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $doc['headers']['status-code']);
$this->assertEquals($createDate, $doc['body']['$createdAt'], "createdAt mismatch for $id");
$this->assertEquals($updateDate, $doc['body']['$updatedAt'], "updatedAt mismatch for $id");
$this->assertEquals('updated', $doc['body']['string'], "string mismatch for $id");
}
foreach (['doc2', 'doc4', 'doc5', 'doc6'] as $id) {
$doc = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $doc['headers']['status-code']);
$this->assertEquals($updateDate, $doc['body']['$updatedAt'], "updatedAt mismatch for $id");
$this->assertEquals('updated', $doc['body']['string'], "string mismatch for $id");
}
$newDate = '2000-03-01T20:45:00.000+00:00';
$updateDataEnabled = [
'data' => [
'string' => 'enabled_update',
'$createdAt' => $newDate,
'$updatedAt' => $newDate
],
];
// Use bulk update instead of individual updates
$response = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), $updateDataEnabled);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertCount(6, $response['body']['documents']);
// Verify final state
foreach (['doc1', 'doc2', 'doc3', 'doc4', 'doc5', 'doc6'] as $id) {
$doc = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $doc['headers']['status-code']);
$this->assertEquals($newDate, $doc['body']['$createdAt'], "createdAt mismatch for $id");
$this->assertEquals($newDate, $doc['body']['$updatedAt'], "updatedAt mismatch for $id");
$this->assertEquals('enabled_update', $doc['body']['string'], "string mismatch for $id");
}
// 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']
]));
}
public function testUpsertDateOperations(): void
{
$databaseId = $this->client->call(Client::METHOD_POST, '/databases', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'databaseId' => ID::unique(),
'name' => 'Upsert Date Operations Database',
]);
$this->assertEquals(201, $databaseId['headers']['status-code']);
$databaseId = $databaseId['body']['$id'];
$collection = $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' => 'upsert_date_operations',
'documentSecurity' => true,
'permissions' => [],
]);
$this->assertEquals(201, $collection['headers']['status-code']);
$collectionId = $collection['body']['$id'];
// Create string attribute
$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' => 'string',
'size' => 128,
'required' => false,
]);
sleep(1);
$createDate = '2000-01-01T10:00:00.000+00:00';
$updateDate = '2000-02-01T15:30:00.000+00:00';
$date1 = '2000-01-01T10:00:00.000+00:00';
$date2 = '2000-02-01T15:30:00.000+00:00';
$date3 = '2000-03-01T20:45:00.000+00:00';
// Test 1: Upsert new document with custom createdAt
$upsertDoc1 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'documentId' => 'upsert1',
'data' => [
'string' => 'upsert1_initial',
'$permissions' => [
Permission::read(Role::any()),
Permission::write(Role::any()),
Permission::update(Role::any()),
],
'$createdAt' => $createDate
],
]);
$this->assertEquals(201, $upsertDoc1['headers']['status-code']);
$this->assertEquals($createDate, $upsertDoc1['body']['$createdAt']);
$this->assertNotEquals($createDate, $upsertDoc1['body']['$updatedAt']);
// Test 2: Upsert existing document with custom updatedAt
$updatedUpsertDoc1 = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/upsert1', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'data' => [
'string' => 'upsert1_updated',
'$updatedAt' => $updateDate
],
]);
$this->assertEquals(200, $updatedUpsertDoc1['headers']['status-code']);
$this->assertEquals($createDate, $updatedUpsertDoc1['body']['$createdAt']);
$this->assertEquals($updateDate, $updatedUpsertDoc1['body']['$updatedAt']);
// Test 3: Upsert new document with both custom dates
$upsertDoc2 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'documentId' => 'upsert2',
'data' => [
'string' => 'upsert2_both_dates',
'$permissions' => [
Permission::read(Role::any()),
Permission::write(Role::any()),
Permission::update(Role::any()),
],
'$createdAt' => $createDate,
'$updatedAt' => $updateDate
],
]);
$this->assertEquals(201, $upsertDoc2['headers']['status-code']);
$this->assertEquals($createDate, $upsertDoc2['body']['$createdAt']);
$this->assertEquals($updateDate, $upsertDoc2['body']['$updatedAt']);
// Test 4: Upsert existing document with different dates
$updatedUpsertDoc2 = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/upsert2', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'data' => [
'string' => 'upsert2_updated',
'$createdAt' => $date3,
'$updatedAt' => $date3,
'$permissions' => [
Permission::read(Role::any()),
Permission::write(Role::any()),
Permission::update(Role::any()),
],
]
]);
$this->assertEquals(200, $updatedUpsertDoc2['headers']['status-code']);
$this->assertEquals($date3, $updatedUpsertDoc2['body']['$createdAt']);
$this->assertEquals($date3, $updatedUpsertDoc2['body']['$updatedAt']);
// Test 5: Bulk upsert operations with custom dates
$upsertDocuments = [
[
'$id' => 'bulk_upsert1',
'string' => 'bulk_upsert1_initial',
'$createdAt' => $createDate
],
[
'$id' => 'bulk_upsert2',
'string' => 'bulk_upsert2_initial',
'$updatedAt' => $updateDate
],
[
'$id' => 'bulk_upsert3',
'string' => 'bulk_upsert3_initial',
'$createdAt' => $createDate,
'$updatedAt' => $updateDate
],
[
'$id' => 'bulk_upsert4',
'string' => 'bulk_upsert4_initial'
]
];
// Create documents using bulk upsert
$response = $this->client->call(Client::METHOD_PUT, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'documents' => $upsertDocuments
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertCount(4, $response['body']['documents']);
// Test 7: Verify initial bulk upsert state
foreach (['bulk_upsert1', 'bulk_upsert3'] as $id) {
$doc = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $doc['headers']['status-code']);
$this->assertEquals($createDate, $doc['body']['$createdAt'], "createdAt mismatch for $id");
}
foreach (['bulk_upsert2', 'bulk_upsert3'] as $id) {
$doc = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $doc['headers']['status-code']);
$this->assertEquals($updateDate, $doc['body']['$updatedAt'], "updatedAt mismatch for $id");
}
foreach (['bulk_upsert4'] as $id) {
$doc = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $doc['headers']['status-code']);
$this->assertNotEmpty($doc['body']['$createdAt'], "createdAt missing for $id");
$this->assertNotEmpty($doc['body']['$updatedAt'], "updatedAt missing for $id");
}
// Test 8: Bulk upsert update with custom dates
$newDate = '2000-04-01T12:00:00.000+00:00';
$updateUpsertData = [
'data' => [
'string' => 'bulk_upsert_updated',
'$createdAt' => $newDate,
'$updatedAt' => $newDate
],
'queries' => [Query::equal('$id', ['bulk_upsert1','bulk_upsert2','bulk_upsert3','bulk_upsert4'])->toString()]
];
$upsertIds = ['bulk_upsert1', 'bulk_upsert2', 'bulk_upsert3', 'bulk_upsert4'];
// Use bulk update instead of individual updates
$response = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), $updateUpsertData);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertCount(4, $response['body']['documents']);
// Verify updated state
foreach ($upsertIds as $id) {
$doc = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $doc['headers']['status-code']);
$this->assertEquals($newDate, $doc['body']['$createdAt'], "createdAt mismatch for $id");
$this->assertEquals($newDate, $doc['body']['$updatedAt'], "updatedAt mismatch for $id");
$this->assertEquals('bulk_upsert_updated', $doc['body']['string'], "string mismatch for $id");
}
// Test 9: checking by passing null to each
$updateUpsertDataNull = [
'data' => [
'string' => 'bulk_upsert_null_test',
'$createdAt' => null,
'$updatedAt' => null
],
'queries' => [Query::equal('$id', ['bulk_upsert1','bulk_upsert2','bulk_upsert3','bulk_upsert4'])->toString()]
];
// Use bulk update instead of individual updates
$response = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), $updateUpsertDataNull);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertCount(4, $response['body']['documents']);
// Verify null handling
foreach ($upsertIds as $id) {
$doc = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $doc['headers']['status-code']);
$this->assertNotEmpty($doc['body']['$createdAt'], "createdAt missing for $id");
$this->assertNotEmpty($doc['body']['$updatedAt'], "updatedAt missing for $id");
}
// 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']
]));
}
}