From d3f5598eb751636279f2a3fafab60e41198419a1 Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Wed, 21 Feb 2024 13:03:26 +0100 Subject: [PATCH] feat(Contexts): API endpoint for getting Contexts Signed-off-by: Arthur Schiwon --- appinfo/info.xml | 1 + appinfo/routes.php | 2 + lib/Command/ListContexts.php | 87 +++++++ lib/Controller/ContextsController.php | 65 +++++ lib/Db/Context.php | 41 +++ lib/Db/ContextMapper.php | 63 +++++ lib/ResponseDefinitions.php | 9 + lib/Service/ContextService.php | 43 ++++ openapi.json | 349 +++++++++++++++++++++----- 9 files changed, 594 insertions(+), 66 deletions(-) create mode 100644 lib/Command/ListContexts.php create mode 100644 lib/Controller/ContextsController.php create mode 100644 lib/Db/Context.php create mode 100644 lib/Db/ContextMapper.php create mode 100644 lib/Service/ContextService.php diff --git a/appinfo/info.xml b/appinfo/info.xml index e3070c31d..b80cd8257 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -57,6 +57,7 @@ Have a good time and manage whatever you want. OCA\Tables\Command\RemoveTable OCA\Tables\Command\RenameTable OCA\Tables\Command\ChangeOwnershipTable + OCA\Tables\Command\ListContexts OCA\Tables\Command\Clean OCA\Tables\Command\CleanLegacy OCA\Tables\Command\TransferLegacyRows diff --git a/appinfo/routes.php b/appinfo/routes.php index 5df857fd7..75a185db0 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -123,5 +123,7 @@ ['name' => 'ApiColumns#createTextColumn', 'url' => '/api/2/columns/text', 'verb' => 'POST'], ['name' => 'ApiColumns#createSelectionColumn', 'url' => '/api/2/columns/selection', 'verb' => 'POST'], ['name' => 'ApiColumns#createDatetimeColumn', 'url' => '/api/2/columns/datetime', 'verb' => 'POST'], + + ['name' => 'Contexts#index', 'url' => '/api/3/contexts', 'verb' => 'GET'], ] ]; diff --git a/lib/Command/ListContexts.php b/lib/Command/ListContexts.php new file mode 100644 index 000000000..fdc427981 --- /dev/null +++ b/lib/Command/ListContexts.php @@ -0,0 +1,87 @@ +contextService = $contextService; + $this->logger = $logger; + $this->config = $config; + } + + protected function configure(): void { + parent::configure(); + $this + ->setName('tables:contexts:list') + ->setDescription('Get all contexts or contexts available to a specified user') + ->addArgument( + 'user-id', + InputArgument::OPTIONAL, + 'User ID of the user' + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $userId = trim($input->getArgument('user-id')); + if ($userId === '') { + $userId = null; + } + + try { + $contexts = $this->contextService->findAll($userId); + } catch (InternalError|Exception $e) { + $output->writeln('Error while reading contexts from DB.'); + $this->logger->warning('Following error occurred during executing occ command "{class}"', + [ + 'app' => 'tables', + 'class' => self::class, + 'exception' => $e, + ] + ); + if ($this->config->getSystemValueBool('debug', false)) { + $output->writeln(sprintf('%s', $e->getMessage())); + $output->writeln(''); + debug_print_backtrace(); + $output->writeln(''); + } + return 1; + } + + foreach ($contexts as $context) { + $contextArray = json_decode(json_encode($context), true); + + $contextArray['ownerType'] = match ($contextArray['ownerType']) { + 1 => 'group', + default => 'user', + }; + + $out = ['ID ' . $contextArray['id'] => $contextArray]; + unset($out[$contextArray['id']]['id']); + $this->writeArrayInOutputFormat($input, $output, $out); + } + + return 0; + } +} diff --git a/lib/Controller/ContextsController.php b/lib/Controller/ContextsController.php new file mode 100644 index 000000000..d1297c226 --- /dev/null +++ b/lib/Controller/ContextsController.php @@ -0,0 +1,65 @@ +contextService = $contextService; + $this->userId = $userId; + } + + /** + * [api v3] Get all contexts available to the requesting person + * + * Return an empty array if no contexts were found + * + * @return DataResponse|DataResponse + * + * 200: reporting in available contexts + * + * @NoAdminRequired + */ + public function index(): DataResponse { + try { + $contexts = $this->contextService->findAll($this->userId); + return new DataResponse($this->contextsToArray($contexts)); + } catch (InternalError|Exception $e) { + return $this->handleError($e); + } + } + + /** + * @param Context[] $contexts + * @return array + */ + protected function contextsToArray(array $contexts): array { + $result = []; + foreach ($contexts as $context) { + $result[] = $context->jsonSerialize(); + } + return $result; + } +} diff --git a/lib/Db/Context.php b/lib/Db/Context.php new file mode 100644 index 000000000..ef23e9284 --- /dev/null +++ b/lib/Db/Context.php @@ -0,0 +1,41 @@ +addType('id', 'integer'); + } + + public function jsonSerialize(): array { + return [ + 'id' => $this->getId(), + 'name' => $this->getName(), + 'iconName' => $this->getIcon(), + 'description' => $this->getDescription(), + 'owner' => $this->getOwnerId(), + 'ownerType' => $this->getOwnerType() + ]; + } +} diff --git a/lib/Db/ContextMapper.php b/lib/Db/ContextMapper.php new file mode 100644 index 000000000..aa0b24d68 --- /dev/null +++ b/lib/Db/ContextMapper.php @@ -0,0 +1,63 @@ +userHelper = $userHelper; + parent::__construct($db, $this->table, Context::class); + } + + /** + * @return Context[] + * @throws Exception + */ + public function findAll(?string $userId = null): array { + $qb = $this->db->getQueryBuilder(); + $qb->select('c.*') + ->from($this->table, 'c'); + if ($userId !== null) { + $sharedToConditions = $qb->expr()->orX(); + + // shared to user clause + $userShare = $qb->expr()->andX( + $qb->expr()->eq('s.receiver_type', $qb->createNamedParameter('user')), + $qb->expr()->eq('s.receiver', $qb->createNamedParameter($userId)), + ); + $sharedToConditions->add($userShare); + + // shared to group clause + $groupIDs = $this->userHelper->getGroupIdsForUser($userId); + if (!empty($groupIDs)) { + $groupShares = $qb->expr()->andX( + $qb->expr()->eq('s.receiver_type', $qb->createNamedParameter('group')), + $qb->expr()->in('s.receiver', $qb->createNamedParameter($groupIDs, IQueryBuilder::PARAM_STR_ARRAY)), + ); + $sharedToConditions->add($groupShares); + } + + // owned contexts + apply share conditions + $qb->leftJoin('c', 'tables_shares', 's', $qb->expr()->andX( + $qb->expr()->eq('c.id', 's.node_id'), + $qb->expr()->eq('s.node_type', $qb->createNamedParameter('context')), + $sharedToConditions, + )); + + $qb->where($qb->expr()->orX( + $qb->expr()->eq('owner_id', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)), + $qb->expr()->isNotNull('s.receiver'), + )); + } + + return $this->findEntities($qb); + } +} diff --git a/lib/ResponseDefinitions.php b/lib/ResponseDefinitions.php index 42668bce5..d3dee211f 100644 --- a/lib/ResponseDefinitions.php +++ b/lib/ResponseDefinitions.php @@ -125,6 +125,15 @@ * errors_parsing_count: int, * errors_count: int, * } + * + * @psalm-type TablesContext = array{ + * id: int, + * name: string, + * iconName: string, + * description: string, + * owner: string, + * ownerType: int, + * } */ class ResponseDefinitions { } diff --git a/lib/Service/ContextService.php b/lib/Service/ContextService.php new file mode 100644 index 000000000..f1190fe2d --- /dev/null +++ b/lib/Service/ContextService.php @@ -0,0 +1,43 @@ +mapper = $mapper; + $this->isCLI = $isCLI; + $this->logger = $logger; + } + + /** + * @throws InternalError + * @throws Exception + * @return Context[] + */ + public function findAll(?string $userId): array { + if ($userId !== null && trim($userId) === '') { + $userId = null; + } + if ($userId === null && !$this->isCLI) { + $error = 'Try to set no user in context, but request is not allowed.'; + $this->logger->warning($error); + throw new InternalError($error); + } + return $this->mapper->findAll($userId); + } +} diff --git a/openapi.json b/openapi.json index cf542b1fb..82b1e5665 100644 --- a/openapi.json +++ b/openapi.json @@ -168,6 +168,39 @@ } } }, + "Context": { + "type": "object", + "required": [ + "id", + "name", + "iconName", + "description", + "owner", + "ownerType" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "iconName": { + "type": "string" + }, + "description": { + "type": "string" + }, + "owner": { + "type": "string" + }, + "ownerType": { + "type": "integer", + "format": "int64" + } + } + }, "ImportState": { "type": "object", "required": [ @@ -534,9 +567,8 @@ } }, "sort": { - "type": "object", - "nullable": true, - "additionalProperties": { + "type": "array", + "items": { "type": "object", "required": [ "columnId", @@ -558,48 +590,50 @@ } }, "filter": { - "type": "object", - "nullable": true, - "additionalProperties": { - "type": "object", - "required": [ - "columnId", - "operator", - "value" - ], - "properties": { - "columnId": { - "type": "integer", - "format": "int64" - }, - "operator": { - "type": "string", - "enum": [ - "begins-with", - "ends-with", - "contains", - "is-equal", - "is-greater-than", - "is-greater-than-or-equal", - "is-lower-than", - "is-lower-than-or-equal", - "is-empty" - ] - }, - "value": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "integer", - "format": "int64" - }, - { - "type": "number", - "format": "float" - } - ] + "type": "array", + "items": { + "type": "array", + "items": { + "type": "object", + "required": [ + "columnId", + "operator", + "value" + ], + "properties": { + "columnId": { + "type": "integer", + "format": "int64" + }, + "operator": { + "type": "string", + "enum": [ + "begins-with", + "ends-with", + "contains", + "is-equal", + "is-greater-than", + "is-greater-than-or-equal", + "is-lower-than", + "is-lower-than-or-equal", + "is-empty" + ] + }, + "value": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "integer", + "format": "int64" + }, + { + "type": "number", + "format": "float" + } + ] + } } } } @@ -1865,7 +1899,11 @@ "description": "New permission value", "required": true, "schema": { - "type": "integer" + "type": "integer", + "enum": [ + 0, + 1 + ] } }, { @@ -2104,7 +2142,11 @@ "description": "Permission if receiver can read data", "required": true, "schema": { - "type": "integer" + "type": "integer", + "enum": [ + 0, + 1 + ] } }, { @@ -2113,7 +2155,11 @@ "description": "Permission if receiver can create data", "required": true, "schema": { - "type": "integer" + "type": "integer", + "enum": [ + 0, + 1 + ] } }, { @@ -2122,7 +2168,11 @@ "description": "Permission if receiver can update data", "required": true, "schema": { - "type": "integer" + "type": "integer", + "enum": [ + 0, + 1 + ] } }, { @@ -2131,7 +2181,11 @@ "description": "Permission if receiver can delete data", "required": true, "schema": { - "type": "integer" + "type": "integer", + "enum": [ + 0, + 1 + ] } }, { @@ -2140,7 +2194,11 @@ "description": "Permission if receiver can manage table", "required": true, "schema": { - "type": "integer" + "type": "integer", + "enum": [ + 0, + 1 + ] } }, { @@ -2286,7 +2344,11 @@ "description": "Permission if receiver can read data", "schema": { "type": "integer", - "default": 0 + "default": 0, + "enum": [ + 0, + 1 + ] } }, { @@ -2295,7 +2357,11 @@ "description": "Permission if receiver can create data", "schema": { "type": "integer", - "default": 0 + "default": 0, + "enum": [ + 0, + 1 + ] } }, { @@ -2304,7 +2370,11 @@ "description": "Permission if receiver can update data", "schema": { "type": "integer", - "default": 0 + "default": 0, + "enum": [ + 0, + 1 + ] } }, { @@ -2313,7 +2383,11 @@ "description": "Permission if receiver can delete data", "schema": { "type": "integer", - "default": 0 + "default": 0, + "enum": [ + 0, + 1 + ] } }, { @@ -2322,7 +2396,11 @@ "description": "Permission if receiver can manage node", "schema": { "type": "integer", - "default": 0 + "default": 0, + "enum": [ + 0, + 1 + ] } } ], @@ -2549,7 +2627,11 @@ "description": "Is the column mandatory", "required": true, "schema": { - "type": "integer" + "type": "integer", + "enum": [ + 0, + 1 + ] } }, { @@ -2937,7 +3019,11 @@ "description": "Is the column mandatory", "required": true, "schema": { - "type": "integer" + "type": "integer", + "enum": [ + 0, + 1 + ] } }, { @@ -3185,7 +3271,11 @@ "description": "Is the column mandatory", "required": true, "schema": { - "type": "integer" + "type": "integer", + "enum": [ + 0, + 1 + ] } }, { @@ -4521,7 +4611,11 @@ "description": "Create missing columns", "schema": { "type": "integer", - "default": 1 + "default": 1, + "enum": [ + 0, + 1 + ] } }, { @@ -4631,7 +4725,11 @@ "description": "Create missing columns", "schema": { "type": "integer", - "default": 1 + "default": 1, + "enum": [ + 0, + 1 + ] } }, { @@ -6326,7 +6424,11 @@ "description": "Is mandatory", "schema": { "type": "integer", - "default": 0 + "default": 0, + "enum": [ + 0, + 1 + ] } }, { @@ -6612,7 +6714,11 @@ "description": "Is mandatory", "schema": { "type": "integer", - "default": 0 + "default": 0, + "enum": [ + 0, + 1 + ] } }, { @@ -6888,7 +6994,11 @@ "description": "Is mandatory", "schema": { "type": "integer", - "default": 0 + "default": 0, + "enum": [ + 0, + 1 + ] } }, { @@ -7159,7 +7269,11 @@ "description": "Is mandatory", "schema": { "type": "integer", - "default": 0 + "default": 0, + "enum": [ + 0, + 1 + ] } }, { @@ -7338,7 +7452,110 @@ } } } + }, + "/ocs/v2.php/apps/tables/api/3/contexts": { + "get": { + "operationId": "contexts-list", + "summary": "[api v3] Get all contexts available to the requesting person", + "description": "Return an empty array if no contexts were found", + "tags": [ + "contexts" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "reporting in available contexts", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Context" + } + } + } + } + } + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + } + } + } } }, "tags": [] -} +} \ No newline at end of file