Skip to content
Draft
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
5 changes: 5 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@
['name' => 'ApiTables#update', 'url' => '/api/2/tables/{id}', 'verb' => 'PUT'],
['name' => 'ApiTables#destroy', 'url' => '/api/2/tables/{id}', 'verb' => 'DELETE'],
['name' => 'ApiTables#transfer', 'url' => '/api/2/tables/{id}/transfer', 'verb' => 'PUT'],
['name' => 'ApiTables#previewSchemeChanges', 'url' => '/api/2/tables/{id}/scheme/preview-changes', 'verb' => 'POST'],
['name' => 'ApiTables#importScheme', 'url' => '/api/2/tables/{id}/scheme/import', 'verb' => 'POST'],

['name' => 'ApiColumns#index', 'url' => '/api/2/columns/{nodeType}/{nodeId}', 'verb' => 'GET'],
['name' => 'ApiColumns#show', 'url' => '/api/2/columns/{id}', 'verb' => 'GET'],
Expand All @@ -153,6 +155,9 @@
['name' => 'Context#create', 'url' => '/api/2/contexts', 'verb' => 'POST'],
['name' => 'Context#update', 'url' => '/api/2/contexts/{contextId}', 'verb' => 'PUT'],
['name' => 'Context#destroy', 'url' => '/api/2/contexts/{contextId}', 'verb' => 'DELETE'],
['name' => 'Context#exportScheme', 'url' => '/api/2/contexts/{contextId}/scheme/export', 'verb' => 'GET'],
['name' => 'Context#previewSchemeChanges', 'url' => '/api/2/contexts/{contextId}/scheme/preview-changes', 'verb' => 'POST'],
['name' => 'Context#importScheme', 'url' => '/api/2/contexts/{contextId}/scheme/import', 'verb' => 'POST'],
['name' => 'Context#transfer', 'url' => '/api/2/contexts/{contextId}/transfer', 'verb' => 'PUT'],
['name' => 'Context#updateContentOrder', 'url' => '/api/2/contexts/{contextId}/pages/{pageId}', 'verb' => 'PUT'],

