added modify createdAt/updatedAt support

This commit is contained in:
ArnabChatterjee20k
2025-07-31 20:32:14 +05:30
parent 767f575984
commit de69692ff2
10 changed files with 1246 additions and 91 deletions
+1 -1
View File
@@ -52,7 +52,7 @@
"utopia-php/cache": "0.13.*",
"utopia-php/cli": "0.15.*",
"utopia-php/config": "0.2.*",
"utopia-php/database": "0.73.0",
"utopia-php/database": "0.74.*",
"utopia-php/detector": "0.1.*",
"utopia-php/domains": "0.8.*",
"utopia-php/dsn": "0.2.1",
Generated
+73 -49
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "f4876940be4499bea26a77a036dece4f",
"content-hash": "43a92af943693f80341a8582cf582986",
"packages": [
{
"name": "adhocore/jwt",
@@ -2547,16 +2547,16 @@
},
{
"name": "symfony/http-client",
"version": "v7.3.1",
"version": "v7.3.2",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-client.git",
"reference": "4403d87a2c16f33345dca93407a8714ee8c05a64"
"reference": "1c064a0c67749923483216b081066642751cc2c7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-client/zipball/4403d87a2c16f33345dca93407a8714ee8c05a64",
"reference": "4403d87a2c16f33345dca93407a8714ee8c05a64",
"url": "https://api.github.com/repos/symfony/http-client/zipball/1c064a0c67749923483216b081066642751cc2c7",
"reference": "1c064a0c67749923483216b081066642751cc2c7",
"shasum": ""
},
"require": {
@@ -2622,7 +2622,7 @@
"http"
],
"support": {
"source": "https://github.com/symfony/http-client/tree/v7.3.1"
"source": "https://github.com/symfony/http-client/tree/v7.3.2"
},
"funding": [
{
@@ -2633,12 +2633,16 @@
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2025-06-28T07:58:39+00:00"
"time": "2025-07-15T11:36:08+00:00"
},
{
"name": "symfony/http-client-contracts",
@@ -3493,16 +3497,16 @@
},
{
"name": "utopia-php/database",
"version": "0.73.0",
"version": "0.74.3",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/database.git",
"reference": "f9b6c587e54069dc64c2d29174ce8a42b7ba6491"
"reference": "065b4812799d57fd2c596f88aadd51644247c7e3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/database/zipball/f9b6c587e54069dc64c2d29174ce8a42b7ba6491",
"reference": "f9b6c587e54069dc64c2d29174ce8a42b7ba6491",
"url": "https://api.github.com/repos/utopia-php/database/zipball/065b4812799d57fd2c596f88aadd51644247c7e3",
"reference": "065b4812799d57fd2c596f88aadd51644247c7e3",
"shasum": ""
},
"require": {
@@ -3543,9 +3547,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/database/issues",
"source": "https://github.com/utopia-php/database/tree/0.73.0"
"source": "https://github.com/utopia-php/database/tree/0.74.3"
},
"time": "2025-07-27T04:10:06+00:00"
"time": "2025-07-31T15:00:30+00:00"
},
{
"name": "utopia-php/detector",
@@ -4810,16 +4814,16 @@
"packages-dev": [
{
"name": "appwrite/sdk-generator",
"version": "0.41.24",
"version": "0.41.26",
"source": {
"type": "git",
"url": "https://github.com/appwrite/sdk-generator.git",
"reference": "c9ebf8daa25332ca701515d42e8d0c4a7be6a489"
"reference": "5a13191a5a4bdec8fe1b1180ff67f75c4ff6ac0b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/c9ebf8daa25332ca701515d42e8d0c4a7be6a489",
"reference": "c9ebf8daa25332ca701515d42e8d0c4a7be6a489",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/5a13191a5a4bdec8fe1b1180ff67f75c4ff6ac0b",
"reference": "5a13191a5a4bdec8fe1b1180ff67f75c4ff6ac0b",
"shasum": ""
},
"require": {
@@ -4855,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.24"
"source": "https://github.com/appwrite/sdk-generator/tree/0.41.26"
},
"time": "2025-07-28T06:27:00+00:00"
"time": "2025-07-30T06:53:12+00:00"
},
{
"name": "doctrine/annotations",
@@ -7258,16 +7262,16 @@
},
{
"name": "symfony/console",
"version": "v7.3.1",
"version": "v7.3.2",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "9e27aecde8f506ba0fd1d9989620c04a87697101"
"reference": "5f360ebc65c55265a74d23d7fe27f957870158a1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/9e27aecde8f506ba0fd1d9989620c04a87697101",
"reference": "9e27aecde8f506ba0fd1d9989620c04a87697101",
"url": "https://api.github.com/repos/symfony/console/zipball/5f360ebc65c55265a74d23d7fe27f957870158a1",
"reference": "5f360ebc65c55265a74d23d7fe27f957870158a1",
"shasum": ""
},
"require": {
@@ -7332,7 +7336,7 @@
"terminal"
],
"support": {
"source": "https://github.com/symfony/console/tree/v7.3.1"
"source": "https://github.com/symfony/console/tree/v7.3.2"
},
"funding": [
{
@@ -7343,25 +7347,29 @@
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2025-06-27T19:55:54+00:00"
"time": "2025-07-30T17:13:41+00:00"
},
{
"name": "symfony/filesystem",
"version": "v7.3.0",
"version": "v7.3.2",
"source": {
"type": "git",
"url": "https://github.com/symfony/filesystem.git",
"reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb"
"reference": "edcbb768a186b5c3f25d0643159a787d3e63b7fd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/filesystem/zipball/b8dce482de9d7c9fe2891155035a7248ab5c7fdb",
"reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb",
"url": "https://api.github.com/repos/symfony/filesystem/zipball/edcbb768a186b5c3f25d0643159a787d3e63b7fd",
"reference": "edcbb768a186b5c3f25d0643159a787d3e63b7fd",
"shasum": ""
},
"require": {
@@ -7398,7 +7406,7 @@
"description": "Provides basic utilities for the filesystem",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/filesystem/tree/v7.3.0"
"source": "https://github.com/symfony/filesystem/tree/v7.3.2"
},
"funding": [
{
@@ -7409,25 +7417,29 @@
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-10-25T15:15:23+00:00"
"time": "2025-07-07T08:17:47+00:00"
},
{
"name": "symfony/finder",
"version": "v7.3.0",
"version": "v7.3.2",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
"reference": "ec2344cf77a48253bbca6939aa3d2477773ea63d"
"reference": "2a6614966ba1074fa93dae0bc804227422df4dfe"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/ec2344cf77a48253bbca6939aa3d2477773ea63d",
"reference": "ec2344cf77a48253bbca6939aa3d2477773ea63d",
"url": "https://api.github.com/repos/symfony/finder/zipball/2a6614966ba1074fa93dae0bc804227422df4dfe",
"reference": "2a6614966ba1074fa93dae0bc804227422df4dfe",
"shasum": ""
},
"require": {
@@ -7462,7 +7474,7 @@
"description": "Finds files and directories via an intuitive fluent interface",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/finder/tree/v7.3.0"
"source": "https://github.com/symfony/finder/tree/v7.3.2"
},
"funding": [
{
@@ -7473,25 +7485,29 @@
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-12-30T19:00:26+00:00"
"time": "2025-07-15T13:41:35+00:00"
},
{
"name": "symfony/options-resolver",
"version": "v7.3.0",
"version": "v7.3.2",
"source": {
"type": "git",
"url": "https://github.com/symfony/options-resolver.git",
"reference": "afb9a8038025e5dbc657378bfab9198d75f10fca"
"reference": "119bcf13e67dbd188e5dbc74228b1686f66acd37"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/options-resolver/zipball/afb9a8038025e5dbc657378bfab9198d75f10fca",
"reference": "afb9a8038025e5dbc657378bfab9198d75f10fca",
"url": "https://api.github.com/repos/symfony/options-resolver/zipball/119bcf13e67dbd188e5dbc74228b1686f66acd37",
"reference": "119bcf13e67dbd188e5dbc74228b1686f66acd37",
"shasum": ""
},
"require": {
@@ -7529,7 +7545,7 @@
"options"
],
"support": {
"source": "https://github.com/symfony/options-resolver/tree/v7.3.0"
"source": "https://github.com/symfony/options-resolver/tree/v7.3.2"
},
"funding": [
{
@@ -7540,12 +7556,16 @@
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2025-04-04T13:12:05+00:00"
"time": "2025-07-15T11:36:08+00:00"
},
{
"name": "symfony/polyfill-ctype",
@@ -7924,16 +7944,16 @@
},
{
"name": "symfony/string",
"version": "v7.3.0",
"version": "v7.3.2",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
"reference": "f3570b8c61ca887a9e2938e85cb6458515d2b125"
"reference": "42f505aff654e62ac7ac2ce21033818297ca89ca"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/string/zipball/f3570b8c61ca887a9e2938e85cb6458515d2b125",
"reference": "f3570b8c61ca887a9e2938e85cb6458515d2b125",
"url": "https://api.github.com/repos/symfony/string/zipball/42f505aff654e62ac7ac2ce21033818297ca89ca",
"reference": "42f505aff654e62ac7ac2ce21033818297ca89ca",
"shasum": ""
},
"require": {
@@ -7991,7 +8011,7 @@
"utf8"
],
"support": {
"source": "https://github.com/symfony/string/tree/v7.3.0"
"source": "https://github.com/symfony/string/tree/v7.3.2"
},
"funding": [
{
@@ -8002,12 +8022,16 @@
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2025-04-20T20:19:01+00:00"
"time": "2025-07-10T08:47:49+00:00"
},
{
"name": "textalk/websocket",
@@ -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) {
@@ -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) {
@@ -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) {
$document['$collection'] = $collection->getId();
// Determine the source ID depending on whether it's a bulk operation.
@@ -350,6 +350,20 @@ 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)
$createdAt = $document['$createdAt'] ?? null;
$updatedAt = $document['$updatedAt'] ?? null;
if (!$isAPIKey) {
if ($createdAt !== null) {
throw new Exception($this->getInvalidStructureException(), 'Attribute "$createdAt" is not allowed');
}
if ($updatedAt !== null) {
throw new Exception($this->getInvalidStructureException(), 'Attribute "$updatedAt" is not allowed');
}
}
$document = new Document($document);
$setPermissions($document, $permissions);
$checkPermissions($collection, $document, Database::PERMISSION_CREATE);
@@ -358,9 +372,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());
@@ -109,6 +109,18 @@ class Update extends Action
throw new Exception($this->getParentNotFoundException());
}
// Allowing to add createdAt and updatedAt timestamps if server side(api key)
$createdAt = $data['$createdAt'] ?? null;
$updatedAt = $data['$updatedAt'] ?? null;
if (!$isAPIKey) {
if ($createdAt !== null) {
throw new Exception($this->getInvalidStructureException(), 'Attribute "$createdAt" is not allowed');
}
if ($updatedAt !== null) {
throw new Exception($this->getInvalidStructureException(), 'Attribute "$updatedAt" is not allowed');
}
}
// 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 +245,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());
@@ -153,6 +153,18 @@ class Upsert extends Action
}
}
}
// Allowing to add createdAt and updatedAt timestamps if server side(api key)
$createdAt = $data['$createdAt'] ?? null;
$updatedAt = $data['$updatedAt'] ?? null;
if (!$isAPIKey) {
if ($createdAt !== null) {
throw new Exception($this->getInvalidStructureException(), 'Attribute "$createdAt" is not allowed');
}
if ($updatedAt !== null) {
throw new Exception($this->getInvalidStructureException(), 'Attribute "$updatedAt" is not allowed');
}
}
$data['$id'] = $documentId;
$data['$permissions'] = $permissions ?? [];
@@ -236,13 +248,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) {
@@ -2876,7 +2876,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'])),
@@ -4206,17 +4205,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;
}
@@ -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']
]));
}
}
@@ -5243,4 +5243,936 @@ 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']
]));
}
}