From dcf1684c8ca0cd0196c98b3080772e7f3876c8cb Mon Sep 17 00:00:00 2001 From: Khushboo Verma <43381712+vermakhushboo@users.noreply.github.com> Date: Wed, 7 Aug 2024 18:45:53 +0530 Subject: [PATCH] Update dynamic response method --- app/controllers/api/functions.php | 43 ++------------ composer.lock | 30 +++++----- src/Appwrite/Utopia/Response.php | 56 ++++++++++++++++++- .../Functions/FunctionsCustomServerTest.php | 8 +-- 4 files changed, 79 insertions(+), 58 deletions(-) diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php index b83229f54f..5fd3519da4 100644 --- a/app/controllers/api/functions.php +++ b/app/controllers/api/functions.php @@ -18,7 +18,6 @@ use Appwrite\Utopia\Database\Validator\CustomId; use Appwrite\Utopia\Database\Validator\Queries\Deployments; use Appwrite\Utopia\Database\Validator\Queries\Executions; use Appwrite\Utopia\Database\Validator\Queries\Functions; -use Appwrite\Utopia\Fetch\BodyMultipart; use Appwrite\Utopia\Response; use Appwrite\Utopia\Response\Model\Rule; use Executor\Executor; @@ -1641,20 +1640,6 @@ App::post('/v1/functions/:functionId/executions') } } - $datetimeParams = ['scheduledAt']; - foreach ($datetimeParams as $datetimeParam) { - if (!empty($$datetimeParam)) { - $$datetimeParam = new DateTime($$datetimeParam); - } - } - - $validator = new DatetimeValidator(requireDateInFuture: true); - foreach ($datetimeParams as $datetimeParam) { - if (!empty($$datetimeParam) && !$validator->isValid($$datetimeParam)) { - throw new Exception($validator->getDescription(), 400); - } - } - // 'headers' validator $validator = new Headers(); if (!$validator->isValid($headers)) { @@ -1967,9 +1952,9 @@ App::post('/v1/functions/:functionId/executions') $execution->setAttribute('responseBody', $executionResponse['body'] ?? ''); $execution->setAttribute('responseHeaders', $headers); - $acceptTypes = \explode(', ', $request->getHeader('accept', 'application/json')); $isJson = false; + $acceptTypes = \explode(', ', $request->getHeader('accept', 'application/json')); foreach ($acceptTypes as $acceptType) { if (\str_starts_with($acceptType, 'application/json') || \str_starts_with($acceptType, 'application/*')) { $isJson = true; @@ -1977,28 +1962,10 @@ App::post('/v1/functions/:functionId/executions') } } - if ($isJson) { - $executionString = \json_encode($execution, JSON_UNESCAPED_UNICODE); - if (!$executionString) { - throw new Exception('Execution resulted in binary response, but JSON response does not allow binaries. Use "Accept: multipart/form-data" header to support binaries.', 400); - } - - $response - ->setStatusCode(Response::STATUS_CODE_OK) - ->addHeader('content-type', 'application/json') - ->send($executionString); - } else { - // Multipart form data response - $multipart = new BodyMultipart(); - foreach ($execution as $key => $value) { - $multipart->setPart($key, $value); - } - - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->addHeader('content-type', $multipart->exportHeader()) - ->send($multipart->exportBody()); - } + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->setContentType($isJson ? Response::CONTENT_TYPE_JSON : Response::CONTENT_TYPE_MULTIPART) + ->dynamic($execution, Response::MODEL_EXECUTION); }); App::get('/v1/functions/:functionId/executions') diff --git a/composer.lock b/composer.lock index 1509935c09..76b1579a87 100644 --- a/composer.lock +++ b/composer.lock @@ -2990,16 +2990,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "0.39.4", + "version": "0.39.5", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "501b92d73ae55e0f880ed00f57bc64a54d0ce137" + "reference": "40d0f66f2f85be74049ad710b46203aa151f53fd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/501b92d73ae55e0f880ed00f57bc64a54d0ce137", - "reference": "501b92d73ae55e0f880ed00f57bc64a54d0ce137", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/40d0f66f2f85be74049ad710b46203aa151f53fd", + "reference": "40d0f66f2f85be74049ad710b46203aa151f53fd", "shasum": "" }, "require": { @@ -3035,9 +3035,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.39.4" + "source": "https://github.com/appwrite/sdk-generator/tree/0.39.5" }, - "time": "2024-07-26T22:34:10+00:00" + "time": "2024-08-06T00:51:40+00:00" }, { "name": "doctrine/deprecations", @@ -3158,16 +3158,16 @@ }, { "name": "laravel/pint", - "version": "v1.17.1", + "version": "v1.17.2", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "b5b6f716db298671c1dfea5b1082ec2c0ae7064f" + "reference": "e8a88130a25e3f9d4d5785e6a1afca98268ab110" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/b5b6f716db298671c1dfea5b1082ec2c0ae7064f", - "reference": "b5b6f716db298671c1dfea5b1082ec2c0ae7064f", + "url": "https://api.github.com/repos/laravel/pint/zipball/e8a88130a25e3f9d4d5785e6a1afca98268ab110", + "reference": "e8a88130a25e3f9d4d5785e6a1afca98268ab110", "shasum": "" }, "require": { @@ -3178,13 +3178,13 @@ "php": "^8.1.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.59.3", - "illuminate/view": "^10.48.12", - "larastan/larastan": "^2.9.7", + "friendsofphp/php-cs-fixer": "^3.61.1", + "illuminate/view": "^10.48.18", + "larastan/larastan": "^2.9.8", "laravel-zero/framework": "^10.4.0", "mockery/mockery": "^1.6.12", "nunomaduro/termwind": "^1.15.1", - "pestphp/pest": "^2.34.8" + "pestphp/pest": "^2.35.0" }, "bin": [ "builds/pint" @@ -3220,7 +3220,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2024-08-01T09:06:33+00:00" + "time": "2024-08-06T15:11:54+00:00" }, { "name": "matthiasmullie/minify", diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index d2e78bb310..f0dace191e 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -2,6 +2,7 @@ namespace Appwrite\Utopia; +use Appwrite\Utopia\Fetch\BodyMultipart; use Appwrite\Utopia\Response\Filter; use Appwrite\Utopia\Response\Model; use Appwrite\Utopia\Response\Model\Account; @@ -107,6 +108,7 @@ use Appwrite\Utopia\Response\Model\Variable; use Appwrite\Utopia\Response\Model\VcsContent; use Appwrite\Utopia\Response\Model\Webhook; use Exception; +use JsonException; use Swoole\Http\Response as SwooleHTTPResponse; // Keep last use Utopia\Database\Document; @@ -486,6 +488,7 @@ class Response extends SwooleResponse */ public const CONTENT_TYPE_YAML = 'application/x-yaml'; public const CONTENT_TYPE_NULL = 'null'; + public const CONTENT_TYPE_MULTIPART = 'multipart/form-data'; /** * List of defined output objects @@ -556,7 +559,11 @@ class Response extends SwooleResponse switch ($this->getContentType()) { case self::CONTENT_TYPE_JSON: - $this->json(!empty($output) ? $output : new \stdClass()); + try { + $this->json(!empty($output) ? $output : new \stdClass()); + } catch (JsonException $e) { + throw new Exception('Failed to parse binary response: ' . $e->getMessage(), 400); + } break; case self::CONTENT_TYPE_YAML: @@ -566,6 +573,10 @@ class Response extends SwooleResponse case self::CONTENT_TYPE_NULL: break; + case self::CONTENT_TYPE_MULTIPART: + $this->multipart(!empty($output) ? $output : new \stdClass()); + break; + default: if ($model === self::MODEL_NONE) { $this->noContent(); @@ -697,6 +708,49 @@ class Response extends SwooleResponse ->send(\yaml_emit($data, YAML_UTF8_ENCODING)); } + /** + * Multipart + * + * This helper is for sending multipart/form-data HTTP response. + * It sets relevant content type header ('multipart/form-data') and convert a PHP array ($data) to valid Multipart using BodyMultipart + * + * @param array $data + * + * @return void + */ + public function multipart(array $data): void + { + $multipart = new BodyMultipart(); + foreach ($data as $key => $value) { + $multipart->setPart($key, $value); + } + + $this + ->send($multipart->exportBody()); + } + + /** + * JSON + * + * This helper is for sending JSON HTTP response. + * It sets relevant content type header ('application/json') and convert a PHP array ($data) to valid JSON using native json_encode + * + * @see http://en.wikipedia.org/wiki/JSON + * + * @param mixed $data + * @return void + */ + public function json($data): void + { + if (!is_array($data) && !$data instanceof \stdClass) { + throw new \Exception('Invalid JSON input var'); + } + + $this + ->setContentType(Response::CONTENT_TYPE_JSON, self::CHARSET_UTF8) + ->send(\json_encode($data, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR)); + } + /** * @return array */ diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index f52fba051e..6f3b3d0c52 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -1403,8 +1403,8 @@ class FunctionsCustomServerTest extends Scope 'body' => null, ]); - $this->assertEquals(500, $execution['headers']['status-code']); - $this->assertStringContainsString('Execution resulted in binary response, but JSON response does not allow binaries.', $execution['body']['type']); + $this->assertEquals(400, $execution['headers']['status-code']); + $this->assertStringContainsString('Failed to parse binary response', $execution['body']['message']); // Cleanup : Delete function $response = $this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId, [ @@ -1499,9 +1499,9 @@ class FunctionsCustomServerTest extends Scope $executionBody = json_decode($execution['body'], true); - $this->assertEquals(200, $execution['headers']['status-code']); + $this->assertEquals(201, $execution['headers']['status-code']); $this->assertEquals(\md5($bytes), $executionBody['responseBody']); - $this->assertEquals($execution['headers']['content-type'], 'application/json'); + $this->assertStringStartsWith('application/json', $execution['headers']['content-type']); /** * Test for FAILURE