Skip to content

Commit c3f6a2d

Browse files
committed
feat: add relation column type
Signed-off-by: Kostiantyn Miakshyn <molodchick@gmail.com>
1 parent f792683 commit c3f6a2d

29 files changed

+1098
-14
lines changed

appinfo/routes.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@
5050
['name' => 'api1#updateColumn', 'url' => '/api/1/columns/{columnId}', 'verb' => 'PUT'],
5151
['name' => 'api1#getColumn', 'url' => '/api/1/columns/{columnId}', 'verb' => 'GET'],
5252
['name' => 'api1#deleteColumn', 'url' => '/api/1/columns/{columnId}', 'verb' => 'DELETE'],
53+
// -> relations
54+
['name' => 'api1#indexTableRelations', 'url' => '/api/1/tables/{tableId}/relations', 'verb' => 'GET'],
55+
['name' => 'api1#indexViewRelations', 'url' => '/api/1/views/{viewId}/relations', 'verb' => 'GET'],
5356
// -> rows
5457
['name' => 'api1#indexTableRowsSimple', 'url' => '/api/1/tables/{tableId}/rows/simple', 'verb' => 'GET'],
5558
['name' => 'api1#indexTableRows', 'url' => '/api/1/tables/{tableId}/rows', 'verb' => 'GET'],

