feat: Add backend for table archive and favorite flag

Signed-off-by: Julius Härtl <jus@bitgrid.net>
This commit is contained in:
Julius Härtl
2024-02-22 16:40:45 +01:00
parent 298e4850f1
commit 32e3100e83
8 changed files with 254 additions and 9 deletions

View File

@ -17,7 +17,7 @@ return [
// -> tables
['name' => 'api1#index', 'url' => '/api/1/tables', 'verb' => 'GET'],
['name' => 'api1#createTable', 'url' => '/api/1/tables', 'verb' => 'POST'],
['name' => 'api1#updateTable', 'url' => '/api/1/tables/{tableId}', 'verb' => 'PUT'],
['name' => 'api1#updateTable', 'url' => '/api/1/tables/{tableId}', 'verb' => 'PUT'], // needs archived
['name' => 'api1#getTable', 'url' => '/api/1/tables/{tableId}', 'verb' => 'GET'],
['name' => 'api1#deleteTable', 'url' => '/api/1/tables/{tableId}', 'verb' => 'DELETE'],
// -> views
@ -61,7 +61,7 @@ return [
['name' => 'table#index', 'url' => '/table', 'verb' => 'GET'],
['name' => 'table#show', 'url' => '/table/{id}', 'verb' => 'GET'],
['name' => 'table#create', 'url' => '/table', 'verb' => 'POST'],
['name' => 'table#update', 'url' => '/table/{id}', 'verb' => 'PUT'],
['name' => 'table#update', 'url' => '/table/{id}', 'verb' => 'PUT'], // needs archived
['name' => 'table#destroy', 'url' => '/table/{id}', 'verb' => 'DELETE'],
// view
@ -115,7 +115,7 @@ return [
['name' => 'ApiTables#index', 'url' => '/api/2/tables', 'verb' => 'GET'],
['name' => 'ApiTables#show', 'url' => '/api/2/tables/{id}', 'verb' => 'GET'],
['name' => 'ApiTables#create', 'url' => '/api/2/tables', 'verb' => 'POST'],
['name' => 'ApiTables#update', 'url' => '/api/2/tables/{id}', 'verb' => 'PUT'],
['name' => 'ApiTables#update', 'url' => '/api/2/tables/{id}', 'verb' => 'PUT'], // needs archived
['name' => 'ApiTables#destroy', 'url' => '/api/2/tables/{id}', 'verb' => 'DELETE'],
['name' => 'ApiTables#transfer', 'url' => '/api/2/tables/{id}/transfer', 'verb' => 'PUT'],
@ -125,5 +125,8 @@ return [
['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' => 'ApiFavorite#create', 'url' => '/api/2/favorites/{nodeType}/{nodeId}', 'verb' => 'POST'],
['name' => 'ApiFavorite#destroy', 'url' => '/api/2/favorites/{nodeType}/{nodeId}', 'verb' => 'DELETE'],
]
];

View File

@ -0,0 +1,80 @@
<?php
namespace OCA\Tables\Controller;
use Exception;
use OCA\Tables\Errors\InternalError;
use OCA\Tables\Errors\NotFoundError;
use OCA\Tables\Errors\PermissionError;
use OCA\Tables\ResponseDefinitions;
use OCA\Tables\Service\FavoritesService;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
use OCP\IL10N;
use OCP\IRequest;
use Psr\Log\LoggerInterface;
/**
* @psalm-import-type TablesTable from ResponseDefinitions
*/
class ApiFavoriteController extends AOCSController {
private FavoritesService $service;
public function __construct(
IRequest $request,
LoggerInterface $logger,
FavoritesService $service,
IL10N $n,
string $userId) {
parent::__construct($request, $logger, $n, $userId);
$this->service = $service;
}
/**
* [api v2] Create a new table and return it
*
* @NoAdminRequired
*
* @param string $title Title of the table
* @param string|null $emoji Emoji for the table
* @param string $template Template to use if wanted
*
* @return DataResponse<Http::STATUS_OK, TablesTable, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}>
*
* 200: Tables returned
*/
public function create(int $nodeType, int $nodeId): DataResponse {
try {
$this->service->addFavorite($nodeType, $nodeId);
return new DataResponse(['ok']);
} catch (InternalError|Exception $e) {
return $this->handleError($e);
}
}
/**
* [api v2] Delete a table
*
* @NoAdminRequired
*
* @param int $id Table ID
* @return DataResponse<Http::STATUS_OK, TablesTable, array{}>|DataResponse<Http::STATUS_FORBIDDEN|Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND, array{message: string}, array{}>
*
* 200: Deleted table returned
* 403: No permissions
* 404: Not found
*/
public function destroy(int $nodeType, int $nodeId): DataResponse {
try {
$this->service->removeFavorite($nodeType, $nodeId);
return new DataResponse(['ok']);
} catch (PermissionError $e) {
return $this->handlePermissionError($e);
} catch (InternalError $e) {
return $this->handleError($e);
} catch (NotFoundError $e) {
return $this->handleNotFoundError($e);
}
}
}

View File

@ -106,9 +106,9 @@ class ApiTablesController extends AOCSController {
* 403: No permissions
* 404: Not found
*/
public function update(int $id, string $title = null, string $emoji = null): DataResponse {
public function update(int $id, ?string $title = null, ?string $emoji = null, ?bool $archived = null): DataResponse {
try {
return new DataResponse($this->service->update($id, $title, $emoji, $this->userId)->jsonSerialize());
return new DataResponse($this->service->update($id, $title, $emoji, $archived, $this->userId)->jsonSerialize());
} catch (PermissionError $e) {
return $this->handlePermissionError($e);
} catch (InternalError $e) {

View File

@ -70,9 +70,9 @@ class TableController extends Controller {
/**
* @NoAdminRequired
*/
public function update(int $id, string $title = null, string $emoji = null): DataResponse {
return $this->handleError(function () use ($id, $title, $emoji) {
return $this->service->update($id, $title, $emoji, $this->userId);
public function update(int $id, string $title = null, string $emoji = null, ?bool $archived = null): DataResponse {
return $this->handleError(function () use ($id, $title, $emoji, $archived) {
return $this->service->update($id, $title, $emoji, $archived, $this->userId);
});
}
}

View File

@ -16,6 +16,8 @@ use OCP\AppFramework\Db\Entity;
* @method setTitle(string $title)
* @method getEmoji(): string
* @method setEmoji(string $emoji)
* @method getArchived(): bool
* @method setArchived(bool $archived)
* @method getOwnership(): string
* @method setOwnership(string $ownership)
* @method getOwnerDisplayName(): string
@ -26,6 +28,8 @@ use OCP\AppFramework\Db\Entity;
* @method setOnSharePermissions(array $onSharePermissions)
* @method getHasShares(): bool
* @method setHasShares(bool $hasShares)
* @method getFavorite(): bool
* @method setFavorite(bool $favorite)
* @method getRowsCount(): int
* @method setRowsCount(int $rowsCount)
* @method getColumnsCount(): int
@ -52,10 +56,12 @@ class Table extends Entity implements JsonSerializable {
protected ?string $createdAt = null;
protected ?string $lastEditBy = null;
protected ?string $lastEditAt = null;
protected bool $archived = false;
protected ?bool $isShared = null;
protected ?array $onSharePermissions = null;
protected ?bool $hasShares = false;
protected ?bool $favorite = false;
protected ?int $rowsCount = 0;
protected ?int $columnsCount = 0;
protected ?array $views = null;
@ -63,6 +69,7 @@ class Table extends Entity implements JsonSerializable {
public function __construct() {
$this->addType('id', 'integer');
$this->addType('archived', 'boolean');
}
/**
@ -79,7 +86,9 @@ class Table extends Entity implements JsonSerializable {
'createdAt' => $this->createdAt ?: '',
'lastEditBy' => $this->lastEditBy ?: '',
'lastEditAt' => $this->lastEditAt ?: '',
'archived' => $this->archived,
'isShared' => !!$this->isShared,
'favorite' => $this->favorite,
'onSharePermissions' => $this->getSharePermissions(),
'hasShares' => !!$this->hasShares,
'rowsCount' => $this->rowsCount ?: 0,

View File

@ -0,0 +1,59 @@
<?php
/** @noinspection PhpUnused */
declare(strict_types=1);
namespace OCA\Tables\Migration;
use Closure;
use OCP\DB\Exception;
use OCP\DB\ISchemaWrapper;
use OCP\DB\Types;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
class Version000800Date20240222000000 extends SimpleMigrationStep {
/**
* @param IOutput $output
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
* @return null|ISchemaWrapper
* @throws Exception
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
if ($schema->hasTable('tables_tables')) {
$table = $schema->getTable('tables_tables');
$table->addColumn('archived', Types::BOOLEAN, [
'default' => false,
'notnull' => true,
]);
}
if (!$schema->hasTable('tables_favorites')) {
$table = $schema->createTable('tables_favorites');
$table->addColumn('id', Types::BIGINT, [
'notnull' => true,
'autoincrement' => true,
'unsigned' => true,
]);
$table->addColumn('node_type', Types::SMALLINT, [
'notnull' => true,
]);
$table->addColumn('node_id', Types::BIGINT, [
'notnull' => true,
]);
$table->addColumn('user_id', Types::STRING, [
'notnull' => true,
'length' => 64,
]);
$table->setPrimaryKey(['id']);
}
return $schema;
}
}

View File

@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2024 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace OCA\Tables\Service;
use OCP\Cache\CappedMemoryCache;
use OCP\IDBConnection;
class FavoritesService {
private CappedMemoryCache $cache;
public function __construct(private IDBConnection $connection, private ?string $userId) {
$this->cache = new CappedMemoryCache();
}
public function isFavorite(int $nodeType, int $id): bool {
if ($cached = $this->cache->get($this->userId . '_' . $nodeType . '_' . $id)) {
return $cached;
}
// We still might run this multiple times
$qb = $this->connection->getQueryBuilder();
$qb->select('*')
->from('tables_favorites')
->where($qb->expr()->eq('user_id', $qb->createNamedParameter($this->userId)));
$result = $qb->executeQuery();
while ($row = $result->fetch()) {
$this->cache->set($this->userId . '_' . $row['node_type'] . '_' . $row['node_id'], true);
}
return $this->cache->get($this->userId . '_' . $nodeType . '_' . $id) ?? false;
}
public function addFavorite(int $nodeType, int $id): void {
$qb = $this->connection->getQueryBuilder();
$qb->insert('tables_favorites')
->values([
'user_id' => $qb->createNamedParameter($this->userId),
'node_type' => $qb->createNamedParameter($nodeType),
'node_id' => $qb->createNamedParameter($id),
]);
$qb->executeStatement();
$this->cache->set($this->userId . '_' . $nodeType . '_' . $id, true);
}
public function removeFavorite(int $nodeType, int $id): void {
$qb = $this->connection->getQueryBuilder();
$qb->delete('tables_favorites')
->where($qb->expr()->eq('user_id', $qb->createNamedParameter($this->userId)))
->andWhere($qb->expr()->eq('node_type', $qb->createNamedParameter($nodeType)))
->andWhere($qb->expr()->eq('node_id', $qb->createNamedParameter($id)));
$qb->executeStatement();
$this->cache->set($this->userId . '_' . $nodeType . '_' . $id, false);
}
}

View File

@ -6,6 +6,7 @@ namespace OCA\Tables\Service;
use DateTime;
use OCA\Tables\AppInfo\Application;
use OCA\Tables\Db\Table;
use OCA\Tables\Db\TableMapper;
use OCA\Tables\Errors\InternalError;
@ -38,6 +39,9 @@ class TableService extends SuperService {
protected UserHelper $userHelper;
protected FavoritesService $favoritesService;
protected IL10N $l;
public function __construct(
@ -51,6 +55,7 @@ class TableService extends SuperService {
ViewService $viewService,
ShareService $shareService,
UserHelper $userHelper,
FavoritesService $favoritesService,
IL10N $l
) {
parent::__construct($logger, $userId, $permissionsService);
@ -61,6 +66,7 @@ class TableService extends SuperService {
$this->viewService = $viewService;
$this->shareService = $shareService;
$this->userHelper = $userHelper;
$this->favoritesService = $favoritesService;
$this->l = $l;
}
@ -197,6 +203,11 @@ class TableService extends SuperService {
$table->setViews($this->viewService->findAll($table));
}
if ($this->favoritesService->isFavorite(Application::NODE_TYPE_TABLE, $table->getId())) {
$table->setFavorite(true);
}
}
@ -420,7 +431,7 @@ class TableService extends SuperService {
* @throws NotFoundError
* @throws PermissionError
*/
public function update(int $id, ?string $title, ?string $emoji, ?string $userId = null): Table {
public function update(int $id, ?string $title, ?string $emoji, ?bool $archived = null, ?string $userId = null): Table {
$userId = $this->permissionsService->preCheckUserId($userId);
try {
@ -445,6 +456,9 @@ class TableService extends SuperService {
if ($emoji !== null) {
$table->setEmoji($emoji);
}
if ($archived !== null) {
$table->setArchived($archived);
}
$table->setLastEditBy($userId);
$table->setLastEditAt($time->format('Y-m-d H:i:s'));
try {