mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Merge remote-tracking branch 'upstream/1.8.x' into documents-db-api
This commit is contained in:
@@ -103,6 +103,11 @@ CLI::setResource('console', function () {
|
||||
return new Document(Config::getParam('console'));
|
||||
}, []);
|
||||
|
||||
CLI::setResource(
|
||||
'isResourceBlocked',
|
||||
fn () => fn (Document $project, string $resourceType, ?string $resourceId) => false
|
||||
);
|
||||
|
||||
CLI::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache) {
|
||||
$databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools
|
||||
|
||||
|
||||
@@ -578,6 +578,11 @@ return [
|
||||
'description' => 'The requested runtime is either inactive or unsupported. Please check the value of the _APP_FUNCTIONS_RUNTIMES environment variable.',
|
||||
'code' => 404,
|
||||
],
|
||||
Exception::FUNCTION_ALREADY_EXISTS => [
|
||||
'name' => Exception::FUNCTION_ALREADY_EXISTS,
|
||||
'description' => 'Function with the requested ID already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
|
||||
'code' => 409,
|
||||
],
|
||||
Exception::FUNCTION_ENTRYPOINT_MISSING => [
|
||||
'name' => Exception::FUNCTION_ENTRYPOINT_MISSING,
|
||||
'description' => 'Entrypoint for your Appwrite Function is missing. Please specify it when making deployment or update the entrypoint under your function\'s "Settings" > "Configuration" > "Entrypoint".',
|
||||
|
||||
@@ -357,7 +357,7 @@ return [
|
||||
[
|
||||
'key' => 'dotnet',
|
||||
'name' => '.NET',
|
||||
'version' => '0.21.1',
|
||||
'version' => '0.21.2',
|
||||
'url' => 'https://github.com/appwrite/sdk-for-dotnet',
|
||||
'package' => 'https://www.nuget.org/packages/Appwrite',
|
||||
'enabled' => true,
|
||||
|
||||
@@ -5247,7 +5247,7 @@
|
||||
},
|
||||
"\/databases\/transactions\/{transactionId}\/operations": {
|
||||
"post": {
|
||||
"summary": "Create operations scoped to a transaction",
|
||||
"summary": "Create operations",
|
||||
"operationId": "databasesCreateOperations",
|
||||
"tags": [
|
||||
"databases"
|
||||
@@ -8389,7 +8389,7 @@
|
||||
},
|
||||
"\/tablesdb\/transactions\/{transactionId}\/operations": {
|
||||
"post": {
|
||||
"summary": "Create operations scoped to a transaction",
|
||||
"summary": "Create operations",
|
||||
"operationId": "tablesDBCreateOperations",
|
||||
"tags": [
|
||||
"tablesDB"
|
||||
|
||||
@@ -5646,7 +5646,7 @@
|
||||
},
|
||||
"\/databases\/transactions\/{transactionId}\/operations": {
|
||||
"post": {
|
||||
"summary": "Create operations scoped to a transaction",
|
||||
"summary": "Create operations",
|
||||
"operationId": "databasesCreateOperations",
|
||||
"tags": [
|
||||
"databases"
|
||||
@@ -33781,7 +33781,7 @@
|
||||
},
|
||||
"\/tablesdb\/transactions\/{transactionId}\/operations": {
|
||||
"post": {
|
||||
"summary": "Create operations scoped to a transaction",
|
||||
"summary": "Create operations",
|
||||
"operationId": "tablesDBCreateOperations",
|
||||
"tags": [
|
||||
"tablesDB"
|
||||
|
||||
@@ -5198,7 +5198,7 @@
|
||||
},
|
||||
"\/databases\/transactions\/{transactionId}\/operations": {
|
||||
"post": {
|
||||
"summary": "Create operations scoped to a transaction",
|
||||
"summary": "Create operations",
|
||||
"operationId": "databasesCreateOperations",
|
||||
"tags": [
|
||||
"databases"
|
||||
@@ -24311,7 +24311,7 @@
|
||||
},
|
||||
"\/tablesdb\/transactions\/{transactionId}\/operations": {
|
||||
"post": {
|
||||
"summary": "Create operations scoped to a transaction",
|
||||
"summary": "Create operations",
|
||||
"operationId": "tablesDBCreateOperations",
|
||||
"tags": [
|
||||
"tablesDB"
|
||||
|
||||
@@ -5247,7 +5247,7 @@
|
||||
},
|
||||
"\/databases\/transactions\/{transactionId}\/operations": {
|
||||
"post": {
|
||||
"summary": "Create operations scoped to a transaction",
|
||||
"summary": "Create operations",
|
||||
"operationId": "databasesCreateOperations",
|
||||
"tags": [
|
||||
"databases"
|
||||
@@ -8389,7 +8389,7 @@
|
||||
},
|
||||
"\/tablesdb\/transactions\/{transactionId}\/operations": {
|
||||
"post": {
|
||||
"summary": "Create operations scoped to a transaction",
|
||||
"summary": "Create operations",
|
||||
"operationId": "tablesDBCreateOperations",
|
||||
"tags": [
|
||||
"tablesDB"
|
||||
|
||||
@@ -5646,7 +5646,7 @@
|
||||
},
|
||||
"\/databases\/transactions\/{transactionId}\/operations": {
|
||||
"post": {
|
||||
"summary": "Create operations scoped to a transaction",
|
||||
"summary": "Create operations",
|
||||
"operationId": "databasesCreateOperations",
|
||||
"tags": [
|
||||
"databases"
|
||||
@@ -33781,7 +33781,7 @@
|
||||
},
|
||||
"\/tablesdb\/transactions\/{transactionId}\/operations": {
|
||||
"post": {
|
||||
"summary": "Create operations scoped to a transaction",
|
||||
"summary": "Create operations",
|
||||
"operationId": "tablesDBCreateOperations",
|
||||
"tags": [
|
||||
"tablesDB"
|
||||
|
||||
@@ -5198,7 +5198,7 @@
|
||||
},
|
||||
"\/databases\/transactions\/{transactionId}\/operations": {
|
||||
"post": {
|
||||
"summary": "Create operations scoped to a transaction",
|
||||
"summary": "Create operations",
|
||||
"operationId": "databasesCreateOperations",
|
||||
"tags": [
|
||||
"databases"
|
||||
@@ -24311,7 +24311,7 @@
|
||||
},
|
||||
"\/tablesdb\/transactions\/{transactionId}\/operations": {
|
||||
"post": {
|
||||
"summary": "Create operations scoped to a transaction",
|
||||
"summary": "Create operations",
|
||||
"operationId": "tablesDBCreateOperations",
|
||||
"tags": [
|
||||
"tablesDB"
|
||||
|
||||
@@ -5386,7 +5386,7 @@
|
||||
},
|
||||
"\/databases\/transactions\/{transactionId}\/operations": {
|
||||
"post": {
|
||||
"summary": "Create operations scoped to a transaction",
|
||||
"summary": "Create operations",
|
||||
"operationId": "databasesCreateOperations",
|
||||
"consumes": [
|
||||
"application\/json"
|
||||
@@ -8463,7 +8463,7 @@
|
||||
},
|
||||
"\/tablesdb\/transactions\/{transactionId}\/operations": {
|
||||
"post": {
|
||||
"summary": "Create operations scoped to a transaction",
|
||||
"summary": "Create operations",
|
||||
"operationId": "tablesDBCreateOperations",
|
||||
"consumes": [
|
||||
"application\/json"
|
||||
|
||||
@@ -5805,7 +5805,7 @@
|
||||
},
|
||||
"\/databases\/transactions\/{transactionId}\/operations": {
|
||||
"post": {
|
||||
"summary": "Create operations scoped to a transaction",
|
||||
"summary": "Create operations",
|
||||
"operationId": "databasesCreateOperations",
|
||||
"consumes": [
|
||||
"application\/json"
|
||||
@@ -33897,7 +33897,7 @@
|
||||
},
|
||||
"\/tablesdb\/transactions\/{transactionId}\/operations": {
|
||||
"post": {
|
||||
"summary": "Create operations scoped to a transaction",
|
||||
"summary": "Create operations",
|
||||
"operationId": "tablesDBCreateOperations",
|
||||
"consumes": [
|
||||
"application\/json"
|
||||
|
||||
@@ -5345,7 +5345,7 @@
|
||||
},
|
||||
"\/databases\/transactions\/{transactionId}\/operations": {
|
||||
"post": {
|
||||
"summary": "Create operations scoped to a transaction",
|
||||
"summary": "Create operations",
|
||||
"operationId": "databasesCreateOperations",
|
||||
"consumes": [
|
||||
"application\/json"
|
||||
@@ -24483,7 +24483,7 @@
|
||||
},
|
||||
"\/tablesdb\/transactions\/{transactionId}\/operations": {
|
||||
"post": {
|
||||
"summary": "Create operations scoped to a transaction",
|
||||
"summary": "Create operations",
|
||||
"operationId": "tablesDBCreateOperations",
|
||||
"consumes": [
|
||||
"application\/json"
|
||||
|
||||
@@ -5386,7 +5386,7 @@
|
||||
},
|
||||
"\/databases\/transactions\/{transactionId}\/operations": {
|
||||
"post": {
|
||||
"summary": "Create operations scoped to a transaction",
|
||||
"summary": "Create operations",
|
||||
"operationId": "databasesCreateOperations",
|
||||
"consumes": [
|
||||
"application\/json"
|
||||
@@ -8463,7 +8463,7 @@
|
||||
},
|
||||
"\/tablesdb\/transactions\/{transactionId}\/operations": {
|
||||
"post": {
|
||||
"summary": "Create operations scoped to a transaction",
|
||||
"summary": "Create operations",
|
||||
"operationId": "tablesDBCreateOperations",
|
||||
"consumes": [
|
||||
"application\/json"
|
||||
|
||||
@@ -5805,7 +5805,7 @@
|
||||
},
|
||||
"\/databases\/transactions\/{transactionId}\/operations": {
|
||||
"post": {
|
||||
"summary": "Create operations scoped to a transaction",
|
||||
"summary": "Create operations",
|
||||
"operationId": "databasesCreateOperations",
|
||||
"consumes": [
|
||||
"application\/json"
|
||||
@@ -33897,7 +33897,7 @@
|
||||
},
|
||||
"\/tablesdb\/transactions\/{transactionId}\/operations": {
|
||||
"post": {
|
||||
"summary": "Create operations scoped to a transaction",
|
||||
"summary": "Create operations",
|
||||
"operationId": "tablesDBCreateOperations",
|
||||
"consumes": [
|
||||
"application\/json"
|
||||
|
||||
@@ -5345,7 +5345,7 @@
|
||||
},
|
||||
"\/databases\/transactions\/{transactionId}\/operations": {
|
||||
"post": {
|
||||
"summary": "Create operations scoped to a transaction",
|
||||
"summary": "Create operations",
|
||||
"operationId": "databasesCreateOperations",
|
||||
"consumes": [
|
||||
"application\/json"
|
||||
@@ -24483,7 +24483,7 @@
|
||||
},
|
||||
"\/tablesdb\/transactions\/{transactionId}\/operations": {
|
||||
"post": {
|
||||
"summary": "Create operations scoped to a transaction",
|
||||
"summary": "Create operations",
|
||||
"operationId": "tablesDBCreateOperations",
|
||||
"consumes": [
|
||||
"application\/json"
|
||||
|
||||
+4
-4
@@ -606,8 +606,8 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
|
||||
$message = $th->getMessage();
|
||||
|
||||
// sanitize 5xx errors
|
||||
if ($code >= 500 && !App::isDevelopment()) {
|
||||
// sanitize 0 && 5xx errors
|
||||
if (($code === 0 || $code >= 500) && !App::isDevelopment()) {
|
||||
$message = 'Error: Server Error';
|
||||
}
|
||||
|
||||
@@ -719,8 +719,8 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
|
||||
|
||||
$message = $th->getMessage();
|
||||
|
||||
// sanitize 5xx errors
|
||||
if ($code >= 500 && !App::isDevelopment()) {
|
||||
// sanitize 0 && 5xx errors
|
||||
if (($code === 0 || $code >= 500) && !App::isDevelopment()) {
|
||||
$message = 'Error: Server Error';
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -64,7 +64,7 @@
|
||||
"utopia-php/image": "0.8.*",
|
||||
"utopia-php/locale": "0.8.*",
|
||||
"utopia-php/logger": "0.6.*",
|
||||
"utopia-php/messaging": "0.18.*",
|
||||
"utopia-php/messaging": "0.19.*",
|
||||
"utopia-php/migration": "dev-feat-documents-db as 1.1.1",
|
||||
"utopia-php/orchestration": "0.9.*",
|
||||
"utopia-php/platform": "0.7.*",
|
||||
|
||||
Generated
+11
-11
@@ -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": "1e4422c6cbca9b7b99c329ecc02e695d",
|
||||
"content-hash": "5634dc368b359aa28cbb2960c32bccd3",
|
||||
"packages": [
|
||||
{
|
||||
"name": "adhocore/jwt",
|
||||
@@ -4290,16 +4290,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/messaging",
|
||||
"version": "0.18.2",
|
||||
"version": "0.19.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/messaging.git",
|
||||
"reference": "0d364edacf4d4867964c7e17f653031dd39394bf"
|
||||
"reference": "0b866d54e70c792a3c4f5ca9c12a6d358a31f4b8"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/messaging/zipball/0d364edacf4d4867964c7e17f653031dd39394bf",
|
||||
"reference": "0d364edacf4d4867964c7e17f653031dd39394bf",
|
||||
"url": "https://api.github.com/repos/utopia-php/messaging/zipball/0b866d54e70c792a3c4f5ca9c12a6d358a31f4b8",
|
||||
"reference": "0b866d54e70c792a3c4f5ca9c12a6d358a31f4b8",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4335,9 +4335,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/messaging/issues",
|
||||
"source": "https://github.com/utopia-php/messaging/tree/0.18.2"
|
||||
"source": "https://github.com/utopia-php/messaging/tree/0.19.0"
|
||||
},
|
||||
"time": "2025-07-21T18:27:03+00:00"
|
||||
"time": "2025-10-14T11:46:49+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/migration",
|
||||
@@ -4345,12 +4345,12 @@
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/migration.git",
|
||||
"reference": "b77e8c2ced7f6dc412dc2669b43a719fba19fcd6"
|
||||
"reference": "0a3abfaad2e4a35280d4e401cd510147d6ddd719"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/migration/zipball/b77e8c2ced7f6dc412dc2669b43a719fba19fcd6",
|
||||
"reference": "b77e8c2ced7f6dc412dc2669b43a719fba19fcd6",
|
||||
"url": "https://api.github.com/repos/utopia-php/migration/zipball/0a3abfaad2e4a35280d4e401cd510147d6ddd719",
|
||||
"reference": "0a3abfaad2e4a35280d4e401cd510147d6ddd719",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4393,7 +4393,7 @@
|
||||
"issues": "https://github.com/utopia-php/migration/issues",
|
||||
"source": "https://github.com/utopia-php/migration/tree/feat-documents-db"
|
||||
},
|
||||
"time": "2025-10-14T15:02:00+00:00"
|
||||
"time": "2025-10-14T15:30:04+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/mongo",
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# Change Log
|
||||
|
||||
## 0.21.2
|
||||
|
||||
* Fix: handle Object[] during array deserialization
|
||||
|
||||
## 0.21.1
|
||||
|
||||
* Add transaction support for Databases and TablesDB
|
||||
|
||||
@@ -164,6 +164,7 @@ class Exception extends \Exception
|
||||
|
||||
/** Functions */
|
||||
public const string FUNCTION_NOT_FOUND = 'function_not_found';
|
||||
public const string FUNCTION_ALREADY_EXISTS = 'function_already_exists';
|
||||
public const string FUNCTION_RUNTIME_UNSUPPORTED = 'function_runtime_unsupported';
|
||||
public const string FUNCTION_ENTRYPOINT_MISSING = 'function_entrypoint_missing';
|
||||
public const string FUNCTION_SYNCHRONOUS_TIMEOUT = 'function_synchronous_timeout';
|
||||
|
||||
+25
@@ -205,6 +205,31 @@ abstract class Action extends AppwriteAction
|
||||
return $this->isCollectionsAPI() ? 'collection' : 'table';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the correct attribute/column key for increment/decrement operations.
|
||||
*/
|
||||
protected function getAttributeKey(): string
|
||||
{
|
||||
return $this->isCollectionsAPI() ? 'attribute' : 'column';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the key used in ID parameters (e.g., 'collectionId' or 'tableId').
|
||||
*/
|
||||
protected function getGroupId(): string
|
||||
{
|
||||
return $this->getCollectionsEventsContext() . 'Id';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the resource ID key for the current action.
|
||||
*/
|
||||
protected function getResourceId(): string
|
||||
{
|
||||
$resource = $this->isCollectionsAPI() ? 'document' : 'row';
|
||||
return $resource . 'Id';
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove configured removable attributes from a document.
|
||||
* Used for relationship path handling to remove API-specific attributes.
|
||||
|
||||
+3
-2
@@ -137,7 +137,7 @@ class Decrement extends Action
|
||||
'documentId' => $documentId,
|
||||
'action' => 'decrement',
|
||||
'data' => [
|
||||
'attribute' => $attribute,
|
||||
$this->getAttributeKey() => $attribute,
|
||||
'value' => $value,
|
||||
'min' => $min,
|
||||
],
|
||||
@@ -154,9 +154,10 @@ class Decrement extends Action
|
||||
});
|
||||
|
||||
// Return successful response without actually decrementing
|
||||
$groupId = $this->getGroupId();
|
||||
$mockDocument = new Document([
|
||||
'$id' => $documentId,
|
||||
'$collectionId' => $collectionId,
|
||||
'$' . $groupId => $collectionId,
|
||||
'$databaseId' => $databaseId,
|
||||
$attribute => $value,
|
||||
]);
|
||||
|
||||
+3
-2
@@ -137,7 +137,7 @@ class Increment extends Action
|
||||
'documentId' => $documentId,
|
||||
'action' => 'increment',
|
||||
'data' => [
|
||||
'attribute' => $attribute,
|
||||
$this->getAttributeKey() => $attribute,
|
||||
'value' => $value,
|
||||
'max' => $max,
|
||||
],
|
||||
@@ -154,9 +154,10 @@ class Increment extends Action
|
||||
});
|
||||
|
||||
// Return successful response without actually incrementing
|
||||
$groupId = $this->getGroupId();
|
||||
$mockDocument = new Document([
|
||||
'$id' => $documentId,
|
||||
'$collectionId' => $collectionId,
|
||||
'$' . $groupId => $collectionId,
|
||||
'$databaseId' => $databaseId,
|
||||
$attribute => $value,
|
||||
]);
|
||||
|
||||
+2
-1
@@ -422,9 +422,10 @@ class Create extends Action
|
||||
'total' => \count($documents),
|
||||
]), $this->getBulkResponseModel());
|
||||
} else {
|
||||
$groupId = $this->getGroupId();
|
||||
$mockDocument = new Document([
|
||||
'$id' => $documents[0]['$id'] ?? $documentId,
|
||||
'$collectionId' => $collectionId,
|
||||
'$' . $groupId => $collectionId,
|
||||
'$databaseId' => $databaseId,
|
||||
...$documents[0]
|
||||
]);
|
||||
|
||||
+2
-1
@@ -296,9 +296,10 @@ class Update extends Action
|
||||
});
|
||||
|
||||
// Return successful response without actually updating document
|
||||
$groupId = $this->getGroupId();
|
||||
$mockDocument = new Document([
|
||||
'$id' => $documentId,
|
||||
'$collectionId' => $collectionId,
|
||||
'$' . $groupId => $collectionId,
|
||||
'$databaseId' => $databaseId,
|
||||
...$document->getArrayCopy(),
|
||||
...$data
|
||||
|
||||
+2
-1
@@ -307,9 +307,10 @@ class Upsert extends Action
|
||||
});
|
||||
|
||||
// Return successful response without actually upserting document
|
||||
$groupId = $this->getGroupId();
|
||||
$mockDocument = new Document([
|
||||
'$id' => $documentId,
|
||||
'$collectionId' => $collectionId,
|
||||
'$' . $groupId => $collectionId,
|
||||
'$databaseId' => $databaseId,
|
||||
...$data
|
||||
]);
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ class Create extends Action
|
||||
$this
|
||||
->setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
|
||||
->setHttpPath('/v1/databases/transactions/:transactionId/operations')
|
||||
->desc('Create operations scoped to a transaction')
|
||||
->desc('Create operations')
|
||||
->groups(['api', 'database', 'transactions'])
|
||||
->label('scope', 'documents.write')
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
|
||||
@@ -167,8 +167,10 @@ class Update extends Action
|
||||
}
|
||||
}
|
||||
|
||||
$totalOperations++;
|
||||
$databaseOperations[$databaseInternalId] = ($databaseOperations[$databaseInternalId] ?? 0) + 1;
|
||||
if (!\in_array($action, ['bulkCreate', 'bulkUpdate', 'bulkUpsert', 'bulkDelete'])) {
|
||||
$totalOperations++;
|
||||
$databaseOperations[$databaseInternalId] = ($databaseOperations[$databaseInternalId] ?? 0) + 1;
|
||||
}
|
||||
|
||||
if ($data instanceof Document) {
|
||||
$data = $data->getArrayCopy();
|
||||
@@ -194,16 +196,24 @@ class Update extends Action
|
||||
$this->handleDecrementOperation($dbForProject, $collectionId, $documentId, $data, $createdAt, $state);
|
||||
break;
|
||||
case 'bulkCreate':
|
||||
$this->handleBulkCreateOperation($dbForProject, $collectionId, $data, $createdAt, $state);
|
||||
$count = $this->handleBulkCreateOperation($dbForProject, $collectionId, $data, $createdAt, $state);
|
||||
$totalOperations += $count;
|
||||
$databaseOperations[$databaseInternalId] = ($databaseOperations[$databaseInternalId] ?? 0) + $count;
|
||||
break;
|
||||
case 'bulkUpdate':
|
||||
$this->handleBulkUpdateOperation($dbForProject, $transactionState, $collectionId, $data, $createdAt, $state);
|
||||
$count = $this->handleBulkUpdateOperation($dbForProject, $transactionState, $collectionId, $data, $createdAt, $state);
|
||||
$totalOperations += $count;
|
||||
$databaseOperations[$databaseInternalId] = ($databaseOperations[$databaseInternalId] ?? 0) + $count;
|
||||
break;
|
||||
case 'bulkUpsert':
|
||||
$this->handleBulkUpsertOperation($dbForProject, $transactionState, $collectionId, $data, $createdAt, $state);
|
||||
$count = $this->handleBulkUpsertOperation($dbForProject, $transactionState, $collectionId, $data, $createdAt, $state);
|
||||
$totalOperations += $count;
|
||||
$databaseOperations[$databaseInternalId] = ($databaseOperations[$databaseInternalId] ?? 0) + $count;
|
||||
break;
|
||||
case 'bulkDelete':
|
||||
$this->handleBulkDeleteOperation($dbForProject, $transactionState, $collectionId, $data, $createdAt, $state);
|
||||
$count = $this->handleBulkDeleteOperation($dbForProject, $transactionState, $collectionId, $data, $createdAt, $state);
|
||||
$totalOperations += $count;
|
||||
$databaseOperations[$databaseInternalId] = ($databaseOperations[$databaseInternalId] ?? 0) + $count;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -547,6 +557,28 @@ class Update extends Action
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attribute/column name from data, with fallback for cross-API compatibility
|
||||
*
|
||||
* @param array $data The operation data
|
||||
* @return string The attribute/column name
|
||||
*/
|
||||
private function getAttributeNameFromData(array $data): string
|
||||
{
|
||||
$expectedKey = $this->getAttributeKey();
|
||||
if (isset($data[$expectedKey])) {
|
||||
return $data[$expectedKey];
|
||||
}
|
||||
|
||||
// Try the opposite key for cross-API compatibility
|
||||
$fallbackKey = $expectedKey === 'attribute' ? 'column' : 'attribute';
|
||||
if (isset($data[$fallbackKey])) {
|
||||
return $data[$fallbackKey];
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle increment operation
|
||||
*
|
||||
@@ -569,23 +601,24 @@ class Update extends Action
|
||||
array &$state
|
||||
): void {
|
||||
$dependent = isset($state[$collectionId][$documentId]);
|
||||
$attribute = $this->getAttributeNameFromData($data);
|
||||
|
||||
if ($dependent) {
|
||||
$state[$collectionId][$documentId] = $dbForProject->increaseDocumentAttribute(
|
||||
collection: $collectionId,
|
||||
id: $documentId,
|
||||
attribute: $data[$this->getAttributeKey()],
|
||||
attribute: $attribute,
|
||||
value: $data['value'] ?? 1,
|
||||
max: $data['max'] ?? null
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
$dbForProject->withRequestTimestamp($createdAt, function () use ($dbForProject, $collectionId, $documentId, $data, &$state) {
|
||||
$dbForProject->withRequestTimestamp($createdAt, function () use ($dbForProject, $collectionId, $documentId, $data, &$state, $attribute) {
|
||||
$state[$collectionId][$documentId] = $dbForProject->increaseDocumentAttribute(
|
||||
collection: $collectionId,
|
||||
id: $documentId,
|
||||
attribute: $data[$this->getAttributeKey()],
|
||||
attribute: $attribute,
|
||||
value: $data['value'] ?? 1,
|
||||
max: $data['max'] ?? null
|
||||
);
|
||||
@@ -614,23 +647,24 @@ class Update extends Action
|
||||
array &$state
|
||||
): void {
|
||||
$dependent = isset($state[$collectionId][$documentId]);
|
||||
$attribute = $this->getAttributeNameFromData($data);
|
||||
|
||||
if ($dependent) {
|
||||
$state[$collectionId][$documentId] = $dbForProject->decreaseDocumentAttribute(
|
||||
collection: $collectionId,
|
||||
id: $documentId,
|
||||
attribute: $data[$this->getAttributeKey()],
|
||||
attribute: $attribute,
|
||||
value: $data['value'] ?? 1,
|
||||
min: $data['min'] ?? null
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
$dbForProject->withRequestTimestamp($createdAt, function () use ($dbForProject, $collectionId, $documentId, $data, &$state) {
|
||||
$dbForProject->withRequestTimestamp($createdAt, function () use ($dbForProject, $collectionId, $documentId, $data, &$state, $attribute) {
|
||||
$state[$collectionId][$documentId] = $dbForProject->decreaseDocumentAttribute(
|
||||
collection: $collectionId,
|
||||
id: $documentId,
|
||||
attribute: $data[$this->getAttributeKey()],
|
||||
attribute: $attribute,
|
||||
value: $data['value'] ?? 1,
|
||||
min: $data['min'] ?? null
|
||||
);
|
||||
@@ -645,7 +679,7 @@ class Update extends Action
|
||||
* @param array $data
|
||||
* @param \DateTime $createdAt
|
||||
* @param array &$state
|
||||
* @return void
|
||||
* @return int Number of documents created
|
||||
* @throws \Utopia\Database\Exception
|
||||
*/
|
||||
private function handleBulkCreateOperation(
|
||||
@@ -654,13 +688,14 @@ class Update extends Action
|
||||
array $data,
|
||||
\DateTime $createdAt,
|
||||
array &$state
|
||||
): void {
|
||||
$dbForProject->withRequestTimestamp($createdAt, function () use ($dbForProject, $collectionId, $data, &$state) {
|
||||
): int {
|
||||
$count = 0;
|
||||
$dbForProject->withRequestTimestamp($createdAt, function () use ($dbForProject, $collectionId, $data, &$state, &$count) {
|
||||
$documents = \array_map(function ($doc) {
|
||||
return $doc instanceof Document ? $doc : new Document($doc);
|
||||
}, $data);
|
||||
|
||||
$dbForProject->createDocuments(
|
||||
$count = $dbForProject->createDocuments(
|
||||
$collectionId,
|
||||
$documents,
|
||||
onNext: function (Document $document) use (&$state, $collectionId) {
|
||||
@@ -668,6 +703,7 @@ class Update extends Action
|
||||
}
|
||||
);
|
||||
});
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -679,7 +715,7 @@ class Update extends Action
|
||||
* @param array $data
|
||||
* @param \DateTime $createdAt
|
||||
* @param array &$state
|
||||
* @return void
|
||||
* @return int Number of documents updated
|
||||
* @throws \Utopia\Database\Exception
|
||||
* @throws \Utopia\Database\Exception\Query
|
||||
* @throws ConflictException
|
||||
@@ -691,7 +727,7 @@ class Update extends Action
|
||||
array $data,
|
||||
\DateTime $createdAt,
|
||||
array &$state
|
||||
): void {
|
||||
): int {
|
||||
$queries = Query::parseQueries($data['queries'] ?? []);
|
||||
$updateData = new Document($data['data']);
|
||||
|
||||
@@ -701,7 +737,7 @@ class Update extends Action
|
||||
|
||||
// Clone the document before passing to updateDocuments to prevent mutation
|
||||
// The database layer mutates the input document, which would corrupt transaction state
|
||||
$dbForProject->updateDocuments(
|
||||
$count = $dbForProject->updateDocuments(
|
||||
$collectionId,
|
||||
clone $updateData,
|
||||
$queries,
|
||||
@@ -739,6 +775,8 @@ class Update extends Action
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -750,7 +788,7 @@ class Update extends Action
|
||||
* @param array $data
|
||||
* @param \DateTime $createdAt
|
||||
* @param array &$state
|
||||
* @return void
|
||||
* @return int Number of documents upserted
|
||||
* @throws ConflictException
|
||||
* @throws \Utopia\Database\Exception
|
||||
*/
|
||||
@@ -761,14 +799,14 @@ class Update extends Action
|
||||
array $data,
|
||||
\DateTime $createdAt,
|
||||
array &$state
|
||||
): void {
|
||||
): int {
|
||||
$documents = \array_map(function ($doc) {
|
||||
return $doc instanceof Document ? $doc : new Document($doc);
|
||||
}, $data);
|
||||
|
||||
$mergedDocuments = $transactionState->applyBulkUpsertToState($collectionId, $documents, $state);
|
||||
|
||||
$dbForProject->upsertDocuments(
|
||||
$count = $dbForProject->upsertDocuments(
|
||||
$collectionId,
|
||||
$mergedDocuments,
|
||||
onNext: function (Document $upserted, ?Document $old) use (&$state, $collectionId, $createdAt) {
|
||||
@@ -786,6 +824,8 @@ class Update extends Action
|
||||
$state[$collectionId][$upserted->getId()] = $upserted;
|
||||
}
|
||||
);
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -797,7 +837,7 @@ class Update extends Action
|
||||
* @param array $data
|
||||
* @param \DateTime $createdAt
|
||||
* @param array &$state
|
||||
* @return void
|
||||
* @return int Number of documents deleted
|
||||
* @throws \Utopia\Database\Exception\Query
|
||||
* @throws ConflictException
|
||||
* @throws \Utopia\Database\Exception
|
||||
@@ -809,10 +849,10 @@ class Update extends Action
|
||||
array $data,
|
||||
\DateTime $createdAt,
|
||||
array &$state
|
||||
): void {
|
||||
): int {
|
||||
$queries = Query::parseQueries($data['queries'] ?? []);
|
||||
|
||||
$dbForProject->deleteDocuments(
|
||||
$count = $dbForProject->deleteDocuments(
|
||||
$collectionId,
|
||||
$queries,
|
||||
onNext: function (Document $deleted, Document $old) use (&$state, $collectionId, $createdAt) {
|
||||
@@ -832,5 +872,7 @@ class Update extends Action
|
||||
);
|
||||
|
||||
$transactionState->applyBulkDeleteToState($collectionId, $queries, $state);
|
||||
|
||||
return $count;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ class Create extends TransactionsCreate
|
||||
->setHttpPath('/v1/tablesdb/transactions')
|
||||
->desc('Create transaction')
|
||||
->groups(['api', 'database', 'transactions'])
|
||||
->label('scope', 'rows.write')
|
||||
->label('scope', ['documents.write', 'rows.write'])
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('sdk', new Method(
|
||||
namespace: 'tablesDB',
|
||||
|
||||
@@ -30,7 +30,7 @@ class Delete extends TransactionsDelete
|
||||
->setHttpPath('/v1/tablesdb/transactions/:transactionId')
|
||||
->desc('Delete transaction')
|
||||
->groups(['api', 'database', 'transactions'])
|
||||
->label('scope', 'rows.write')
|
||||
->label('scope', ['documents.write', 'rows.write'])
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('sdk', new Method(
|
||||
namespace: 'tablesDB',
|
||||
|
||||
@@ -30,7 +30,7 @@ class Get extends TransactionsGet
|
||||
->setHttpPath('/v1/tablesdb/transactions/:transactionId')
|
||||
->desc('Get transaction')
|
||||
->groups(['api', 'database', 'transactions'])
|
||||
->label('scope', 'rows.read')
|
||||
->label('scope', ['documents.read', 'rows.read'])
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('sdk', new Method(
|
||||
namespace: 'tablesDB',
|
||||
|
||||
+2
-2
@@ -30,9 +30,9 @@ class Create extends OperationsCreate
|
||||
$this
|
||||
->setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
|
||||
->setHttpPath('/v1/tablesdb/transactions/:transactionId/operations')
|
||||
->desc('Create operations scoped to a transaction')
|
||||
->desc('Create operations')
|
||||
->groups(['api', 'database', 'transactions'])
|
||||
->label('scope', 'rows.write')
|
||||
->label('scope', ['documents.write', 'rows.write'])
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('sdk', new Method(
|
||||
namespace: 'tablesDB',
|
||||
|
||||
@@ -31,7 +31,7 @@ class Update extends TransactionsUpdate
|
||||
->setHttpPath('/v1/tablesdb/transactions/:transactionId')
|
||||
->desc('Update transaction')
|
||||
->groups(['api', 'database', 'transactions'])
|
||||
->label('scope', 'rows.write')
|
||||
->label('scope', ['documents.write', 'rows.write'])
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('sdk', new Method(
|
||||
namespace: 'tablesDB',
|
||||
|
||||
@@ -30,7 +30,7 @@ class XList extends TransactionsList
|
||||
->setHttpPath('/v1/tablesdb/transactions')
|
||||
->desc('List transactions')
|
||||
->groups(['api', 'database', 'transactions'])
|
||||
->label('scope', 'rows.read')
|
||||
->label('scope', ['documents.read', 'rows.read'])
|
||||
->label('resourceType', RESOURCE_TYPE_DATABASES)
|
||||
->label('sdk', new Method(
|
||||
namespace: 'tablesDB',
|
||||
|
||||
@@ -23,6 +23,7 @@ use Utopia\Config\Config;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Duplicate as DuplicateException;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
@@ -201,36 +202,40 @@ class Create extends Base
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'When connecting to VCS (Version Control System), you need to provide "installationId" and "providerBranch".');
|
||||
}
|
||||
|
||||
$function = $dbForProject->createDocument('functions', new Document([
|
||||
'$id' => $functionId,
|
||||
'execute' => $execute,
|
||||
'enabled' => $enabled,
|
||||
'live' => true,
|
||||
'logging' => $logging,
|
||||
'name' => $name,
|
||||
'runtime' => $runtime,
|
||||
'deploymentInternalId' => '',
|
||||
'deploymentId' => '',
|
||||
'events' => $events,
|
||||
'schedule' => $schedule,
|
||||
'scheduleInternalId' => '',
|
||||
'scheduleId' => '',
|
||||
'timeout' => $timeout,
|
||||
'entrypoint' => $entrypoint,
|
||||
'commands' => $commands,
|
||||
'scopes' => $scopes,
|
||||
'search' => implode(' ', [$functionId, $name, $runtime]),
|
||||
'version' => 'v5',
|
||||
'installationId' => $installation->getId(),
|
||||
'installationInternalId' => $installation->getSequence(),
|
||||
'providerRepositoryId' => $providerRepositoryId,
|
||||
'repositoryId' => '',
|
||||
'repositoryInternalId' => '',
|
||||
'providerBranch' => $providerBranch,
|
||||
'providerRootDirectory' => $providerRootDirectory,
|
||||
'providerSilentMode' => $providerSilentMode,
|
||||
'specification' => $specification
|
||||
]));
|
||||
try {
|
||||
$function = $dbForProject->createDocument('functions', new Document([
|
||||
'$id' => $functionId,
|
||||
'execute' => $execute,
|
||||
'enabled' => $enabled,
|
||||
'live' => true,
|
||||
'logging' => $logging,
|
||||
'name' => $name,
|
||||
'runtime' => $runtime,
|
||||
'deploymentInternalId' => '',
|
||||
'deploymentId' => '',
|
||||
'events' => $events,
|
||||
'schedule' => $schedule,
|
||||
'scheduleInternalId' => '',
|
||||
'scheduleId' => '',
|
||||
'timeout' => $timeout,
|
||||
'entrypoint' => $entrypoint,
|
||||
'commands' => $commands,
|
||||
'scopes' => $scopes,
|
||||
'search' => implode(' ', [$functionId, $name, $runtime]),
|
||||
'version' => 'v5',
|
||||
'installationId' => $installation->getId(),
|
||||
'installationInternalId' => $installation->getSequence(),
|
||||
'providerRepositoryId' => $providerRepositoryId,
|
||||
'repositoryId' => '',
|
||||
'repositoryInternalId' => '',
|
||||
'providerBranch' => $providerBranch,
|
||||
'providerRootDirectory' => $providerRootDirectory,
|
||||
'providerSilentMode' => $providerSilentMode,
|
||||
'specification' => $specification
|
||||
]));
|
||||
} catch (DuplicateException) {
|
||||
throw new Exception(Exception::FUNCTION_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
$schedule = Authorization::skip(
|
||||
fn () => $dbForPlatform->createDocument('schedules', new Document([
|
||||
|
||||
@@ -280,6 +280,25 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if the latest commit on the target branch already has a release
|
||||
$latestCommitCommand = 'gh api repos/' . $repoName . '/commits/' . $releaseTarget . ' --jq ".sha" 2>/dev/null';
|
||||
$latestCommitSha = trim(\shell_exec($latestCommitCommand) ?? '');
|
||||
|
||||
if (!empty($latestCommitSha)) {
|
||||
$latestReleaseTagCommand = 'gh api repos/' . $repoName . '/releases --jq ".[0] | .tag_name" 2>/dev/null';
|
||||
$latestReleaseTag = trim(\shell_exec($latestReleaseTagCommand) ?? '');
|
||||
|
||||
if (!empty($latestReleaseTag)) {
|
||||
$tagCommitCommand = 'gh api repos/' . $repoName . '/git/ref/tags/' . $latestReleaseTag . ' --jq ".object.sha" 2>/dev/null';
|
||||
$tagCommitSha = trim(\shell_exec($tagCommitCommand) ?? '');
|
||||
|
||||
if (!empty($tagCommitSha) && $latestCommitSha === $tagCommitSha) {
|
||||
Console::warning("Latest commit on {$releaseTarget} already has a release ({$latestReleaseTag}) for {$language['name']} SDK, skipping to avoid empty release...");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$previousVersion = '';
|
||||
$tagListCommand = 'gh release list --repo "' . $repoName . '" --limit 1 --json tagName --jq ".[0].tagName" 2>&1';
|
||||
$previousVersion = trim(\shell_exec($tagListCommand) ?? '');
|
||||
|
||||
@@ -49,6 +49,7 @@ abstract class ScheduleBase extends Action
|
||||
->inject('publisherMigrations')
|
||||
->inject('publisherFunctions')
|
||||
->inject('publisherMessaging')
|
||||
->inject('isResourceBlocked')
|
||||
->inject('dbForPlatform')
|
||||
->inject('getProjectDB')
|
||||
->inject('telemetry')
|
||||
@@ -71,7 +72,7 @@ abstract class ScheduleBase extends Action
|
||||
* 2. Create timer that sync all changes from 'schedules' collection to local copy. Only reading changes thanks to 'resourceUpdatedAt' attribute
|
||||
* 3. Create timer that prepares coroutines for soon-to-execute schedules. When it's ready, coroutine sleeps until exact time before sending request to worker.
|
||||
*/
|
||||
public function action(BrokerPool $publisher, BrokerPool $publisherMigrations, BrokerPool $publisherFunctions, BrokerPool $publisherMessaging, Database $dbForPlatform, callable $getProjectDB, Telemetry $telemetry): void
|
||||
public function action(BrokerPool $publisher, BrokerPool $publisherMigrations, BrokerPool $publisherFunctions, BrokerPool $publisherMessaging, callable $isResourceBlocked, Database $dbForPlatform, callable $getProjectDB, Telemetry $telemetry): void
|
||||
{
|
||||
Console::title(\ucfirst(static::getSupportedResource()) . ' scheduler V1');
|
||||
Console::success(APP_NAME . ' ' . \ucfirst(static::getSupportedResource()) . ' scheduler v1 has started');
|
||||
@@ -88,16 +89,16 @@ abstract class ScheduleBase extends Action
|
||||
|
||||
// start with "0" to load all active documents.
|
||||
$lastSyncUpdate = "0";
|
||||
$this->collectSchedules($dbForPlatform, $getProjectDB, $lastSyncUpdate);
|
||||
$this->collectSchedules($dbForPlatform, $getProjectDB, $lastSyncUpdate, $isResourceBlocked);
|
||||
|
||||
Console::success("Starting timers at " . DateTime::now());
|
||||
/**
|
||||
* The timer synchronize $schedules copy with database collection.
|
||||
*/
|
||||
Timer::tick(static::UPDATE_TIMER * 1000, function () use ($dbForPlatform, $getProjectDB, &$lastSyncUpdate) {
|
||||
Timer::tick(static::UPDATE_TIMER * 1000, function () use ($dbForPlatform, $getProjectDB, &$lastSyncUpdate, $isResourceBlocked) {
|
||||
$time = DateTime::now();
|
||||
Console::log("Sync tick: Running at $time");
|
||||
$this->collectSchedules($dbForPlatform, $getProjectDB, $lastSyncUpdate);
|
||||
$this->collectSchedules($dbForPlatform, $getProjectDB, $lastSyncUpdate, $isResourceBlocked);
|
||||
});
|
||||
|
||||
while (true) {
|
||||
@@ -112,7 +113,7 @@ abstract class ScheduleBase extends Action
|
||||
}
|
||||
}
|
||||
|
||||
private function collectSchedules(Database $dbForPlatform, callable $getProjectDB, string &$lastSyncUpdate): void
|
||||
private function collectSchedules(Database $dbForPlatform, callable $getProjectDB, string &$lastSyncUpdate, callable $isResourceBlocked): void
|
||||
{
|
||||
// If we haven't synced yet, load all active schedules
|
||||
$initialLoad = $lastSyncUpdate === "0";
|
||||
@@ -178,34 +179,40 @@ abstract class ScheduleBase extends Action
|
||||
$paginationQueries[] = Query::greaterThanEqual('resourceUpdatedAt', $lastSyncUpdate);
|
||||
}
|
||||
|
||||
$results = $dbForPlatform->find('schedules', $paginationQueries);
|
||||
$collectionId = static::getCollectionId();
|
||||
$schedules = $dbForPlatform->find('schedules', $paginationQueries);
|
||||
$sum = count($schedules);
|
||||
$total += $sum;
|
||||
|
||||
$sum = count($results);
|
||||
$total = $total + $sum;
|
||||
foreach ($schedules as $schedule) {
|
||||
$existing = $this->schedules[$schedule->getSequence()] ?? null;
|
||||
$updated = strtotime($existing['resourceUpdatedAt'] ?? '0') !== strtotime($schedule['resourceUpdatedAt'] ?? '0');
|
||||
|
||||
foreach ($results as $document) {
|
||||
$localDocument = $this->schedules[$document->getSequence()] ?? null;
|
||||
|
||||
if ($localDocument !== null) {
|
||||
if (!$document['active']) {
|
||||
Console::info("Removing: {$document['resourceType']}::{$document['resourceId']}");
|
||||
unset($this->schedules[$document->getSequence()]);
|
||||
} elseif (strtotime($localDocument['resourceUpdatedAt']) !== strtotime($document['resourceUpdatedAt'])) {
|
||||
Console::info("Updating: {$document['resourceType']}::{$document['resourceId']}");
|
||||
$this->schedules[$document->getSequence()] = $getSchedule($document);
|
||||
}
|
||||
} else {
|
||||
if ($existing === null || $updated) {
|
||||
try {
|
||||
$this->schedules[$document->getSequence()] = $getSchedule($document);
|
||||
$candidate = $getSchedule($schedule);
|
||||
} catch (\Throwable $th) {
|
||||
$collectionId = static::getCollectionId();
|
||||
Console::error("Failed to load schedule for project {$document['projectId']} {$collectionId} {$document['resourceId']}");
|
||||
Console::error("Failed to load schedule for project {$schedule['projectId']} {$collectionId} {$schedule['resourceId']}");
|
||||
Console::error($th->getMessage());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$candidate['active']) {
|
||||
unset($this->schedules[$schedule->getSequence()]);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($isResourceBlocked($candidate['project'], $collectionId, $candidate['resourceId'])) {
|
||||
unset($this->schedules[$schedule->getSequence()]);
|
||||
continue;
|
||||
}
|
||||
|
||||
Console::info("Updating: {$schedule['resourceType']}::{$schedule['resourceId']}");
|
||||
$this->schedules[$schedule->getSequence()] = $candidate;
|
||||
}
|
||||
}
|
||||
|
||||
$latestDocument = \end($results);
|
||||
$latestDocument = \end($schedules);
|
||||
}
|
||||
|
||||
$lastSyncUpdate = $time;
|
||||
|
||||
@@ -593,6 +593,14 @@ class Messaging extends Action
|
||||
$content = $data['content'];
|
||||
$html = $data['html'] ?? false;
|
||||
|
||||
// For SMTP, move all recipients to BCC and use default recipient in TO field
|
||||
if ($provider->getAttribute('provider') === 'smtp') {
|
||||
foreach ($to as $recipient) {
|
||||
$bcc[] = ['email' => $recipient];
|
||||
}
|
||||
$to = [];
|
||||
}
|
||||
|
||||
return new Email(
|
||||
$to,
|
||||
$subject,
|
||||
|
||||
@@ -18,24 +18,24 @@ class StatsUsage extends Action
|
||||
/**
|
||||
* In memory per project metrics calculation
|
||||
*/
|
||||
private array $stats = [];
|
||||
private int $lastTriggeredTime = 0;
|
||||
private int $keys = 0;
|
||||
private const INFINITY_PERIOD = '_inf_';
|
||||
private const BATCH_SIZE_DEVELOPMENT = 1;
|
||||
private const BATCH_SIZE_PRODUCTION = 10_000;
|
||||
protected array $stats = [];
|
||||
protected int $lastTriggeredTime = 0;
|
||||
protected int $keys = 0;
|
||||
protected const INFINITY_PERIOD = '_inf_';
|
||||
protected const BATCH_SIZE_DEVELOPMENT = 1;
|
||||
protected const BATCH_SIZE_PRODUCTION = 10_000;
|
||||
|
||||
/**
|
||||
* Stats for batch write separated per project
|
||||
* @var array
|
||||
*/
|
||||
private array $projects = [];
|
||||
protected array $projects = [];
|
||||
|
||||
/**
|
||||
* Array of stat documents to batch write to logsDB
|
||||
* @var array
|
||||
*/
|
||||
private array $statDocuments = [];
|
||||
protected array $statDocuments = [];
|
||||
|
||||
protected Registry $register;
|
||||
|
||||
@@ -101,7 +101,7 @@ class StatsUsage extends Action
|
||||
return 'stats-usage';
|
||||
}
|
||||
|
||||
private function getBatchSize(): int
|
||||
protected function getBatchSize(): int
|
||||
{
|
||||
return System::getEnv('_APP_ENV', 'development') === 'development'
|
||||
? self::BATCH_SIZE_DEVELOPMENT
|
||||
@@ -195,7 +195,7 @@ class StatsUsage extends Action
|
||||
* @param callable(): Database $getProjectDB
|
||||
* @return void
|
||||
*/
|
||||
private function reduce(Document $project, Document $document, array &$metrics, callable $getProjectDB): void
|
||||
protected function reduce(Document $project, Document $document, array &$metrics, callable $getProjectDB): void
|
||||
{
|
||||
$dbForProject = $getProjectDB($project);
|
||||
|
||||
|
||||
@@ -82,9 +82,8 @@ class Document extends Any
|
||||
{
|
||||
$document->removeAttribute('$collection');
|
||||
$document->removeAttribute('$tenant');
|
||||
$sequence = $document->getAttribute('$sequence', 0);
|
||||
// to support both -> uuids(mongodb) and numeric ids(mariadb/mysql)
|
||||
if (is_numeric($sequence)) {
|
||||
|
||||
if (!$document->isEmpty() && is_numeric($document->getAttribute('$sequence', 0))) {
|
||||
$document->setAttribute('$sequence', (int)$document->getAttribute('$sequence', 0));
|
||||
}
|
||||
|
||||
|
||||
@@ -3732,6 +3732,149 @@ trait TransactionsBase
|
||||
$this->assertEquals(0, $doc['body']['score']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test individual increment/decrement endpoints with transactions for Legacy Collections API
|
||||
* This test ensures that:
|
||||
* 1. Transaction logs store the correct attribute key ('attribute' for Collections API)
|
||||
* 2. Mock responses return the correct ID keys ('$collectionId' not '$tableId')
|
||||
*/
|
||||
public function testIncrementDecrementEndpointsWithTransaction(): void
|
||||
{
|
||||
// Create database and collection
|
||||
$database = $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' => 'IncrDecrEndpointTestDB'
|
||||
]);
|
||||
|
||||
$databaseId = $database['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' => 'AccountsCollection',
|
||||
'permissions' => [
|
||||
Permission::create(Role::any()),
|
||||
Permission::read(Role::any()),
|
||||
Permission::update(Role::any()),
|
||||
],
|
||||
]);
|
||||
|
||||
$collectionId = $collection['body']['$id'];
|
||||
|
||||
// Add balance attribute
|
||||
$this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/attributes/integer", array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]), [
|
||||
'key' => 'balance',
|
||||
'required' => false,
|
||||
'default' => 0,
|
||||
]);
|
||||
|
||||
sleep(2);
|
||||
|
||||
// Create initial documents
|
||||
$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' => 'joe',
|
||||
'data' => ['balance' => 100]
|
||||
]);
|
||||
|
||||
$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' => 'jane',
|
||||
'data' => ['balance' => 50]
|
||||
]);
|
||||
|
||||
// Create transaction
|
||||
$transaction = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
|
||||
$this->assertEquals(201, $transaction['headers']['status-code']);
|
||||
$transactionId = $transaction['body']['$id'];
|
||||
|
||||
// Test: Decrement using individual endpoint - should store 'attribute' not 'column' in transaction log
|
||||
$decrementResponse = $this->client->call(
|
||||
Client::METHOD_PATCH,
|
||||
"/databases/{$databaseId}/collections/{$collectionId}/documents/joe/balance/decrement",
|
||||
array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()),
|
||||
[
|
||||
'transactionId' => $transactionId,
|
||||
'value' => 50,
|
||||
'min' => 0,
|
||||
]
|
||||
);
|
||||
|
||||
// Test: Response should return '$collectionId' not '$tableId' for Collections API
|
||||
$this->assertEquals(200, $decrementResponse['headers']['status-code']);
|
||||
$this->assertArrayHasKey('$collectionId', $decrementResponse['body'], 'Response should contain $collectionId for Collections API');
|
||||
$this->assertArrayNotHasKey('$tableId', $decrementResponse['body'], 'Response should not contain $tableId for Collections API');
|
||||
$this->assertEquals($collectionId, $decrementResponse['body']['$collectionId']);
|
||||
|
||||
// Test increment endpoint
|
||||
$incrementResponse = $this->client->call(
|
||||
Client::METHOD_PATCH,
|
||||
"/databases/{$databaseId}/collections/{$collectionId}/documents/jane/balance/increment",
|
||||
array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()),
|
||||
[
|
||||
'transactionId' => $transactionId,
|
||||
'value' => 50,
|
||||
]
|
||||
);
|
||||
|
||||
$this->assertEquals(200, $incrementResponse['headers']['status-code']);
|
||||
$this->assertArrayHasKey('$collectionId', $incrementResponse['body'], 'Response should contain $collectionId for Collections API');
|
||||
$this->assertArrayNotHasKey('$tableId', $incrementResponse['body'], 'Response should not contain $tableId for Collections API');
|
||||
$this->assertEquals($collectionId, $incrementResponse['body']['$collectionId']);
|
||||
|
||||
// Commit transaction - this will fail if transaction log has 'column' instead of 'attribute'
|
||||
$commitResponse = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'commit' => true
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $commitResponse['headers']['status-code'], 'Transaction commit should succeed');
|
||||
|
||||
// Verify final values
|
||||
$joe = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/joe", array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
|
||||
$jane = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents/jane", array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
|
||||
$this->assertEquals(200, $joe['headers']['status-code']);
|
||||
$this->assertEquals(50, $joe['body']['balance'], 'Joe should have 100 - 50 = 50');
|
||||
|
||||
$this->assertEquals(200, $jane['headers']['status-code']);
|
||||
$this->assertEquals(100, $jane['body']['balance'], 'Jane should have 50 + 50 = 100');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test bulk update operations in transaction
|
||||
*/
|
||||
|
||||
@@ -3867,6 +3867,252 @@ trait TransactionsBase
|
||||
$this->assertEquals('updated', $row['body']['status'], 'Status should be updated');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test individual increment/decrement endpoints with transactions
|
||||
* This test ensures that:
|
||||
* 1. Transaction logs store the correct attribute key ('column' for TablesDB)
|
||||
* 2. Mock responses return the correct ID keys ('$tableId' not '$collectionId')
|
||||
*/
|
||||
public function testIncrementDecrementEndpointsWithTransaction(): void
|
||||
{
|
||||
// Create database and table
|
||||
$database = $this->client->call(Client::METHOD_POST, '/tablesdb', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]), [
|
||||
'databaseId' => ID::unique(),
|
||||
'name' => 'IncrDecrEndpointTestDB'
|
||||
]);
|
||||
|
||||
$databaseId = $database['body']['$id'];
|
||||
|
||||
$table = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables", array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]), [
|
||||
'tableId' => ID::unique(),
|
||||
'name' => 'AccountsTable',
|
||||
'permissions' => [
|
||||
Permission::create(Role::any()),
|
||||
Permission::read(Role::any()),
|
||||
Permission::update(Role::any()),
|
||||
],
|
||||
]);
|
||||
|
||||
$tableId = $table['body']['$id'];
|
||||
|
||||
// Add balance column
|
||||
$this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/integer", array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]), [
|
||||
'key' => 'balance',
|
||||
'required' => false,
|
||||
'default' => 0,
|
||||
]);
|
||||
|
||||
sleep(2);
|
||||
|
||||
// Create initial rows
|
||||
$this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'rowId' => 'joe',
|
||||
'data' => ['balance' => 100]
|
||||
]);
|
||||
|
||||
$this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'rowId' => 'jane',
|
||||
'data' => ['balance' => 50]
|
||||
]);
|
||||
|
||||
// Create transaction
|
||||
$transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
|
||||
$this->assertEquals(201, $transaction['headers']['status-code']);
|
||||
$transactionId = $transaction['body']['$id'];
|
||||
|
||||
// Test Bug 1: Decrement using individual endpoint - should store 'column' not 'attribute' in transaction log
|
||||
$decrementResponse = $this->client->call(
|
||||
Client::METHOD_PATCH,
|
||||
"/tablesdb/{$databaseId}/tables/{$tableId}/rows/joe/balance/decrement",
|
||||
array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()),
|
||||
[
|
||||
'transactionId' => $transactionId,
|
||||
'value' => 50,
|
||||
'min' => 0,
|
||||
]
|
||||
);
|
||||
|
||||
// Test Bug 2: Response should return '$tableId' not '$collectionId'
|
||||
$this->assertEquals(200, $decrementResponse['headers']['status-code']);
|
||||
$this->assertArrayHasKey('$tableId', $decrementResponse['body'], 'Response should contain $tableId for TablesDB API');
|
||||
$this->assertArrayNotHasKey('$collectionId', $decrementResponse['body'], 'Response should not contain $collectionId for TablesDB API');
|
||||
$this->assertEquals($tableId, $decrementResponse['body']['$tableId']);
|
||||
|
||||
// Test increment endpoint
|
||||
$incrementResponse = $this->client->call(
|
||||
Client::METHOD_PATCH,
|
||||
"/tablesdb/{$databaseId}/tables/{$tableId}/rows/jane/balance/increment",
|
||||
array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()),
|
||||
[
|
||||
'transactionId' => $transactionId,
|
||||
'value' => 50,
|
||||
]
|
||||
);
|
||||
|
||||
$this->assertEquals(200, $incrementResponse['headers']['status-code']);
|
||||
$this->assertArrayHasKey('$tableId', $incrementResponse['body'], 'Response should contain $tableId for TablesDB API');
|
||||
$this->assertArrayNotHasKey('$collectionId', $incrementResponse['body'], 'Response should not contain $collectionId for TablesDB API');
|
||||
$this->assertEquals($tableId, $incrementResponse['body']['$tableId']);
|
||||
|
||||
// Commit transaction - this will fail if transaction log has 'attribute' instead of 'column'
|
||||
$commitResponse = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'commit' => true
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $commitResponse['headers']['status-code'], 'Transaction commit should succeed');
|
||||
|
||||
// Verify final values
|
||||
$joe = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/joe", array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
|
||||
$jane = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/jane", array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
|
||||
$this->assertEquals(200, $joe['headers']['status-code']);
|
||||
$this->assertEquals(50, $joe['body']['balance'], 'Joe should have 100 - 50 = 50');
|
||||
|
||||
$this->assertEquals(200, $jane['headers']['status-code']);
|
||||
$this->assertEquals(100, $jane['body']['balance'], 'Jane should have 50 + 50 = 100');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test cross-API compatibility: stage operations via TablesDB, commit via Collections API
|
||||
* This ensures fallback logic works when APIs are mixed
|
||||
*/
|
||||
public function testCrossAPIIncrementDecrement(): void
|
||||
{
|
||||
// Create database and table
|
||||
$database = $this->client->call(Client::METHOD_POST, '/tablesdb', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]), [
|
||||
'databaseId' => ID::unique(),
|
||||
'name' => 'CrossAPITestDB'
|
||||
]);
|
||||
|
||||
$databaseId = $database['body']['$id'];
|
||||
|
||||
$table = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables", array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]), [
|
||||
'tableId' => ID::unique(),
|
||||
'name' => 'CrossAPITable',
|
||||
'permissions' => [
|
||||
Permission::create(Role::any()),
|
||||
Permission::read(Role::any()),
|
||||
Permission::update(Role::any()),
|
||||
],
|
||||
]);
|
||||
|
||||
$tableId = $table['body']['$id'];
|
||||
|
||||
// Add balance column
|
||||
$this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/integer", array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]), [
|
||||
'key' => 'balance',
|
||||
'required' => false,
|
||||
'default' => 0,
|
||||
]);
|
||||
|
||||
sleep(2);
|
||||
|
||||
// Create initial row
|
||||
$this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'rowId' => 'test',
|
||||
'data' => ['balance' => 100]
|
||||
]);
|
||||
|
||||
// Create transaction using TablesDB API
|
||||
$transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
|
||||
$transactionId = $transaction['body']['$id'];
|
||||
|
||||
// Stage operations using TablesDB API (will store 'column' key)
|
||||
$this->client->call(
|
||||
Client::METHOD_PATCH,
|
||||
"/tablesdb/{$databaseId}/tables/{$tableId}/rows/test/balance/decrement",
|
||||
array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()),
|
||||
[
|
||||
'transactionId' => $transactionId,
|
||||
'value' => 30,
|
||||
]
|
||||
);
|
||||
|
||||
// Commit using Collections API (expects 'attribute' key but should fallback to 'column')
|
||||
$commitResponse = $this->client->call(
|
||||
Client::METHOD_PATCH,
|
||||
"/databases/transactions/{$transactionId}",
|
||||
array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()),
|
||||
[
|
||||
'commit' => true
|
||||
]
|
||||
);
|
||||
|
||||
$this->assertEquals(200, $commitResponse['headers']['status-code'], 'Cross-API commit should succeed');
|
||||
|
||||
// Verify final value
|
||||
$row = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/test", array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
|
||||
$this->assertEquals(200, $row['headers']['status-code']);
|
||||
$this->assertEquals(70, $row['body']['balance'], 'Balance should be 100 - 30 = 70');
|
||||
}
|
||||
|
||||
public function testBulkUpdateWithDependentDocuments(): void
|
||||
{
|
||||
// Create database and table
|
||||
|
||||
@@ -32,6 +32,41 @@ class FunctionsCustomClientTest extends Scope
|
||||
'timeout' => 10,
|
||||
]);
|
||||
$this->assertEquals(401, $function['headers']['status-code']);
|
||||
|
||||
|
||||
/**
|
||||
* Test for DUPLICATE functionId
|
||||
*/
|
||||
$functionId = $this->setupFunction([
|
||||
'functionId' => ID::unique(),
|
||||
'name' => 'Test',
|
||||
'execute' => [Role::user($this->getUser()['$id'])->toString()],
|
||||
'runtime' => 'node-22',
|
||||
'entrypoint' => 'index.js',
|
||||
'events' => [
|
||||
'users.*.create',
|
||||
'users.*.delete',
|
||||
],
|
||||
'timeout' => 10,
|
||||
]);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_POST, '/functions', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
]), [
|
||||
'functionId' => $functionId,
|
||||
'name' => 'Test',
|
||||
'execute' => [Role::user($this->getUser()['$id'])->toString()],
|
||||
'runtime' => 'node-22',
|
||||
'entrypoint' => 'index.js',
|
||||
'events' => [
|
||||
'users.*.create',
|
||||
'users.*.delete',
|
||||
],
|
||||
'timeout' => 10,
|
||||
]);
|
||||
$this->assertEquals(409, $response['headers']['status-code']);
|
||||
}
|
||||
|
||||
public function testCreateExecution()
|
||||
|
||||
Reference in New Issue
Block a user