From fd6a3ed30cecd03b8648f9c40d56261581f6e61a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 9 Sep 2022 08:05:33 +0000 Subject: [PATCH 1/6] Fix listVariables returing all variables --- app/controllers/api/functions.php | 3 +++ composer.lock | 14 +++++++------- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php index 57928f5f1c..26c8ffc006 100644 --- a/app/controllers/api/functions.php +++ b/app/controllers/api/functions.php @@ -1376,6 +1376,9 @@ App::get('/v1/functions/:functionId/variables') $queries[] = Query::search('search', $search); } + // Apply internal queries + $queries[] = Query::equal('functionInternalId', [$function->getInternalId()]); + // Get cursor document if there was a cursor query $cursor = Query::getByType($queries, Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE); $cursor = reset($cursor); diff --git a/composer.lock b/composer.lock index 8245baac5f..1f61b8aef6 100644 --- a/composer.lock +++ b/composer.lock @@ -2060,16 +2060,16 @@ }, { "name": "utopia-php/database", - "version": "0.25.1", + "version": "0.25.2", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "9d013ce3c111d1477d7986483f1003dcab2b9d14" + "reference": "140bbedf1c4d622990fb94d26681fcca235cd5b9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/9d013ce3c111d1477d7986483f1003dcab2b9d14", - "reference": "9d013ce3c111d1477d7986483f1003dcab2b9d14", + "url": "https://api.github.com/repos/utopia-php/database/zipball/140bbedf1c4d622990fb94d26681fcca235cd5b9", + "reference": "140bbedf1c4d622990fb94d26681fcca235cd5b9", "shasum": "" }, "require": { @@ -2118,9 +2118,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/0.25.1" + "source": "https://github.com/utopia-php/database/tree/0.25.2" }, - "time": "2022-09-07T14:47:52+00:00" + "time": "2022-09-09T03:58:01+00:00" }, { "name": "utopia-php/domains", @@ -5384,5 +5384,5 @@ "platform-overrides": { "php": "8.0" }, - "plugin-api-version": "2.2.0" + "plugin-api-version": "2.3.0" } From b41aaefc7e5ffe3707e27f788b52660646c85c9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 9 Sep 2022 08:49:18 +0000 Subject: [PATCH 2/6] Implement 'enabled' attribute for functions --- app/config/collections.php | 19 ++++++----- app/controllers/api/functions.php | 35 ++++++++++++++------- src/Appwrite/Utopia/Response/Model/Func.php | 10 +++--- 3 files changed, 38 insertions(+), 26 deletions(-) diff --git a/app/config/collections.php b/app/config/collections.php index 633e2c33b9..6396e255cb 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -2084,15 +2084,14 @@ $collections = [ 'filters' => [], ], [ - 'array' => false, - '$id' => ID::custom('status'), - 'type' => Database::VAR_STRING, - 'format' => '', - 'size' => Database::LENGTH_KEY, + '$id' => ID::custom('enabled'), + 'type' => Database::VAR_BOOLEAN, 'signed' => true, - 'required' => false, - 'default' => null, + 'size' => 0, + 'format' => '', 'filters' => [], + 'required' => true, + 'array' => false, ], [ '$id' => ID::custom('runtime'), @@ -2210,10 +2209,10 @@ $collections = [ 'orders' => [Database::ORDER_ASC], ], [ - '$id' => ID::custom('_key_status'), + '$id' => ID::custom('_key_enabled'), 'type' => Database::INDEX_KEY, - 'attributes' => ['status'], - 'lengths' => [Database::LENGTH_KEY], + 'attributes' => ['enabled'], + 'lengths' => [], 'orders' => [Database::ORDER_ASC], ], [ diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php index 26c8ffc006..375ac0e453 100644 --- a/app/controllers/api/functions.php +++ b/app/controllers/api/functions.php @@ -76,7 +76,7 @@ App::post('/v1/functions') $function = $dbForProject->createDocument('functions', new Document([ '$id' => $functionId, 'execute' => $execute, - 'status' => 'disabled', + 'enabled' => true, 'name' => $name, 'runtime' => $runtime, 'deployment' => '', @@ -424,12 +424,13 @@ App::put('/v1/functions/:functionId') ->param('events', [], new ArrayList(new ValidatorEvent(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Events list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.', true) ->param('schedule', '', new Cron(), 'Schedule CRON syntax.', true) ->param('timeout', 15, new Range(1, (int) App::getEnv('_APP_FUNCTIONS_TIMEOUT', 900)), 'Maximum execution time in seconds.', true) + ->param('enabled', true, new Boolean(), 'Is collection enabled?', true) ->inject('response') ->inject('dbForProject') ->inject('project') ->inject('user') ->inject('events') - ->action(function (string $functionId, string $name, array $execute, array $events, string $schedule, int $timeout, Response $response, Database $dbForProject, Document $project, Document $user, Event $eventsInstance) { + ->action(function (string $functionId, string $name, array $execute, array $events, string $schedule, int $timeout, bool $enabled, Response $response, Database $dbForProject, Document $project, Document $user, Event $eventsInstance) { $function = $dbForProject->getDocument('functions', $functionId); @@ -441,6 +442,8 @@ App::put('/v1/functions/:functionId') $cron = (!empty($function->getAttribute('deployment')) && !empty($schedule)) ? new CronExpression($schedule) : null; $next = (!empty($function->getAttribute('deployment')) && !empty($schedule)) ? DateTime::format($cron->getNextRunDate()) : null; + $enabled ??= $function->getAttribute('enabled', true); + $function = $dbForProject->updateDocument('functions', $function->getId(), new Document(array_merge($function->getArrayCopy(), [ 'execute' => $execute, 'name' => $name, @@ -448,6 +451,7 @@ App::put('/v1/functions/:functionId') 'schedule' => $schedule, 'scheduleNext' => $next, 'timeout' => $timeout, + 'enabled' => $enabled, 'search' => implode(' ', [$functionId, $name, $function->getAttribute('runtime')]), ]))); @@ -945,12 +949,15 @@ App::post('/v1/functions/:functionId/executions') ->inject('user') ->inject('events') ->inject('usage') - ->action(function (string $functionId, string $data, bool $async, Response $response, Document $project, Database $dbForProject, Document $user, Event $events, Stats $usage) { + ->inject('mode') + ->action(function (string $functionId, string $data, bool $async, Response $response, Document $project, Database $dbForProject, Document $user, Event $events, Stats $usage, string $mode) { $function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId)); - if ($function->isEmpty()) { - throw new Exception(Exception::FUNCTION_NOT_FOUND); + if ($function->isEmpty() || !$function->getAttribute('enabled')) { + if (!($mode === APP_MODE_ADMIN && Auth::isPrivilegedUser(Authorization::getRoles()))) { + throw new Exception(Exception::FUNCTION_NOT_FOUND); + } } $runtimes = Config::getParam('runtimes', []); @@ -1137,12 +1144,15 @@ App::get('/v1/functions/:functionId/executions') ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) ->inject('response') ->inject('dbForProject') - ->action(function (string $functionId, array $queries, string $search, Response $response, Database $dbForProject) { + ->inject('mode') + ->action(function (string $functionId, array $queries, string $search, Response $response, Database $dbForProject, string $mode) { $function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId)); - if ($function->isEmpty()) { - throw new Exception(Exception::FUNCTION_NOT_FOUND); + if ($function->isEmpty() || !$function->getAttribute('enabled')) { + if (!($mode === APP_MODE_ADMIN && Auth::isPrivilegedUser(Authorization::getRoles()))) { + throw new Exception(Exception::FUNCTION_NOT_FOUND); + } } $queries = Query::parseQueries($queries); @@ -1206,12 +1216,15 @@ App::get('/v1/functions/:functionId/executions/:executionId') ->param('executionId', '', new UID(), 'Execution ID.') ->inject('response') ->inject('dbForProject') - ->action(function (string $functionId, string $executionId, Response $response, Database $dbForProject) { + ->inject('mode') + ->action(function (string $functionId, string $executionId, Response $response, Database $dbForProject, string $mode) { $function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId)); - if ($function->isEmpty()) { - throw new Exception(Exception::FUNCTION_NOT_FOUND); + if ($function->isEmpty() || !$function->getAttribute('enabled')) { + if (!($mode === APP_MODE_ADMIN && Auth::isPrivilegedUser(Authorization::getRoles()))) { + throw new Exception(Exception::FUNCTION_NOT_FOUND); + } } $execution = $dbForProject->getDocument('executions', $executionId); diff --git a/src/Appwrite/Utopia/Response/Model/Func.php b/src/Appwrite/Utopia/Response/Model/Func.php index fbe23bab0b..c7e69fff88 100644 --- a/src/Appwrite/Utopia/Response/Model/Func.php +++ b/src/Appwrite/Utopia/Response/Model/Func.php @@ -43,11 +43,11 @@ class Func extends Model 'default' => '', 'example' => 'My Function', ]) - ->addRule('status', [ - 'type' => self::TYPE_STRING, - 'description' => 'Function status. Possible values: `disabled`, `enabled`', - 'default' => '', - 'example' => 'enabled', + ->addRule('enabled', [ + 'type' => self::TYPE_BOOLEAN, + 'description' => 'Function enabled.', + 'default' => true, + 'example' => false, ]) ->addRule('runtime', [ 'type' => self::TYPE_STRING, From 34dda741764229e7a8f65e52627a7e5c70e3c24f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Sun, 11 Sep 2022 08:31:40 +0000 Subject: [PATCH 3/6] Update tests, fix bug --- src/Appwrite/Utopia/Database/Validator/Queries/Functions.php | 2 +- tests/e2e/Services/Functions/FunctionsCustomServerTest.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Appwrite/Utopia/Database/Validator/Queries/Functions.php b/src/Appwrite/Utopia/Database/Validator/Queries/Functions.php index ee4c311163..a2ba368953 100644 --- a/src/Appwrite/Utopia/Database/Validator/Queries/Functions.php +++ b/src/Appwrite/Utopia/Database/Validator/Queries/Functions.php @@ -6,7 +6,7 @@ class Functions extends Base { public const ALLOWED_ATTRIBUTES = [ 'name', - 'status', + 'enabled', 'runtime', 'deployment', 'schedule', diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index 8e3fd47b38..2d037abbf2 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -140,7 +140,7 @@ class FunctionsCustomServerTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ - 'queries' => [ 'equal("status", "disabled")' ] + 'queries' => [ 'equal("enabled", true)' ] ]); $this->assertEquals($response['headers']['status-code'], 200); @@ -150,7 +150,7 @@ class FunctionsCustomServerTest extends Scope 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ - 'queries' => [ 'equal("status", "enabled")' ] + 'queries' => [ 'equal("enabled", false)' ] ]); $this->assertEquals($response['headers']['status-code'], 200); From ec5a0b8cdef0b655c90912ae2744b12f42f3de12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 12 Sep 2022 09:02:14 +0000 Subject: [PATCH 4/6] Remove queries from listVariables; add enabled to createFunction --- app/controllers/api/functions.php | 41 +++--------- app/views/console/functions/function.phtml | 2 - .../Functions/FunctionsConsoleClientTest.php | 63 +------------------ 3 files changed, 9 insertions(+), 97 deletions(-) diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php index 375ac0e453..d1edee3e38 100644 --- a/app/controllers/api/functions.php +++ b/app/controllers/api/functions.php @@ -67,16 +67,17 @@ App::post('/v1/functions') ->param('events', [], new ArrayList(new ValidatorEvent(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Events list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.', true) ->param('schedule', '', new Cron(), 'Schedule CRON syntax.', true) ->param('timeout', 15, new Range(1, (int) App::getEnv('_APP_FUNCTIONS_TIMEOUT', 900)), 'Function maximum execution time in seconds.', true) + ->param('enabled', true, new Boolean(), 'Is function enabled?', true) ->inject('response') ->inject('dbForProject') ->inject('events') - ->action(function (string $functionId, string $name, array $execute, string $runtime, array $events, string $schedule, int $timeout, Response $response, Database $dbForProject, Event $eventsInstance) { + ->action(function (string $functionId, string $name, array $execute, string $runtime, array $events, string $schedule, int $timeout, bool $enabled, Response $response, Database $dbForProject, Event $eventsInstance) { $functionId = ($functionId == 'unique()') ? ID::unique() : $functionId; $function = $dbForProject->createDocument('functions', new Document([ '$id' => $functionId, 'execute' => $execute, - 'enabled' => true, + 'enabled' => $enabled, 'name' => $name, 'runtime' => $runtime, 'deployment' => '', @@ -424,7 +425,7 @@ App::put('/v1/functions/:functionId') ->param('events', [], new ArrayList(new ValidatorEvent(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Events list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.', true) ->param('schedule', '', new Cron(), 'Schedule CRON syntax.', true) ->param('timeout', 15, new Range(1, (int) App::getEnv('_APP_FUNCTIONS_TIMEOUT', 900)), 'Maximum execution time in seconds.', true) - ->param('enabled', true, new Boolean(), 'Is collection enabled?', true) + ->param('enabled', true, new Boolean(), 'Is function enabled?', true) ->inject('response') ->inject('dbForProject') ->inject('project') @@ -1372,46 +1373,18 @@ App::get('/v1/functions/:functionId/variables') ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_VARIABLE_LIST) ->param('functionId', null, new UID(), 'Function unique ID.', false) - ->param('queries', [], new Variables(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Variables::ALLOWED_ATTRIBUTES), true) - ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) ->inject('response') ->inject('dbForProject') - ->action(function (string $functionId, array $queries, string $search, Response $response, Database $dbForProject) { + ->action(function (string $functionId, Response $response, Database $dbForProject) { $function = $dbForProject->getDocument('functions', $functionId); if ($function->isEmpty()) { throw new Exception(Exception::FUNCTION_NOT_FOUND); } - $queries = Query::parseQueries($queries); - - if (!empty($search)) { - $queries[] = Query::search('search', $search); - } - - // Apply internal queries - $queries[] = Query::equal('functionInternalId', [$function->getInternalId()]); - - // Get cursor document if there was a cursor query - $cursor = Query::getByType($queries, Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE); - $cursor = reset($cursor); - if ($cursor) { - /** @var Query $cursor */ - $variableId = $cursor->getValue(); - $cursorDocument = $dbForProject->getDocument('variables', $variableId); - - if ($cursorDocument->isEmpty()) { - throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Variable '{$variableId}' for the 'cursor' value not found."); - } - - $cursor->setValue($cursorDocument); - } - - $filterQueries = Query::groupByType($queries)['filters']; - $response->dynamic(new Document([ - 'variables' => $dbForProject->find('variables', $queries), - 'total' => $dbForProject->count('variables', $filterQueries, APP_LIMIT_COUNT), + 'variables' => $function->getAttribute('vars'), + 'total' => \count($function->getAttribute('vars')), ]), Response::MODEL_VARIABLE_LIST); }); diff --git a/app/views/console/functions/function.phtml b/app/views/console/functions/function.phtml index 235892c2db..d6290986e8 100644 --- a/app/views/console/functions/function.phtml +++ b/app/views/console/functions/function.phtml @@ -523,8 +523,6 @@ sort($patterns); data-service="functions.listVariables" data-event="load,project.update,functions.createVariable,functions.updateVariable,functions.deleteVariable" data-name="function-variables" - data-param-queries="limit(100)" - data-param-queries-cast-to="array" data-param-queries-cast-from="csv" data-param-function-id="{{router.params.id}}" data-scope="sdk">Variables diff --git a/tests/e2e/Services/Functions/FunctionsConsoleClientTest.php b/tests/e2e/Services/Functions/FunctionsConsoleClientTest.php index a6e0434f67..68b9ac46ea 100644 --- a/tests/e2e/Services/Functions/FunctionsConsoleClientTest.php +++ b/tests/e2e/Services/Functions/FunctionsConsoleClientTest.php @@ -182,74 +182,14 @@ class FunctionsConsoleClientTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals(1, sizeof($response['body']['variables'])); + $this->assertEquals(1, $response['body']['total']); $this->assertEquals("APP_TEST", $response['body']['variables'][0]['key']); $this->assertEquals("TESTINGVALUE", $response['body']['variables'][0]['value']); - $variableId = $response['body']['variables'][0]['$id']; - - $response = $this->client->call(Client::METHOD_GET, '/functions/' . $data['functionId'] . '/variables', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'queries' => [ 'limit(0)' ] - ]); - - $this->assertEquals(200, $response['headers']['status-code']); - $this->assertCount(0, $response['body']['variables']); - - $response = $this->client->call(Client::METHOD_GET, '/functions/' . $data['functionId'] . '/variables', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'queries' => [ 'offset(1)' ] - ]); - - $this->assertEquals(200, $response['headers']['status-code']); - $this->assertCount(0, $response['body']['variables']); - - $response = $this->client->call(Client::METHOD_GET, '/functions/' . $data['functionId'] . '/variables', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'queries' => [ 'equal("key", "APP_TEST")' ] - ]); - - $this->assertEquals(200, $response['headers']['status-code']); - $this->assertCount(1, $response['body']['variables']); - - $response = $this->client->call(Client::METHOD_GET, '/functions/' . $data['functionId'] . '/variables', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'search' => $variableId - ]); - - $this->assertEquals(200, $response['headers']['status-code']); - $this->assertCount(1, $response['body']['variables']); - - $response = $this->client->call(Client::METHOD_GET, '/functions/' . $data['functionId'] . '/variables', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'queries' => [ 'equal("key", "NON_EXISTING_VARIABLE")' ] - ]); - - $this->assertEquals(200, $response['headers']['status-code']); - $this->assertCount(0, $response['body']['variables']); - /** * Test for FAILURE */ - $response = $this->client->call(Client::METHOD_GET, '/functions/' . $data['functionId'] . '/variables', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'queries' => [ 'equal("value", "MY_SECRET")' ] - ]); - - $this->assertEquals(400, $response['headers']['status-code']); - return $data; } @@ -403,6 +343,7 @@ class FunctionsConsoleClientTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals(0, sizeof($response['body']['variables'])); + $this->assertEquals(0, $response['body']['total']); /** * Test for FAILURE From 6544ab48725bdda149670c7d0d71c206b6dd8132 Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Tue, 13 Sep 2022 18:08:06 +0000 Subject: [PATCH 5/6] feat: added breaking change to changelog --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index 7cc19426aa..f95cb93eae 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,7 @@ - Queries have been improved to allow even more flexibility, and introduced to new endpoints. See the Queries V2 section in the document for more information [#3702](https://github.com/appwrite/appwrite/pull/3702) - Compound indexes are now more flexible [#151](https://github.com/utopia-php/database/pull/151) - `createExecution` parameter `async` default value was changed from `true` to `false` [#3781](https://github.com/appwrite/appwrite/pull/3781) +- String attribute `status` has been refactored to a boolean attribute `enabled` in the functions collection [#3798](https://github.com/appwrite/appwrite/pull/3798) ## Features - Added the UI to see the Parent ID of all resources within the UI. [#3653](https://github.com/appwrite/appwrite/pull/3653) From 7d3101e077d81e6a52ff64c5f730a0f535f6d651 Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Tue, 13 Sep 2022 18:08:34 +0000 Subject: [PATCH 6/6] feat: added breaking change to changelog --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index f95cb93eae..f47d2c5bf7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,7 +10,7 @@ - Queries have been improved to allow even more flexibility, and introduced to new endpoints. See the Queries V2 section in the document for more information [#3702](https://github.com/appwrite/appwrite/pull/3702) - Compound indexes are now more flexible [#151](https://github.com/utopia-php/database/pull/151) - `createExecution` parameter `async` default value was changed from `true` to `false` [#3781](https://github.com/appwrite/appwrite/pull/3781) -- String attribute `status` has been refactored to a boolean attribute `enabled` in the functions collection [#3798](https://github.com/appwrite/appwrite/pull/3798) +- String attribute `status` has been refactored to a Boolean attribute `enabled` in the functions collection [#3798](https://github.com/appwrite/appwrite/pull/3798) ## Features - Added the UI to see the Parent ID of all resources within the UI. [#3653](https://github.com/appwrite/appwrite/pull/3653)