Merge pull request #967 from appwrite/feat-962-pass-data-to-function-execution

Pass custom function $data to execution
This commit is contained in:
Eldad A. Fux 2021-03-23 00:53:31 +02:00 committed by GitHub
commit c421741386
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 379 additions and 37 deletions

View file

@ -22,6 +22,7 @@
- Force adding a security email on setup
- SMTP is now disabled by default, no dummy SMTP is included in setup
- Added a new endpoint that returns the server and SDKs latest versions numbers #941
- Custom data strings, userId, and JWT available for cloud functions #967
## Upgrades

View file

@ -1,5 +1,7 @@
<?php
use Ahc\Jwt\JWT;
use Appwrite\Auth\Auth;
use Appwrite\Database\Database;
use Appwrite\Database\Document;
use Appwrite\Database\Validator\Authorization;
@ -440,7 +442,7 @@ App::post('/v1/functions/:functionId/tags')
->label('sdk.response.model', Response::MODEL_TAG)
->param('functionId', '', new UID(), 'Function unique ID.')
->param('command', '', new Text('1028'), 'Code execution command.')
->param('code', null, new File(), 'Gzip file with your code package. When used with the Appwrite CLI, pass the path to your code directory, and the CLI will automatically package your code. Use a path that is within the current directory.', false)
->param('code', [], new File(), 'Gzip file with your code package. When used with the Appwrite CLI, pass the path to your code directory, and the CLI will automatically package your code. Use a path that is within the current directory.', false)
->inject('request')
->inject('response')
->inject('projectDB')
@ -679,12 +681,13 @@ App::post('/v1/functions/:functionId/executions')
->label('abuse-limit', 60)
->label('abuse-time', 60)
->param('functionId', '', new UID(), 'Function unique ID.')
->param('data', '', new Text(8192), 'String of custom data to send to function.', true)
// ->param('async', 1, new Range(0, 1), 'Execute code asynchronously. Pass 1 for true, 0 for false. Default value is 1.', true)
->inject('response')
->inject('project')
->inject('projectDB')
->inject('user')
->action(function ($functionId, /*$async,*/ $response, $project, $projectDB, $user) {
->action(function ($functionId, $data, /*$async,*/ $response, $project, $projectDB, $user) {
/** @var Appwrite\Utopia\Response $response */
/** @var Appwrite\Database\Document $project */
/** @var Appwrite\Database\Database $projectDB */
@ -739,12 +742,36 @@ App::post('/v1/functions/:functionId/executions')
if (false === $execution) {
throw new Exception('Failed saving execution to DB', 500);
}
$jwt = ''; // initialize
if (!empty($user->getId())) { // If userId exists, generate a JWT for function
$tokens = $user->getAttribute('tokens', []);
$session = new Document();
foreach ($tokens as $token) { /** @var Appwrite\Database\Document $token */
if ($token->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too
$session = $token;
}
}
if(!$session->isEmpty()) {
$jwtObj = new JWT(App::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 900, 10); // Instantiate with key, algo, maxAge and leeway.
$jwt = $jwtObj->encode([
'userId' => $user->getId(),
'sessionId' => $session->getId(),
]);
}
}
Resque::enqueue('v1-functions', 'FunctionsV1', [
'projectId' => $project->getId(),
'functionId' => $function->getId(),
'executionId' => $execution->getId(),
'trigger' => 'http',
'data' => $data,
'userId' => $user->getId(),
'jwt' => $jwt,
]);
$response

View file

@ -50,24 +50,9 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled',true);
<p class="text-fade margin-bottom-small" data-ls-bind="{{project-function.env|envName}} {{project-function.env|envVersion}}">
</p>
<form data-ls-if="{{project-function.tag}} !== ''" name="functions.createExecution" class="margin-top"
data-analytics
data-analytics-activity
data-analytics-event="submit"
data-analytics-category="console"
data-analytics-label="Create Function Execution"
data-service="functions.createExecution"
data-event="submit"
data-param-function-id="{{router.params.id}}"
data-success="alert,trigger"
data-success-param-alert-text="Function executed successfully"
data-success-param-trigger-events="functions.createExecution"
data-failure="alert"
data-failure-param-alert-text="Failed to execute function"
data-failure-param-alert-classname="error">
<button style="vertical-align: top;">Execute Now</button> &nbsp; <a data-ls-attrs="href=/console/functions/function/logs?id={{router.params.id}}&project={{router.params.project}}" class="button reverse" style="vertical-align: top;">View Logs</a>
</form>
<div data-ls-if="{{project-function.tag}} !== ''" class="margin-top">
<button data-ls-ui-trigger="execute-now">Execute Now</button> &nbsp; <a data-ls-attrs="href=/console/functions/function/logs?id={{router.params.id}}&project={{router.params.project}}" class="button reverse" style="vertical-align: top;">View Logs</a>
</div>
</div>
</div>
@ -575,6 +560,31 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled',true);
</div>
</div>
<div data-ui-modal class="modal close box sticky-footer" data-button-hide="on" data-open-event="execute-now">
<button type="button" class="close pull-end" data-ui-modal-close=""><i class="icon-cancel"></i></button>
<h1 class="margin-bottom">Execute Function</h1>
<form data-ls-if="{{project-function.tag}} !== ''" name="functions.createExecution" class="margin-top"
data-analytics
data-analytics-activity
data-analytics-event="submit"
data-analytics-category="console"
data-analytics-label="Create Function Execution"
data-service="functions.createExecution"
data-event="submit"
data-param-function-id="{{router.params.id}}"
data-success="alert,trigger"
data-success-param-alert-text="Function executed successfully"
data-success-param-trigger-events="functions.createExecution"
data-failure="alert"
data-failure-param-alert-text="Failed to execute function"
data-failure-param-alert-classname="error">
<label for="execution-data">Custom Data</label>
<textarea id="execution-data" name="data" autocomplete="off" class="margin-bottom" placeholder="Data string (optional)"></textarea>
<button type="submit" style="vertical-align: top;">Execute Now</button>
</form>
</div>
<div data-ui-modal class="modal close box sticky-footer" data-button-hide="on" data-open-event="deploy-tag">
<button type="button" class="close pull-end" data-ui-modal-close=""><i class="icon-cancel"></i></button>

View file

@ -148,7 +148,9 @@ class FunctionsV1
$event = $this->args['event'] ?? '';
$scheduleOriginal = $this->args['scheduleOriginal'] ?? '';
$payload = (!empty($this->args['payload'])) ? json_encode($this->args['payload']) : '';
$data = $this->args['data'] ?? '';
$userId = $this->args['userId'] ?? '';
$jwt = $this->args['jwt'] ?? '';
$database = new Database();
$database->setAdapter(new RedisAdapter(new MySQLAdapter($register), $register));
@ -196,7 +198,7 @@ class FunctionsV1
Console::success('Triggered function: '.$event);
$this->execute('event', $projectId, '', $database, $function, $event, $payload, $userId);
$this->execute('event', $projectId, '', $database, $function, $event, $payload, $data, $userId, $jwt);
}
}
break;
@ -252,8 +254,7 @@ class FunctionsV1
'scheduleOriginal' => $function->getAttribute('schedule', ''),
]); // Async task rescheduale
$this->execute($trigger, $projectId, $executionId, $database, $function, $userId);
$this->execute($trigger, $projectId, $executionId, $database, $function, /*$event*/'', /*$payload*/'', $data, $userId, $jwt);
break;
case 'http':
@ -265,7 +266,7 @@ class FunctionsV1
throw new Exception('Function not found ('.$functionId.')');
}
$this->execute($trigger, $projectId, $executionId, $database, $function, $userId);
$this->execute($trigger, $projectId, $executionId, $database, $function, /*$event*/'', /*$payload*/'', $data, $userId, $jwt);
break;
default:
@ -284,10 +285,11 @@ class FunctionsV1
* @param Database $function
* @param string $event
* @param string $payload
* @param string $data
*
* @return void
*/
public function execute(string $trigger, string $projectId, string $executionId, Database $database, Document $function, string $event = '', string $payload = '', string $userId = ''): void
public function execute(string $trigger, string $projectId, string $executionId, Database $database, Document $function, string $event = '', string $payload = '', string $data = '', string $userId = '', string $jwt = ''): void
{
global $list;
@ -342,6 +344,10 @@ class FunctionsV1
'APPWRITE_FUNCTION_ENV_VERSION' => $environment['version'],
'APPWRITE_FUNCTION_EVENT' => $event,
'APPWRITE_FUNCTION_EVENT_PAYLOAD' => $payload,
'APPWRITE_FUNCTION_DATA' => $data,
'APPWRITE_FUNCTION_USER_ID' => $userId,
'APPWRITE_FUNCTION_JWT' => $jwt,
'APPWRITE_FUNCTION_PROJECT_ID' => $projectId,
]);
\array_walk($vars, function (&$value, $key) {

View file

@ -10,7 +10,23 @@
>
<testsuites>
<testsuite name="Application Test Suite">
<directory>./tests/e2e/</directory>
<file>./tests/e2e/Client.php</file>
<directory>./tests/e2e/General</directory>
<directory>./tests/e2e/Scopes</directory>
<directory>./tests/e2e/Services/Account</directory>
<directory>./tests/e2e/Services/Avatars</directory>
<directory>./tests/e2e/Services/Database</directory>
<file>./tests/e2e/Services/Functions/FunctionsBase.php</file>
<file>./tests/e2e/Services/Functions/FunctionsCustomServerTest.php</file>
<file>./tests/e2e/Services/Functions/FunctionsCustomClientTest.php</file>
<directory>./tests/e2e/Services/Health</directory>
<directory>./tests/e2e/Services/Locale</directory>
<directory>./tests/e2e/Services/Projects</directory>
<directory>./tests/e2e/Services/Storage</directory>
<directory>./tests/e2e/Services/Teams</directory>
<directory>./tests/e2e/Services/Users</directory>
<directory>./tests/e2e/Services/Webhooks</directory>
<directory>./tests/e2e/Services/Workers</directory>
<directory>./tests/unit/</directory>
</testsuite>
</testsuites>

View file

@ -188,8 +188,9 @@ let path='/functions/{functionId}/executions'.replace(new RegExp('{functionId}',
if(limit){payload['limit']=limit;}
if(offset){payload['offset']=offset;}
if(orderType){payload['orderType']=orderType;}
return http.get(path,{'content-type':'application/json',},payload);},createExecution:function(functionId){if(functionId===undefined){throw new Error('Missing required parameter: "functionId"');}
let path='/functions/{functionId}/executions'.replace(new RegExp('{functionId}','g'),functionId);let payload={};return http.post(path,{'content-type':'application/json',},payload);},getExecution:function(functionId,executionId){if(functionId===undefined){throw new Error('Missing required parameter: "functionId"');}
return http.get(path,{'content-type':'application/json',},payload);},createExecution:function(functionId,data){if(functionId===undefined){throw new Error('Missing required parameter: "functionId"');}
let path='/functions/{functionId}/executions'.replace(new RegExp('{functionId}','g'),functionId);let payload={};if(data){payload['data']=data;}
return http.post(path,{'content-type':'application/json',},payload);},getExecution:function(functionId,executionId){if(functionId===undefined){throw new Error('Missing required parameter: "functionId"');}
if(executionId===undefined){throw new Error('Missing required parameter: "executionId"');}
let path='/functions/{functionId}/executions/{executionId}'.replace(new RegExp('{functionId}','g'),functionId).replace(new RegExp('{executionId}','g'),executionId);let payload={};return http.get(path,{'content-type':'application/json',},payload);},updateTag:function(functionId,tag){if(functionId===undefined){throw new Error('Missing required parameter: "functionId"');}
if(tag===undefined){throw new Error('Missing required parameter: "tag"');}

View file

@ -188,8 +188,9 @@ let path='/functions/{functionId}/executions'.replace(new RegExp('{functionId}',
if(limit){payload['limit']=limit;}
if(offset){payload['offset']=offset;}
if(orderType){payload['orderType']=orderType;}
return http.get(path,{'content-type':'application/json',},payload);},createExecution:function(functionId){if(functionId===undefined){throw new Error('Missing required parameter: "functionId"');}
let path='/functions/{functionId}/executions'.replace(new RegExp('{functionId}','g'),functionId);let payload={};return http.post(path,{'content-type':'application/json',},payload);},getExecution:function(functionId,executionId){if(functionId===undefined){throw new Error('Missing required parameter: "functionId"');}
return http.get(path,{'content-type':'application/json',},payload);},createExecution:function(functionId,data){if(functionId===undefined){throw new Error('Missing required parameter: "functionId"');}
let path='/functions/{functionId}/executions'.replace(new RegExp('{functionId}','g'),functionId);let payload={};if(data){payload['data']=data;}
return http.post(path,{'content-type':'application/json',},payload);},getExecution:function(functionId,executionId){if(functionId===undefined){throw new Error('Missing required parameter: "functionId"');}
if(executionId===undefined){throw new Error('Missing required parameter: "executionId"');}
let path='/functions/{functionId}/executions/{executionId}'.replace(new RegExp('{functionId}','g'),functionId).replace(new RegExp('{executionId}','g'),executionId);let payload={};return http.get(path,{'content-type':'application/json',},payload);},updateTag:function(functionId,tag){if(functionId===undefined){throw new Error('Missing required parameter: "functionId"');}
if(tag===undefined){throw new Error('Missing required parameter: "tag"');}

View file

@ -2075,10 +2075,11 @@
* function execution process will start asynchronously.
*
* @param {string} functionId
* @param {string} data
* @throws {Error}
* @return {Promise}
*/
createExecution: function(functionId) {
createExecution: function(functionId, data) {
if(functionId === undefined) {
throw new Error('Missing required parameter: "functionId"');
}
@ -2087,6 +2088,10 @@
let payload = {};
if (data) {
payload['data'] = data;
}
return http
.post(path, {
'content-type': 'application/json',

View file

@ -80,9 +80,9 @@ class FunctionsCustomClientTest extends Scope
]);
$tagId = $tag['body']['$id'] ?? '';
$this->assertEquals(201, $tag['headers']['status-code']);
$function = $this->client->call(Client::METHOD_PATCH, '/functions/'.$function['body']['$id'].'/tag', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
@ -90,7 +90,7 @@ class FunctionsCustomClientTest extends Scope
], [
'tag' => $tagId,
]);
$this->assertEquals(200, $function['headers']['status-code']);
$execution = $this->client->call(Client::METHOD_POST, '/functions/'.$function['body']['$id'].'/executions', [
@ -124,4 +124,81 @@ class FunctionsCustomClientTest extends Scope
return [];
}
}
public function testCreateCustomExecution():array
{
/**
* Test for SUCCESS
*/
$projectId = $this->getProject()['$id'];
$apikey = $this->getProject()['apiKey'];
$function = $this->client->call(Client::METHOD_POST, '/functions', [
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $apikey,
], [
'name' => 'Test',
'execute' => ['*'],
'env' => 'php-7.4',
'vars' => [
'funcKey1' => 'funcValue1',
'funcKey2' => 'funcValue2',
'funcKey3' => 'funcValue3',
],
'timeout' => 10,
]);
$functionId = $function['body']['$id'] ?? '';
$this->assertEquals(201, $function['headers']['status-code']);
$tag = $this->client->call(Client::METHOD_POST, '/functions/'.$functionId.'/tags', [
'content-type' => 'multipart/form-data',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $apikey,
], [
'command' => 'php index.php',
'code' => new CURLFile(realpath(__DIR__ . '/../../../resources/functions/php-fn.tar.gz'), 'application/x-gzip', 'php-fx.tar.gz'), //different tarball names intentional
]);
$tagId = $tag['body']['$id'] ?? '';
$this->assertEquals(201, $tag['headers']['status-code']);
$function = $this->client->call(Client::METHOD_PATCH, '/functions/'.$functionId.'/tag', [
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $apikey,
], [
'tag' => $tagId,
]);
$this->assertEquals(200, $function['headers']['status-code']);
$execution = $this->client->call(Client::METHOD_POST, '/functions/'.$functionId.'/executions', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], $this->getHeaders()), [
'data' => 'foobar',
]);
$this->assertEquals(201, $execution['headers']['status-code']);
sleep(10);
$executions = $this->client->call(Client::METHOD_GET, '/functions/'.$functionId.'/executions', [
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $apikey,
]);
$this->assertEquals(200, $executions['headers']['status-code']);
$this->assertCount(1, $executions['body']['executions']);
$this->assertEquals('completed', $executions['body']['executions'][0]['status']);
$this->assertStringContainsString('foobar', $executions['body']['executions'][0]['stdout']);
$this->assertStringContainsString($this->getUser()['$id'], $executions['body']['executions'][0]['stdout']);
return [];
}
}

View file

@ -628,7 +628,7 @@ class FunctionsCustomServerTest extends Scope
], $this->getHeaders()), [
'tag' => $tagId,
]);
$this->assertEquals(200, $tag['headers']['status-code']);
$execution = $this->client->call(Client::METHOD_POST, '/functions/'.$functionId.'/executions', array_merge([
@ -760,4 +760,77 @@ class FunctionsCustomServerTest extends Scope
$this->assertEquals($executions['body']['executions'][0]['stdout'], '');
$this->assertEquals($executions['body']['executions'][0]['stderr'], '');
}
/**
* @depends testTimeout
*/
public function testCreateCustomExecution()
{
$name = 'php-8.0';
$code = realpath(__DIR__ . '/../../../resources/functions').'/php-fn.tar.gz';
$command = 'php index.php';
$timeout = 2;
$function = $this->client->call(Client::METHOD_POST, '/functions', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'name' => 'Test '.$name,
'env' => $name,
'vars' => [],
'events' => [],
'schedule' => '',
'timeout' => $timeout,
]);
$functionId = $function['body']['$id'] ?? '';
$this->assertEquals(201, $function['headers']['status-code']);
$tag = $this->client->call(Client::METHOD_POST, '/functions/'.$functionId.'/tags', array_merge([
'content-type' => 'multipart/form-data',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'command' => $command,
'code' => new CURLFile($code, 'application/x-gzip', basename($code)),
]);
$tagId = $tag['body']['$id'] ?? '';
$this->assertEquals(201, $tag['headers']['status-code']);
$tag = $this->client->call(Client::METHOD_PATCH, '/functions/'.$functionId.'/tag', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'tag' => $tagId,
]);
$this->assertEquals(200, $tag['headers']['status-code']);
$execution = $this->client->call(Client::METHOD_POST, '/functions/'.$functionId.'/executions', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'data' => 'foobar',
]);
$executionId = $execution['body']['$id'] ?? '';
$this->assertEquals(201, $execution['headers']['status-code']);
sleep(10);
$executions = $this->client->call(Client::METHOD_GET, '/functions/'.$functionId.'/executions', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals($executions['headers']['status-code'], 200);
$this->assertEquals($executions['body']['sum'], 1);
$this->assertIsArray($executions['body']['executions']);
$this->assertCount(1, $executions['body']['executions']);
$this->assertEquals($executions['body']['executions'][0]['$id'], $executionId);
$this->assertEquals($executions['body']['executions'][0]['trigger'], 'http');
$this->assertStringContainsString('foobar', $executions['body']['executions'][0]['stdout']);
}
}

View file

@ -0,0 +1,12 @@
echo 'PHP Packaging...'
cp -r $(pwd)/tests/resources/functions/php-fn $(pwd)/tests/resources/functions/packages/php-fn
docker run --rm -v $(pwd)/tests/resources/functions/packages/php-fn:/app -w /app composer:2.0 composer install --ignore-platform-reqs
docker run --rm -v $(pwd)/tests/resources/functions/packages/php-fn:/app -w /app appwrite/env-php-8.0:1.0.0 tar -zcvf code.tar.gz .
mv $(pwd)/tests/resources/functions/packages/php-fn/code.tar.gz $(pwd)/tests/resources/functions/php-fn.tar.gz
rm -r $(pwd)/tests/resources/functions/packages/php-fn

Binary file not shown.

View file

@ -0,0 +1,18 @@
{
"name": "appwrite/cloud-function-demo",
"description": "Demo cloud function script",
"type": "library",
"license": "BSD-3-Clause",
"authors": [
{
"name": "Team Appwrite",
"email": "team@appwrite.io"
}
],
"require": {
"php": ">=7.4.0",
"ext-curl": "*",
"ext-json": "*",
"appwrite/appwrite": "1.1.*"
}
}

View file

@ -0,0 +1,64 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "afdff6a172e6c44aee11f1562175f81a",
"packages": [
{
"name": "appwrite/appwrite",
"version": "1.1.2",
"source": {
"type": "git",
"url": "https://github.com/appwrite/sdk-for-php.git",
"reference": "98b327d3fd18a72f4582019916afd735a0e9e0e7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/98b327d3fd18a72f4582019916afd735a0e9e0e7",
"reference": "98b327d3fd18a72f4582019916afd735a0e9e0e7",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-json": "*",
"php": ">=7.1.0"
},
"require-dev": {
"phpunit/phpunit": "3.7.35"
},
"type": "library",
"autoload": {
"psr-4": {
"Appwrite\\": "src/Appwrite"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"description": "Appwrite is an open-source backend as a service server that abstract and simplify complex and repetitive development tasks",
"support": {
"email": "team@localhost.test",
"issues": "https://github.com/appwrite/sdk-for-php/issues",
"source": "https://github.com/appwrite/sdk-for-php/tree/1.1.2",
"url": "https://appwrite.io/support"
},
"time": "2020-08-15T18:24:32+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": ">=7.4.0",
"ext-curl": "*",
"ext-json": "*"
},
"platform-dev": [],
"plugin-api-version": "2.0.0"
}

View file

@ -0,0 +1,31 @@
<?php
include './vendor/autoload.php';
use Appwrite\Client;
use Appwrite\Services\Storage;
// $client = new Client();
// $client
// ->setEndpoint($_ENV['APPWRITE_ENDPOINT']) // Your API Endpoint
// ->setProject($_ENV['APPWRITE_PROJECT']) // Your project ID
// ->setKey($_ENV['APPWRITE_SECRET']) // Your secret API key
// ;
// $storage = new Storage($client);
// $result = $storage->getFile($_ENV['APPWRITE_FILEID']);
echo $_ENV['APPWRITE_FUNCTION_ID']."\n";
echo $_ENV['APPWRITE_FUNCTION_NAME']."\n";
echo $_ENV['APPWRITE_FUNCTION_TAG']."\n";
echo $_ENV['APPWRITE_FUNCTION_TRIGGER']."\n";
echo $_ENV['APPWRITE_FUNCTION_ENV_NAME']."\n";
echo $_ENV['APPWRITE_FUNCTION_ENV_VERSION']."\n";
// echo $result['$id'];
echo $_ENV['APPWRITE_FUNCTION_EVENT']."\n";
echo $_ENV['APPWRITE_FUNCTION_EVENT_PAYLOAD']."\n";
echo 'data:'.$_ENV['APPWRITE_FUNCTION_DATA']."\n";
echo 'userId:'.$_ENV['APPWRITE_FUNCTION_USER_ID']."\n";
echo 'jwt:'.$_ENV['APPWRITE_FUNCTION_JWT']."\n";