Expand Down
85 changes: 85 additions & 0 deletions lib/Controller/ApiTablesController.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use OCA\Tables\Model\ViewUpdateInput;
use OCA\Tables\ResponseDefinitions;
use OCA\Tables\Service\ColumnService;
use OCA\Tables\Service\StructureService;
use OCA\Tables\Service\TableService;
use OCA\Tables\Service\ViewService;
use OCA\Tables\Vendor\Symfony\Component\Uid\Uuid;
Expand All @@ -43,6 +44,7 @@ class ApiTablesController extends AOCSController {
private ViewService $viewService;
private IAppManager $appManager;
private IDBConnection $db;
private StructureService $structureService;

public function __construct(
IRequest $request,
Expand All @@ -53,13 +55,15 @@ public function __construct(
IL10N $n,
IAppManager $appManager,
IDBConnection $db,
StructureService $structureService,
string $userId) {
parent::__construct($request, $logger, $n, $userId);
$this->service = $service;
$this->columnService = $columnService;
$this->appManager = $appManager;
$this->viewService = $viewService;
$this->db = $db;
$this->structureService = $structureService;
}

/**
Expand Down Expand Up @@ -286,6 +290,87 @@ public function createFromScheme(string $title, string $emoji, string $descripti
}
}

/**
* [api v2] Preview changes to a table scheme
*
* @param int $id
* @param array $updateScheme
* @return DataResponse<Http::STATUS_OK, array{addedColumns: list<TablesColumn>, removedColumns: list<TablesColumn>, modifiedColumns: list<TablesColumn>}, array{}>|DataResponse<Http::STATUS_FORBIDDEN|Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND, array{message: string}, array{}>
*
* 200: Changes preview returned
* 403: No permissions
* 404: Not found
*/
#[NoAdminRequired]
#[RequirePermission(permission: Application::PERMISSION_MANAGE, type: Application::NODE_TYPE_TABLE, idParam: 'id')]
public function previewSchemeChanges(int $id, array $updateScheme): DataResponse {
try {
$changes = $this->service->compareTableSchemeChanges($id, $updateScheme);
return new DataResponse($changes);
} catch (NotFoundError $e) {
return $this->handleNotFoundError($e);
} catch (PermissionError $e) {
return $this->handlePermissionError($e);
} catch (InternalError|Exception|\Throwable $e) {
return $this->handleError($e);
}
}

/**
* [api v2] import table scheme into existing table
*
* @param int $id Table ID
* @param list<TablesColumn> $addColumns columns to add
* @param list<TablesColumn> $removeColumns columns to remove
* @param list<TablesColumn> $modifyColumns columns to modify
* @return DataResponse<Http::STATUS_OK, TablesTable, array{}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}>
*
* 200: Tables returned
* 400: Invalid request data
* 403: No permissions
*/
#[NoAdminRequired]
#[RequirePermission(permission: Application::PERMISSION_MANAGE, type: Application::NODE_TYPE_TABLE, idParam: 'id')]
public function importScheme(int $id, string $title, string $emoji, string $description, array $columns, array $views, array $columnOrder = [], array $sort = []): DataResponse {
try {
$this->db->beginTransaction();
$this->service->update($id, $title, $emoji, $description, null, $this->userId);
$table = $this->service->updateTableStructure($id, $columns, $views, $columnOrder, $sort, $this->userId);

$this->db->commit();
return new DataResponse($table->jsonSerialize());
} catch (PermissionError $e) {
try {
$this->db->rollBack();
} catch (\OCP\DB\Exception $re) {
return $this->handleError($re);
}
return $this->handlePermissionError($e);
} catch (\InvalidArgumentException $e) {
try {
$this->db->rollBack();
} catch (\OCP\DB\Exception $re) {
return $this->handleError($re);
}
$this->logger->warning('An invalid request occurred: ' . $e->getMessage(), ['exception' => $e]);
return new DataResponse(['message' => $e->getMessage()], Http::STATUS_BAD_REQUEST);
} catch (BadRequestError $e) {
try {
$this->db->rollBack();
} catch (\OCP\DB\Exception $re) {
return $this->handleError($re);
}
return $this->handleBadRequestError($e);
} catch (InternalError|Exception $e) {
try {
$this->db->rollBack();
} catch (\OCP\DB\Exception $e) {
return $this->handleError($e);
}
return $this->handleError($e);
}
}

/**
* [api v2] Create a new table and return it
*
Expand Down
111 changes: 111 additions & 0 deletions lib/Controller/ContextController.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,20 @@
use OCA\Tables\Errors\NotFoundError;
use OCA\Tables\Errors\PermissionError;
use OCA\Tables\Middleware\Attribute\RequirePermission;
use OCA\Tables\Model\ColumnSettings;
use OCA\Tables\Model\SortRuleSet;
use OCA\Tables\ResponseDefinitions;
use OCA\Tables\Service\ColumnService;
use OCA\Tables\Service\ContextService;
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\NoAdminRequired;
use OCP\AppFramework\Http\DataResponse;
use OCP\DB\Exception;
use OCP\IDBConnection;
use OCP\IL10N;
use OCP\IRequest;
use Psr\Log\LoggerInterface;
Expand All @@ -34,17 +40,29 @@

class ContextController extends AOCSController {
private ContextService $contextService;
private TableService $tableService;
private IDBConnection $db;
private ColumnService $columnService;
private ViewService $viewService;

public function __construct(
IRequest $request,
LoggerInterface $logger,
IL10N $n,
string $userId,
ContextService $contextService,
TableService $tableService,
IDBConnection $db,
ColumnService $columnService,
ViewService $viewService,
) {
parent::__construct($request, $logger, $n, $userId);
$this->contextService = $contextService;
$this->tableService = $tableService;
$this->userId = $userId;
$this->columnService = $columnService;
$this->viewService = $viewService;
$this->db = $db;
}

/**
Expand Down Expand Up @@ -282,6 +300,99 @@ public function updateContentOrder(int $contextId, int $pageId, array $content):
return new DataResponse($this->contextService->updateContentOrder($pageId, $content));
}

/**
* [api v2] Export the scheme of a context
*
* @param int $contextId ID of the context
*
* @return DataResponse<Http::STATUS_OK, array<string, mixed>, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND|Http::STATUS_FORBIDDEN, array{message: string}, array{}>
*
* @CanManageContext
*
* 200: returning the scheme of the context
* 403: No permissions
* 404: Not found
*/
#[NoAdminRequired]
#[RequirePermission(Application::PERMISSION_MANAGE, null, 'context', 'contextId')]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick, also can be done in a follow up, just for readability it think this is nicer:

Suggested change
#[RequirePermission(Application::PERMISSION_MANAGE, null, 'context', 'contextId')]
#[RequirePermission(permission: Application::PERMISSION_MANAGE, typeParam: 'context', idParam: 'contextId')]

applies to the other similar lines as well.

(yes it was used somewhere like this here before, that can also be improved)

Again, not a blocker, by-catch, good enough with follow-up.

public function exportScheme(int $contextId): DataResponse {
try {
$contextScheme = $this->contextService->getScheme($contextId);
return new DataResponse($contextScheme->jsonSerialize());
} catch (NotFoundError $e) {
return $this->handleNotFoundError($e);
} catch (Exception|\Throwable $e) {
return $this->handleError($e);
}
}

/**
* [api v2] Preview the changes that would be applied to a context scheme
*
* @param int $contextId ID of the context
*
* @return DataResponse<Http::STATUS_OK, array<string, mixed>, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND|Http::STATUS_FORBIDDEN, array{message: string}, array{}>
*
* @CanManageContext
*
* 200: returning the changes that would be applied to the context scheme
* 403: No permissions
* 404: Not found
*/
#[NoAdminRequired]
#[RequirePermission(Application::PERMISSION_MANAGE, null, 'context', 'contextId')]
public function previewSchemeChanges(int $contextId, array $updateScheme): DataResponse {
try {
$changes = $this->contextService->compareSchemeChanges($contextId, $updateScheme);
return new DataResponse($changes);
}
catch (NotFoundError $e) {
return $this->handleNotFoundError($e);
}
catch (PermissionError $e) {
$this->db->rollBack();
return $this->handlePermissionError($e);
} catch (Exception|InternalError|\Throwable $e) {
return $this->handleError($e);
}
}

/**
* [api v2] Import the scheme of a context
*
* @param int $contextId ID of the context
*
* @return DataResponse<Http::STATUS_OK, array<string, mixed>, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND|Http::STATUS_FORBIDDEN, array{message: string}, array{}>
*
* @CanManageContext
*
* 200: context updated successfully
* 403: No permissions
* 404: Not found
*/
#[NoAdminRequired]
#[RequirePermission(Application::PERMISSION_MANAGE, null, 'context', 'contextId')]
public function importScheme(int $contextId, string $name, string $iconName, string $description, array $nodes, array $tables): DataResponse {
try {
$this->db->beginTransaction();
$context = $this->contextService->importScheme($contextId, $name, $iconName, $description, $nodes, $tables, $this->userId);
$this->db->commit();
return new DataResponse($context->jsonSerialize());
} catch (\InvalidArgumentException $e) {
$this->db->rollBack();
return $this->handleBadRequestError(new BadRequestError($e->getMessage(), $e->getCode(), $e));
} catch (Exception|MultipleObjectsReturnedException $e) {
$this->db->rollBack();
return $this->handleError($e);
} catch (PermissionError $e) {
$this->db->rollBack();
return $this->handlePermissionError($e);
} catch (DoesNotExistException $e) {
$this->db->rollBack();
return $this->handleNotFoundError(new NotFoundError($e->getMessage(), $e->getCode(), $e));
}
}

protected function isValidIcon(string $iconName): bool {
if ($iconName === '' || !preg_match('/^[a-zA-Z0-9-]+$/', $iconName)) {
return false;
Expand Down
8 changes: 8 additions & 0 deletions lib/Db/ColumnMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -225,4 +225,12 @@ public function findAllByTableIds(array $tableIds): array {

return $this->findEntities($qb);
}

public function findByUuid(string $uuid): ?Column {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from($this->table)
->where($qb->expr()->eq('uuid', $qb->createNamedParameter($uuid)));
return $this->findEntity($qb);
}
}
30 changes: 30 additions & 0 deletions lib/Db/Table.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use OCA\Tables\Model\SortRuleSet;
use OCA\Tables\ResponseDefinitions;
use OCA\Tables\Service\ValueObject\ColumnOrderInformation;
use OCA\Tables\Vendor\Symfony\Component\Uid\Uuid;

/**
* @psalm-suppress PropertyNotSetInConstructor
Expand All @@ -21,6 +22,8 @@
*
* @method getTitle(): string
* @method getId(): int
* @method getUuid(): string
* @method setUuid(?string $uuid)
* @method setTitle(string $title)
* @method getEmoji(): string
* @method setEmoji(string $emoji)
Expand Down Expand Up @@ -62,6 +65,7 @@
* @method setLastEditAt(string $lastEditAt)
*/
class Table extends EntitySuper implements JsonSerializable {
protected ?string $uuid = null;
protected ?string $title = null;
protected ?string $emoji = null;
protected ?string $ownership = null;
Expand Down Expand Up @@ -90,15 +94,41 @@ class Table extends EntitySuper implements JsonSerializable {

public function __construct() {
$this->addType('id', 'integer');
$this->addType('uuid', 'string');
$this->addType('archived', 'boolean');
}

public function setter(string $name, array $args): void {
if ($name === 'uuid') {
$this->setOrAssignUuid($args[0]);
return;
}
parent::setter($name, $args);
}

private function setOrAssignUuid(?string $uuid): void {
if ($this->uuid !== null) {
throw new \RuntimeException('This table already has a UUID, they are immutable');
}
if ($uuid === null) {
$this->applyUuid(Uuid::v7()->toRfc4122());
return;
}
$this->applyUuid($uuid);
}

private function applyUuid(string $uuid): void {
$this->uuid = $uuid;
$this->markFieldUpdated('uuid');
}

/**
* @psalm-return TablesTable
*/
public function jsonSerialize(): array {
return [
'id' => $this->id,
'uuid' => $this->uuid,
'title' => $this->title ?: '',
'emoji' => $this->emoji,
'ownership' => $this->ownership ?: '',
Expand Down
15 changes: 15 additions & 0 deletions lib/Db/TableMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,21 @@ public function find(int $id): Table {
return $this->cache[$cacheKey];
}

/**
* @param string $uuid
* @return Table
* @throws DoesNotExistException
* @throws Exception
* @throws MultipleObjectsReturnedException
*/
public function findByUuid(string $uuid): Table {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from($this->table)
->where($qb->expr()->eq('uuid', $qb->createNamedParameter($uuid, IQueryBuilder::PARAM_STR)));
return $this->findEntity($qb);
}

/**
* @param int[] $ids
* @return array<int, Table> indexed by table id
Expand Down
Loading