lib/Controller/Api1Controller.php

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use OCA\Tables\ResponseDefinitions;
2424
use OCA\Tables\Service\ColumnService;
2525
use OCA\Tables\Service\ImportService;
26+
use OCA\Tables\Service\RelationService;
2627
use OCA\Tables\Service\RowService;
2728
use OCA\Tables\Service\ShareService;
2829
use OCA\Tables\Service\TableService;
@@ -57,6 +58,7 @@ class Api1Controller extends ApiController {
5758
private RowService $rowService;
5859
private ImportService $importService;
5960
private ViewService $viewService;
61+
private RelationService $relationService;
6062
private ViewMapper $viewMapper;
6163
private IL10N $l10N;
6264

@@ -77,6 +79,7 @@ public function __construct(
7779
RowService $rowService,
7880
ImportService $importService,
7981
ViewService $viewService,
82+
RelationService $relationService,
8083
ViewMapper $viewMapper,
8184
V1Api $v1Api,
8285
LoggerInterface $logger,
@@ -90,6 +93,7 @@ public function __construct(
9093
$this->rowService = $rowService;
9194
$this->importService = $importService;
9295
$this->viewService = $viewService;
96+
$this->relationService = $relationService;
9397
$this->viewMapper = $viewMapper;
9498
$this->userId = $userId;
9599
$this->v1Api = $v1Api;
@@ -803,13 +807,77 @@ public function indexViewColumns(int $viewId): DataResponse {
803807
}
804808
}
805809

810+
/**
811+
* Get all relation data for a table
812+
*
813+
* @param int $tableId Table ID
814+
* @return DataResponse<Http::STATUS_OK, array<string, array<string, array{id: int, label: string}>>, array{}>|DataResponse<Http::STATUS_FORBIDDEN|Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND, array{message: string}, array{}>
815+
*
816+
* 200: Relation data returned
817+
* 403: No permissions
818+
* 404: Not found
819+
*/
820+
#[NoAdminRequired]
821+
#[NoCSRFRequired]
822+
#[CORS]
823+
#[RequirePermission(permission: Application::PERMISSION_READ, type: Application::NODE_TYPE_TABLE, idParam: 'tableId')]
824+
public function indexTableRelations(int $tableId): DataResponse {
825+
try {
826+
return new DataResponse($this->relationService->getRelationsForTable($tableId));
827+
} catch (PermissionError $e) {
828+
$this->logger->warning('A permission error occurred: ' . $e->getMessage(), ['exception' => $e]);
829+
$message = ['message' => $e->getMessage()];
830+
return new DataResponse($message, Http::STATUS_FORBIDDEN);
831+
} catch (InternalError $e) {
832+
$this->logger->error('An internal error or exception occurred: ' . $e->getMessage(), ['exception' => $e]);
833+
$message = ['message' => $e->getMessage()];
834+
return new DataResponse($message, Http::STATUS_INTERNAL_SERVER_ERROR);
835+
} catch (NotFoundError $e) {
836+
$this->logger->info('A not found error occurred: ' . $e->getMessage(), ['exception' => $e]);
837+
$message = ['message' => $e->getMessage()];
838+
return new DataResponse($message, Http::STATUS_NOT_FOUND);
839+
}
840+
}
841+
842+
/**
843+
* Get all relation data for a view
844+
*
845+
* @param int $viewId View ID
846+
* @return DataResponse<Http::STATUS_OK, array<string, array<string, array{id: int, label: string}>>, array{}>|DataResponse<Http::STATUS_FORBIDDEN|Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND, array{message: string}, array{}>
847+
*
848+
* 200: Relation data returned
849+
* 403: No permissions
850+
* 404: Not found
851+
*/
852+
#[NoAdminRequired]
853+
#[NoCSRFRequired]
854+
#[CORS]
855+
#[RequirePermission(permission: Application::PERMISSION_READ, type: Application::NODE_TYPE_VIEW, idParam: 'viewId')]
856+
public function indexViewRelations(int $viewId): DataResponse {
857+
try {
858+
return new DataResponse($this->relationService->getRelationsForView($viewId));
859+
} catch (PermissionError $e) {
860+
$this->logger->warning('A permission error occurred: ' . $e->getMessage(), ['exception' => $e]);
861+
$message = ['message' => $e->getMessage()];
862+
return new DataResponse($message, Http::STATUS_FORBIDDEN);
863+
} catch (InternalError $e) {
864+
$this->logger->error('An internal error or exception occurred: ' . $e->getMessage(), ['exception' => $e]);
865+
$message = ['message' => $e->getMessage()];
866+
return new DataResponse($message, Http::STATUS_INTERNAL_SERVER_ERROR);
867+
} catch (NotFoundError $e) {
868+
$this->logger->info('A not found error occurred: ' . $e->getMessage(), ['exception' => $e]);
869+
$message = ['message' => $e->getMessage()];
870+
return new DataResponse($message, Http::STATUS_NOT_FOUND);
871+
}
872+
}
873+
806874
/**
807875
* Create a column
808876
*
809877
* @param int|null $tableId Table ID
810878
* @param int|null $viewId View ID
811879
* @param string $title Title
812-
* @param 'text'|'number'|'datetime'|'select'|'usergroup' $type Column main type
880+
* @param 'text'|'number'|'datetime'|'select'|'usergroup'|'relation' $type Column main type
813881
* @param string|null $subtype Column sub type
814882
* @param bool $mandatory Is the column mandatory
815883
* @param string|null $description Description
@@ -1311,7 +1379,7 @@ public function createRowInTable(int $tableId, $data): DataResponse {
13111379
#[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
13121380
public function getRow(int $rowId): DataResponse {
13131381
try {
1314-
return new DataResponse($this->rowService->find($rowId)->jsonSerialize());
1382+
return new DataResponse($this->rowService->find($rowId, $this->userId)->jsonSerialize());
13151383
} catch (PermissionError $e) {
13161384
$this->logger->warning('A permission error occurred: ' . $e->getMessage(), ['exception' => $e]);
13171385
$message = ['message' => $e->getMessage()];
@@ -1572,7 +1640,7 @@ public function createTableShare(int $tableId, string $receiver, string $receive
15721640
*
15731641
* @param int $tableId Table ID
15741642
* @param string $title Title
1575-
* @param 'text'|'number'|'datetime'|'select'|'usergroup' $type Column main type
1643+
* @param 'text'|'number'|'datetime'|'select'|'usergroup'|'relation' $type Column main type
15761644
* @param string|null $subtype Column sub type
15771645
* @param bool $mandatory Is the column mandatory
15781646
* @param string|null $description Description

lib/Controller/RowController.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public function indexView(int $viewId): DataResponse {
4747
#[NoAdminRequired]
4848
public function show(int $id): DataResponse {
4949
return $this->handleError(function () use ($id) {
50-
return $this->service->find($id);
50+
return $this->service->find($id, $this->userId);
5151
});
5252
}
5353

lib/Db/Column.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ class Column extends EntitySuper implements JsonSerializable {
100100
public const TYPE_NUMBER = 'number';
101101
public const TYPE_DATETIME = 'datetime';
102102
public const TYPE_USERGROUP = 'usergroup';
103+
public const TYPE_RELATION = 'relation';
103104

104105
public const SUBTYPE_DATETIME_DATE = 'date';
105106
public const SUBTYPE_DATETIME_TIME = 'time';

lib/Db/RowCellRelation.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OCA\Tables\Db;
6+
7+
class RowCellRelation extends RowCellSuper {
8+
protected ?int $value = null;
9+
protected ?int $valueType = null;
10+
11+
public function __construct() {
12+
parent::__construct();
13+
$this->addType('value', 'integer');
14+
}
15+
16+
public function jsonSerialize(): array {
17+
return parent::jsonSerializePreparation($this->value, $this->valueType);
18+
}
19+
}

lib/Db/RowCellRelationMapper.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OCA\Tables\Db;
6+
7+
use OCP\DB\QueryBuilder\IQueryBuilder;
8+
use OCP\IDBConnection;
9+
10+
class RowCellRelationMapper extends RowCellMapperSuper {
11+
protected string $table = 'tables_row_cells_relation';
12+
13+
public function __construct(IDBConnection $db) {
14+
parent::__construct($db, $this->table, RowCellRelation::class);
15+
}
16+
17+
/**
18+
* @inheritDoc
19+
*/
20+
public function hasMultipleValues(): bool {
21+
return false;
22+
}
23+
24+
/**
25+
* @inheritDoc
26+
*/
27+
public function getDbParamType() {
28+
return IQueryBuilder::PARAM_INT;
29+
}
30+
31+
/**
32+
* @inheritDoc
33+
*/
34+
public function format(Column $column, ?string $value) {
35+
return (int)$value;
36+
}
37+
}

lib/Helper/ColumnsHelper.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ class ColumnsHelper {
2424
Column::TYPE_DATETIME,
2525
Column::TYPE_SELECTION,
2626
Column::TYPE_USERGROUP,
27+
Column::TYPE_RELATION,
2728
];
2829

2930
public function __construct(
@@ -37,6 +38,9 @@ public function resolveSearchValue(string $placeholder, string $userId, ?Column
3738
if (str_starts_with($placeholder, '@selection-id-')) {
3839
return substr($placeholder, 14);
3940
}
41+
if (str_starts_with($placeholder, '@relation-id-')) {
42+
return substr($placeholder, 13);
43+
}
4044

4145
$placeholderParts = explode(':', $placeholder, 2);
4246
$placeholderName = ltrim($placeholderParts[0], '@');
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OCA\Tables\Migration;
6+
7+
use Closure;
8+
use OCP\DB\ISchemaWrapper;
9+
use OCP\DB\Types;
10+
use OCP\Migration\IOutput;
11+
use OCP\Migration\SimpleMigrationStep;
12+
13+
class Version002001Date20260109000000 extends SimpleMigrationStep {
14+
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
15+
/** @var ISchemaWrapper $schema */
16+
$schema = $schemaClosure();
17+
18+
$changes = $this->createRelationTable($schema, 'relation', Types::INTEGER);
19+
return $changes;
20+
}
21+
22+
private function createRelationTable(ISchemaWrapper $schema, string $name, string $type): ?ISchemaWrapper {
23+
if (!$schema->hasTable('tables_row_cells_' . $name)) {
24+
$table = $schema->createTable('tables_row_cells_' . $name);
25+
$table->addColumn('id', Types::INTEGER, [
26+
'autoincrement' => true,
27+
'notnull' => true,
28+
]);
29+
$table->addColumn('column_id', Types::INTEGER, ['notnull' => true]);
30+
$table->addColumn('row_id', Types::INTEGER, ['notnull' => true]);
31+
$table->addColumn('value', $type, ['notnull' => false]);
32+
$table->addColumn('last_edit_at', Types::DATETIME, ['notnull' => true]);
33+
$table->addColumn('last_edit_by', Types::STRING, ['notnull' => true, 'length' => 64]);
34+
$table->addIndex(['column_id', 'row_id']);
35+
$table->addIndex(['column_id', 'value']);
36+
$table->setPrimaryKey(['id']);
37+
return $schema;
38+
}
39+
40+
return null;
41+
}
42+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php
2+
3+
/**
4+
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
8+
namespace OCA\Tables\Service\ColumnTypes;
9+
10+
use OCA\Tables\Db\Column;
11+
use OCA\Tables\Service\RelationService;
12+
use Psr\Log\LoggerInterface;
13+
14+
class RelationBusiness extends SuperBusiness implements IColumnTypeBusiness {
15+
16+
public function __construct(LoggerInterface $logger, private RelationService $relationService) {
17+
parent::__construct($logger);
18+
}
19+
20+
/**
21+
* @param mixed $value (array|string|null)
22+
* @param Column|null $column
23+
* @return string
24+
*/
25+
public function parseValue($value, ?Column $column = null): string {
26+
if (!$column) {
27+
$this->logger->warning('No column given, but expected on ' . __FUNCTION__ . ' within ' . __CLASS__, ['exception' => new \Exception()]);
28+
return '';
29+
}
30+
31+
$relationData = $this->relationService->getRelationData($column);
32+
33+
if (is_array($value) && isset($value['context']) && $value['context'] === 'import') {
34+
$matchingRelation = array_filter($relationData, fn($relation) => $relation['label'] === $value['value']);
35+
if (!empty($matchingRelation)) {
36+
return json_encode(reset($matchingRelation)['id']);
37+
}
38+
} else {
39+
if (isset($relationData[$value])) {
40+
return json_encode($relationData[$value]['id']);
41+
}
42+
}
43+
44+
return '';
45+
}
46+
47+
/**
48+
* @param mixed $value (array|string|null)
49+
* @param Column|null $column
50+
* @return bool
51+
*/
52+
public function canBeParsed($value, ?Column $column = null): bool {
53+
if (!$column) {
54+
$this->logger->warning('No column given, but expected on ' . __FUNCTION__ . ' within ' . __CLASS__, ['exception' => new \Exception()]);
55+
return false;
56+
}
57+
if ($value === null) {
58+
return true;
59+
}
60+
61+
$relationData = $this->relationService->getRelationData($column);
62+
63+
if (is_array($value) && isset($value['context']) && $value['context'] === 'import') {
64+
$matchingRelation = array_filter($relationData, fn($relation) => $relation['label'] === $value['value']);
65+
if (!empty($matchingRelation)) {
66+
return true;
67+
}
68+
} else {
69+
if (isset($relationData[$value])) {
70+
return true;
71+
}
72+
}
73+
74+
return false;
75+
}
76+
}

lib/Service/ImportService.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,10 @@ private function getPreviewData(Worksheet $worksheet): array {
183183
$value = $cell->getValue();
184184
// $cellIterator`s index is based on 1, not 0.
185185
$colIndex = $cellIterator->getCurrentColumnIndex() - 1;
186+
if (!array_key_exists($colIndex, $this->columns)) {
187+
continue;
188+
}
189+
186190
$column = $this->columns[$colIndex];
187191

188192
if (!array_key_exists($colIndex, $columns)) {

0 commit comments

Comments
 (0)