Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 5 additions & 7 deletions lib/Activity/ActivityManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,17 +83,15 @@ public function __construct(

/**
* @param Row2|Table|View|Column $object
* @param array<string, mixed>|null|string $additionalParams
* @param array<string, mixed> $additionalParams
* @param string|null $author
*
* @psalm-param self::TABLES_OBJECT_* $objectType
* @psalm-param array<string, mixed>|null|string $additionalParams
* @psalm-param array<string, mixed> $additionalParams
* @psalm-param string|null $author
*/
public function triggerEvent(string $objectType, Row2|Table|View|Column $object, string $subject, array|string|null $additionalParams = [], array|string|null $author = null) {
if ($author === null) {
$author = $this->userId;
}
public function triggerEvent(string $objectType, Row2|Table|View|Column $object, string $subject, array $additionalParams = [], ?string $author = null) {
$author ??= $this->userId;

try {
$event = $this->createEvent($objectType, $object, $subject, $additionalParams, $author);
Expand Down Expand Up @@ -154,7 +152,7 @@ public function triggerUpdateEvents(string $objectType, ChangeSet $changeSet, st
* @psalm-param array<string, mixed> $additionalParams
* @psalm-param string|null $author
*/
private function createEvent(string $objectType, Row2|Table|View|Column $object, string $subject, array $additionalParams = [], array|string|null $author = null) {
private function createEvent(string $objectType, Row2|Table|View|Column $object, string $subject, array $additionalParams = [], ?string $author = null) {
if ($object instanceof Table) {
$objectTitle = $object->getTitle();
$table = $object;
Expand Down
125 changes: 122 additions & 3 deletions lib/Controller/RowOCSController.php
Original file line number Diff line number Diff line change
@@ -1,29 +1,39 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Tables\Controller;

use InvalidArgumentException;
use OCA\Tables\AppInfo\Application;
use OCA\Tables\Db\RowQuery;
use OCA\Tables\Errors\BadRequestError;
use OCA\Tables\Errors\InternalError;
use OCA\Tables\Errors\NotFoundError;
use OCA\Tables\Errors\PermissionError;
use OCA\Tables\Helper\ConversionHelper;
use OCA\Tables\Middleware\Attribute\RequirePermission;
use OCA\Tables\Model\FilterInput;
use OCA\Tables\Model\FilterSet;
use OCA\Tables\Model\RowDataInput;
use OCA\Tables\Model\SortRuleSet;
use OCA\Tables\ResponseDefinitions;
use OCA\Tables\Service\FederationService;
use OCA\Tables\Service\RowService;
use OCA\Tables\Service\TableService;
use OCA\Tables\Service\ViewService;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\ApiRoute;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\DataResponse;
use OCP\DB\Exception;
use OCP\IL10N;
use OCP\IRequest;
use Psr\Log\LoggerInterface;
Expand All @@ -49,10 +59,14 @@ public function __construct(
/**
* [api v2] Create a new row in a table or a view
*
* @param 'tables'|'views' $nodeCollection Indicates whether to create a row on a table or view
* @param 'tables'|'views' $nodeCollection Indicates whether to create a
* row on a table or view
* @param int $nodeId The identifier of the targeted table or view
* @param string|array<string, mixed> $data An array containing the column identifiers and their values
* @return DataResponse<Http::STATUS_OK, TablesRow, array{}>|DataResponse<Http::STATUS_FORBIDDEN|Http::STATUS_BAD_REQUEST|Http::STATUS_NOT_FOUND|Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}>
* @param string|array<string, mixed> $data An array containing the column
* identifiers and their values
* @return DataResponse<Http::STATUS_OK, TablesRow,
* array{}>|DataResponse<Http::STATUS_FORBIDDEN|Http::STATUS_BAD_REQUEST|Http::STATUS_NOT_FOUND|Http::STATUS_INTERNAL_SERVER_ERROR,
* array{message: string}, array{}>
*
* 200: Row returned
* 400: Invalid request parameters
Expand Down Expand Up @@ -105,6 +119,76 @@ public function createRow(string $nodeCollection, int $nodeId, mixed $data): Dat
}
}

/**
* [api v2] Get a number of rows from a table or view
*
* Both `filter` and `sort` are passed as JSON encoded strings.
*
* The filter is a list of filter groups, each group being a list of single
* filter definitions. Definitions within a group are AND-connected, while
* the groups themselves are OR-connected.
*
* When reading from a view, the provided filter is added to each of the
* view's existing filter groups, so the view's base rules are always
* enforced.
*
* A provided sort order overrides the view's default sort order. The view's
* default sort order is only used when no sort order is provided.
*
* @param 'tables'|'views' $nodeCollection Indicates whether to read from a table or a view
* @psalm-param int<0,max> $nodeId The ID of the table or view
* @psalm-param ?int<1,500> $limit Number of rows to return between 1 and 500, fetches all by default (optional)
* @psalm-param ?int<0,max> $offset Offset of the rows to be returned (optional)
* @param ?string $filter JSON encoded list of filter groups. Definitions within a group are AND-connected, groups are OR-connected, e.g. `[[{"columnId":1,"operator":"contains","value":"foo"}]]` (optional)
* @param ?string $sort JSON encoded list of sort rules, e.g. `[{"columnId":1,"mode":"ASC"}]` (optional)
* @return DataResponse<Http::STATUS_OK, list<TablesRow>, array{}>|DataResponse<Http::STATUS_FORBIDDEN|Http::STATUS_BAD_REQUEST|Http::STATUS_NOT_FOUND|Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}>
*
* 200: Rows returned
* 400: Invalid request parameters
* 403: No permissions
* 404: Not found
* 500: Internal error
*/
#[NoAdminRequired]
#[RequirePermission(permission: Application::PERMISSION_READ, typeParam: 'nodeCollection')]
#[ApiRoute(
verb: 'GET',
url: '/api/2/{nodeCollection}/{nodeId}/rows',
requirements: ['nodeCollection' => '(tables|views)', 'nodeId' => '(\\d+)']
)]
public function getRows(string $nodeCollection, int $nodeId, ?int $limit = null, ?int $offset = null, ?string $filter = null, ?string $sort = null): DataResponse {
try {
if (($limit !== null && ($limit <= 0 || $limit > 500))
|| ($offset !== null && $offset < 0)
) {
throw new InvalidArgumentException('Offset or limit parameter is out of bounds');
}

$queryData = new RowQuery(
nodeType: $nodeCollection === 'tables' ? Application::NODE_TYPE_TABLE : Application::NODE_TYPE_VIEW,
nodeId: $nodeId,
);
$queryData->setLimit($limit)
->setOffset($offset)
// the provided filter is set here; any filter defined on a view
// is merged in on the service level
->setFilter($this->parseFilter($filter))
Comment thread
enjeck marked this conversation as resolved.
->setSort($this->parseSort($sort))
Comment thread
enjeck marked this conversation as resolved.
->setUserId($this->userId);

$rows = $this->rowService->findAllByQuery($queryData);
return new DataResponse($this->rowService->formatRows($rows));
} catch (PermissionError $e) {
return $this->handlePermissionError($e);
} catch (DoesNotExistException $e) {
return $this->handleNotFoundError(new NotFoundError($e->getMessage(), $e->getCode(), $e));
} catch (MultipleObjectsReturnedException|InvalidArgumentException $e) {
return $this->handleBadRequestError(new BadRequestError($e->getMessage(), $e->getCode(), $e));
} catch (InternalError|Exception $e) {
return $this->handleError($e);
}
}

/**
* [api v2] Update a row in a table or a view
*
Expand Down Expand Up @@ -198,4 +282,39 @@ public function deleteRow(string $nodeCollection, int $nodeId, int $rowId): Data
return $this->handleError($e);
}
}

/**
* Decode and validate the JSON encoded filter parameter.
*
* @return list<list<array{columnId: int, operator: string, value: string|int|float}>>|null
* @throws InvalidArgumentException
*/
protected function parseFilter(?string $filter): ?array {
if ($filter === null || $filter === '') {
return null;
}
$filterInput = FilterInput::fromRequestValue($filter);
$decoded = $filterInput->filter;
if (!is_array($decoded)) {
throw new InvalidArgumentException('Invalid filter supplied');
}
return FilterSet::createFromInputArray($decoded)->jsonSerialize();
}

/**
* Decode and validate the JSON encoded sort parameter.
*
* @return list<array{columnId: int, mode: 'ASC'|'DESC'}>|null
* @throws InvalidArgumentException
*/
protected function parseSort(?string $sort): ?array {
if ($sort === null || $sort === '') {
return null;
}
$decoded = json_decode($sort, true);
if (!is_array($decoded)) {
throw new InvalidArgumentException('Invalid sort data supplied');
}
return SortRuleSet::createFromInputArray($decoded)->jsonSerialize();
}
}
8 changes: 2 additions & 6 deletions lib/Db/ContextMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,7 @@ protected function formatResultRows(array $rows, ?string $userId) {
'display_mode_default' => (int)$item['display_mode_default'],
];
if ($userId !== null) {
if ($item['display_mode'] === null) {
$item['display_mode'] = $item['display_mode_default'];
}
$item['display_mode'] ??= $item['display_mode_default'];
$carry[$item['share_id']]['display_mode'] = (int)$item['display_mode'];
}
return $carry;
Expand All @@ -130,9 +128,7 @@ protected function formatResultRows(array $rows, ?string $userId) {
// empty Context
return $carry;
}
if (!isset($carry[$item['page_id']])) {
$carry[$item['page_id']] = ['content' => []];
}
$carry[$item['page_id']] ??= ['content' => []];
$carry[$item['page_id']]['id'] = (int)$item['page_id'];
$carry[$item['page_id']]['page_type'] = $item['page_type'];
if ($item['node_rel_id'] !== null) {
Expand Down
6 changes: 2 additions & 4 deletions lib/Db/Row2Mapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ private function getWantedRowIds(string $userId, int $tableId, ?array $filter =
throw new InternalError(static::class . ' - ' . __FUNCTION__ . ': ' . $e->getMessage(), );
}

return array_map(fn (array $item) => $item['id'], $result->fetchAllAssociative());
return array_map(static fn (array $item) => $item['id'], $result->fetchAllAssociative());
}

/**
Expand Down Expand Up @@ -670,9 +670,7 @@ private function parseEntities(IResult $result, array $sleeves): array {

$column = $this->columnMapper->find($rowData['column_id']);
$columnType = $column->getType();
if (!isset($cellMapperCache[$columnType])) {
$cellMapperCache[$columnType] = $this->getCellMapperFromType($columnType);
}
$cellMapperCache[$columnType] ??= $this->getCellMapperFromType($columnType);
$value = $cellMapperCache[$columnType]->formatRowData($column, $rowData);
$compositeKey = (string)$rowData['row_id'] . ',' . (string)$rowData['column_id'];
if ($cellMapperCache[$columnType]->hasMultipleValues()) {
Expand Down
77 changes: 77 additions & 0 deletions lib/Db/RowQuery.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Tables\Db;

class RowQuery {
protected ?string $userId = null;
protected ?int $limit = null;
protected ?int $offset = null;
protected ?array $filter = null;
protected ?array $sort = null;

public function __construct(
protected int $nodeType,
protected int $nodeId,
) {
}

public function getNodeType(): int {
return $this->nodeType;
}

public function getNodeId(): int {
return $this->nodeId;
}

public function getUserId(): ?string {
return $this->userId;
}

public function setUserId(?string $userId): self {
$this->userId = $userId;
return $this;
}

public function getLimit(): ?int {
return $this->limit;
}

public function setLimit(?int $limit): self {
$this->limit = $limit;
return $this;
}

public function getOffset(): ?int {
return $this->offset;
}

public function setOffset(?int $offset): self {
$this->offset = $offset;
return $this;
}

public function getFilter(): ?array {
return $this->filter;
}

public function setFilter(?array $filter): self {
$this->filter = $filter;
return $this;
}

public function getSort(): ?array {
return $this->sort;
}

public function setSort(?array $sort): self {
$this->sort = $sort;
return $this;
}
}
4 changes: 1 addition & 3 deletions lib/Migration/Version2020Date20260513185340.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,7 @@ private function applyColumnOptionsUpdateIfNecessary(IQueryBuilder $query, int $
}

foreach ($selectionOptions as &$selectionOption) {
if (!isset($selectionOption['uuid'])) {
$selectionOption['uuid'] = Uuid::v7()->toRfc4122();
}
$selectionOption['uuid'] ??= Uuid::v7()->toRfc4122();
}

$updatedSelectionOptions = json_encode($selectionOptions);
Expand Down
4 changes: 1 addition & 3 deletions lib/Migration/Version2202Date20260825184226.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,7 @@ private function applyColumnOptionsUpdateIfNecessary(IQueryBuilder $query, int $
}

foreach ($selectionOptions as &$selectionOption) {
if (!isset($selectionOption['uuid'])) {
$selectionOption['uuid'] = Uuid::v7()->toRfc4122();
}
$selectionOption['uuid'] ??= Uuid::v7()->toRfc4122();

}
unset($selectionOption);
Expand Down
14 changes: 14 additions & 0 deletions lib/Model/FilterGroup.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public static function createFromInputArray(array $data): self {
if (!isset($filterInput['columnId'], $filterInput['operator'], $filterInput['value'])) {
throw new InvalidArgumentException('Required input fields are missing');
}
self::assertColumnIdInBounds($filterInput['columnId']);
try {
$filters[] = new Filter(
(int)$filterInput['columnId'],
Expand All @@ -49,6 +50,19 @@ public static function createFromInputArray(array $data): self {
return new self($filters);
}

/**
* @throws InvalidArgumentException
*/
private static function assertColumnIdInBounds(mixed $columnId): void {
$maxDigits = strlen((string)PHP_INT_MAX);
if (!is_numeric($columnId)
|| (int)$columnId < -5
|| !preg_match('/^-?\\d{0,' . $maxDigits . '}$/', (string)$columnId)
) {
throw new InvalidArgumentException(sprintf('Invalid column id supplied: %s', (string)$columnId));
}
}

public function jsonSerialize(): array {
return array_map(static fn (Filter $f) => $f->jsonSerialize(), $this->filters);
}
Expand Down
Loading
Loading