From c42bd256715849738a53610006a1b83ef7981f2a Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Mon, 4 Dec 2023 12:45:53 +0100 Subject: [PATCH 01/73] add migration file for new db tables (only for text-line and number columns yet) Signed-off-by: Florian Steffens --- .../Version000700Date20230916000000.php | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 lib/Migration/Version000700Date20230916000000.php diff --git a/lib/Migration/Version000700Date20230916000000.php b/lib/Migration/Version000700Date20230916000000.php new file mode 100644 index 000000000..2c54d439d --- /dev/null +++ b/lib/Migration/Version000700Date20230916000000.php @@ -0,0 +1,78 @@ +createRowSleevesTable($schema); + + $rowTypeSchema = [ + [ + 'name' => 'text', + 'type' => Types::TEXT, + ], + [ + 'name' => 'number', + 'type' => Types::FLOAT, + ], + ] ; + + foreach ($rowTypeSchema as $colType) { + $this->createRowValueTable($schema, $colType['name'], $colType['type']); + } + + return $schema; + } + + private function createRowValueTable(ISchemaWrapper $schema, string $name, string $type) { + if (!$schema->hasTable('tables_row_cells_'.$name)) { + $table = $schema->createTable('tables_row_cells_'.$name); + $table->addColumn('id', Types::INTEGER, [ + 'autoincrement' => true, + 'notnull' => true, + ]); + $table->addColumn('column_id', Types::INTEGER, ['notnull' => true]); + $table->addColumn('row_id', Types::INTEGER, ['notnull' => true]); + $table->addColumn('value', $type, ['notnull' => false]); + // we will write this data to use it one day to extract versions of rows based on the timestamp + $table->addColumn('last_edit_at', Types::DATETIME, ['notnull' => true]); + $table->addColumn('last_edit_by', Types::STRING, ['notnull' => true, 'length' => 64]); + $table->setPrimaryKey(['id']); + } + } + + private function createRowSleevesTable(ISchemaWrapper $schema) { + if (!$schema->hasTable('tables_row_sleeves')) { + $table = $schema->createTable('tables_row_sleeves'); + $table->addColumn('id', Types::INTEGER, [ + 'autoincrement' => true, + 'notnull' => true, + ]); + $table->addColumn('table_id', Types::INTEGER, ['notnull' => true]); + $table->addColumn('created_by', Types::STRING, ['notnull' => true, 'length' => 64]); + $table->addColumn('created_at', Types::DATETIME, ['notnull' => true]); + $table->addColumn('last_edit_by', Types::STRING, ['notnull' => true, 'length' => 64]); + $table->addColumn('last_edit_at', Types::DATETIME, ['notnull' => true]); + $table->setPrimaryKey(['id']); + } + } +} From 308538e741ca30e9def98c384e28ff55ae227cdd Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Mon, 4 Dec 2023 12:47:04 +0100 Subject: [PATCH 02/73] small adjustments Signed-off-by: Florian Steffens --- lib/Db/ColumnMapper.php | 18 ++++++++++++------ lib/Db/TableMapper.php | 2 +- lib/Db/View.php | 1 - lib/Db/ViewMapper.php | 2 +- openapi.json | 2 +- 5 files changed, 15 insertions(+), 10 deletions(-) diff --git a/lib/Db/ColumnMapper.php b/lib/Db/ColumnMapper.php index 05c9466fc..e61fb3528 100644 --- a/lib/Db/ColumnMapper.php +++ b/lib/Db/ColumnMapper.php @@ -21,19 +21,25 @@ public function __construct(IDBConnection $db, LoggerInterface $logger) { } /** - * @param int $id + * @param int|array $id * - * @return Column + * @return Column|Column[] * @throws DoesNotExistException * @throws Exception * @throws MultipleObjectsReturnedException */ - public function find(int $id): Column { + public function find($id) { $qb = $this->db->getQueryBuilder(); $qb->select('*') - ->from($this->table) - ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))); - return $this->findEntity($qb); + ->from($this->table); + + if(is_array($id)) { + $qb->where($qb->expr()->in('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT_ARRAY))); + return $this->findEntities($qb); + } else { + $qb->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))); + return $this->findEntity($qb); + } } /** diff --git a/lib/Db/TableMapper.php b/lib/Db/TableMapper.php index 614b1baf4..c26fe2a26 100644 --- a/lib/Db/TableMapper.php +++ b/lib/Db/TableMapper.php @@ -51,7 +51,7 @@ public function findOwnership(int $id): string { /** * @param string|null $userId - * @return array + * @return Table[] * @throws Exception */ public function findAll(?string $userId = null): array { diff --git a/lib/Db/View.php b/lib/Db/View.php index ac617238f..8eccd6fbf 100644 --- a/lib/Db/View.php +++ b/lib/Db/View.php @@ -3,7 +3,6 @@ namespace OCA\Tables\Db; use JsonSerializable; - use OCA\Tables\ResponseDefinitions; use OCP\AppFramework\Db\Entity; diff --git a/lib/Db/ViewMapper.php b/lib/Db/ViewMapper.php index 65a82adf7..ab198c825 100644 --- a/lib/Db/ViewMapper.php +++ b/lib/Db/ViewMapper.php @@ -47,7 +47,7 @@ public function find(int $id, bool $skipEnhancement = false): View { /** * @param int|null $tableId - * @return array + * @return View[] * @throws Exception * @throws InternalError */ diff --git a/openapi.json b/openapi.json index 3fbaad0ea..cf542b1fb 100644 --- a/openapi.json +++ b/openapi.json @@ -7341,4 +7341,4 @@ } }, "tags": [] -} \ No newline at end of file +} From cf70afb7813cee71e078b3b6e48d2914ce0827df Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Mon, 4 Dec 2023 12:47:25 +0100 Subject: [PATCH 03/73] new models and mappers Signed-off-by: Florian Steffens --- lib/Db/IRowCell.php | 18 ++ lib/Db/IRowCellMapper.php | 36 +++ lib/Db/Row2.php | 156 +++++++++ lib/Db/Row2Mapper.php | 565 +++++++++++++++++++++++++++++++++ lib/Db/RowCellMapperSuper.php | 73 +++++ lib/Db/RowCellNumber.php | 11 + lib/Db/RowCellNumberMapper.php | 27 ++ lib/Db/RowCellSuper.php | 64 ++++ lib/Db/RowCellText.php | 11 + lib/Db/RowCellTextMapper.php | 15 + lib/Db/RowSleeve.php | 44 +++ lib/Db/RowSleeveMapper.php | 45 +++ 12 files changed, 1065 insertions(+) create mode 100644 lib/Db/IRowCell.php create mode 100644 lib/Db/IRowCellMapper.php create mode 100644 lib/Db/Row2.php create mode 100644 lib/Db/Row2Mapper.php create mode 100644 lib/Db/RowCellMapperSuper.php create mode 100644 lib/Db/RowCellNumber.php create mode 100644 lib/Db/RowCellNumberMapper.php create mode 100644 lib/Db/RowCellSuper.php create mode 100644 lib/Db/RowCellText.php create mode 100644 lib/Db/RowCellTextMapper.php create mode 100644 lib/Db/RowSleeve.php create mode 100644 lib/Db/RowSleeveMapper.php diff --git a/lib/Db/IRowCell.php b/lib/Db/IRowCell.php new file mode 100644 index 000000000..2524b3bc4 --- /dev/null +++ b/lib/Db/IRowCell.php @@ -0,0 +1,18 @@ +id; + } + public function setId(int $id): void { + $this->id = $id; + } + + public function getTableId(): ?int { + return $this->tableId; + } + public function setTableId(int $tableId): void { + $this->tableId = $tableId; + } + + public function getCreatedBy(): ?string { + return $this->createdBy; + } + public function setCreatedBy(string $userId): void { + $this->createdBy = $userId; + } + + public function getCreatedAt(): ?string { + return $this->createdAt; + } + public function setCreatedAt(string $time): void { + $this->createdAt = $time; + } + + public function getLastEditBy(): ?string { + return $this->lastEditBy; + } + public function setLastEditBy(string $userId): void { + $this->lastEditBy = $userId; + } + + public function getLastEditAt(): ?string { + return $this->lastEditAt; + } + public function setLastEditAt(string $time): void { + $this->lastEditAt = $time; + } + + public function getData(): ?array { + return $this->data; + } + + /** + * @param list $data + * @return void + */ + public function setData(array $data): void { + foreach ($data as $cell) { + $this->insertOrUpdateCell($cell); + } + } + + /** + * @param int $columnId + * @param int|float|string $value + * @return void + */ + public function addCell(int $columnId, $value) { + $this->data[] = ['columnId' => $columnId, 'value' => $value]; + $this->addChangedColumnId($columnId); + } + + /** + * @param array{columnId: int, value: mixed} $entry + * @return string + */ + public function insertOrUpdateCell(array $entry): string { + $columnId = $entry['columnId']; + $value = $entry['value']; + foreach ($this->data as &$cell) { + if($cell['columnId'] === $columnId) { + if ($cell['value'] != $value) { // yes, no type safety here + $cell['value'] = $value; + $this->addChangedColumnId($columnId); + } + return 'updated'; + } + } + $this->data[] = ['columnId' => $columnId, 'value' => $value]; + $this->addChangedColumnId($columnId); + return 'inserted'; + } + + public function removeCell(int $columnId): void { + // TODO + } + + public function jsonSerialize(): array { + return [ + 'id' => $this->id, + 'tableId' => $this->tableId, + 'data' => $this->data, + 'createdBy' => $this->createdBy, + 'createdAt' => $this->createdAt, + 'lastEditBy' => $this->lastEditBy, + 'lastEditAt' => $this->lastEditAt, + ]; + } + + /** + * Can only be changed by private methods + * @param int $columnId + * @return void + */ + private function addChangedColumnId(int $columnId): void { + if ($this->loaded && !in_array($columnId, $this->changedColumnIds)) { + $this->changedColumnIds[] = $columnId; + } + } + + /** + * @return list + */ + public function getChangedCells(): array { + $out = []; + foreach ($this->data as $cell) { + if (in_array($cell['columnId'], $this->changedColumnIds)) { + $out[] = $cell; + } + } + return $out; + } + + /** + * Set loaded status to true + * starting now changes will be tracked + * + * @return void + */ + public function markAsLoaded(): void { + $this->loaded = true; + } + +} diff --git a/lib/Db/Row2Mapper.php b/lib/Db/Row2Mapper.php new file mode 100644 index 000000000..f9a8bdf9a --- /dev/null +++ b/lib/Db/Row2Mapper.php @@ -0,0 +1,565 @@ +rowSleeveMapper = $rowSleeveMapper; + $this->userId = $userId; + $this->db = $db; + $this->logger = $logger; + $this->userHelper = $userHelper; + } + + /** + * @throws InternalError + */ + public function delete(Row2 $row): Row2 { + foreach (['text', 'number'] as $columnType) { + $cellMapperClassName = 'OCA\Tables\Db\RowCell' . ucfirst($columnType) . 'Mapper'; + /** @var RowCellMapperSuper $cellMapper */ + try { + $cellMapper = Server::get($cellMapperClassName); + } catch (NotFoundExceptionInterface|ContainerExceptionInterface $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage()); + } + try { + $cellMapper->deleteAllForRow($row->getId()); + } catch (Exception $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + } + } + try { + $this->rowSleeveMapper->deleteById($row->getId()); + } catch (Exception $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + } + + return $row; + } + + /** + * @param int $id + * @param Column[] $columns + * @return Row2 + * @throws InternalError + * @throws NotFoundError + */ + public function find(int $id, array $columns): Row2 { + $this->setColumns($columns); + $columnIdsArray = array_map(fn (Column $column) => $column->getId(), $columns); + $rows = $this->getRows([$id], $columnIdsArray); + if (count($rows) === 1) { + return $rows[0]; + } elseif (count($rows) === 0) { + $e = new Exception('Wanted row not found.'); + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new NotFoundError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + } else { + $e = new Exception('Too many results for one wanted row.'); + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + } + } + + /** + * @throws DoesNotExistException + * @throws MultipleObjectsReturnedException + * @throws Exception + */ + public function getTableIdForRow(int $rowId): int { + $rowSleeve = $this->rowSleeveMapper->find($rowId); + return $rowSleeve->getTableId(); + } + + /** + * @param string $userId + * @param array|null $filter + * @param int|null $limit + * @param int|null $offset + * @return int[] + * @throws InternalError + */ + private function getWantedRowIds(string $userId, ?array $filter = null, ?int $limit = null, ?int $offset = null): array { + $qb = $this->db->getQueryBuilder(); + + $qb->select('id') + ->from('tables_row_sleeves', 'sleeves'); + // ->where($qb->expr()->eq('table_id', $qb->createNamedParameter($tableId, IQueryBuilder::PARAM_INT))); + if($filter) { + $this->addFilterToQuery($qb, $filter, $userId); + } + if ($limit !== null) { + $qb->setMaxResults($limit); + } + if ($offset !== null) { + $qb->setFirstResult($offset); + } + + try { + $result = $this->db->executeQuery($qb->getSQL(), $qb->getParameters(), $qb->getParameterTypes()); + } catch (Exception $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage(), ); + } + + return array_map(fn (array $item) => $item['id'], $result->fetchAll()); + } + + /** + * @param Column[] $columns + * @param int|null $limit + * @param int|null $offset + * @return Row2[] + * @throws InternalError + */ + public function findAll(array $columns, int $limit = null, int $offset = null, array $filter = null, array $sort = null, string $userId = null): array { + $this->setColumns($columns); + $columnIdsArray = array_map(fn (Column $column) => $column->getId(), $columns); + + $wantedRowIdsArray = $this->getWantedRowIds($userId, $filter, $limit, $offset); + + // TODO add sorting + + return $this->getRows($wantedRowIdsArray, $columnIdsArray); + } + + /** + * @param array $rowIds + * @param array $columnIds + * @return Row2[] + * @throws InternalError + */ + private function getRows(array $rowIds, array $columnIds): array { + $qb = $this->db->getQueryBuilder(); + + $qbText = $this->db->getQueryBuilder(); + $qbText->select('*') + ->from('tables_row_cells_text') + ->where($qb->expr()->in('column_id', $qb->createNamedParameter($columnIds, IQueryBuilder::PARAM_INT_ARRAY, ':columnIds'))) + ->andWhere($qb->expr()->in('row_id', $qb->createNamedParameter($rowIds, IQueryBuilder::PARAM_INT_ARRAY, ':rowsIds'))); + + $qbNumber = $this->db->getQueryBuilder(); + $qbNumber->select('*') + ->from('tables_row_cells_number') + ->where($qb->expr()->in('column_id', $qb->createNamedParameter($columnIds, IQueryBuilder::PARAM_INT_ARRAY, ':columnIds'))) + ->andWhere($qb->expr()->in('row_id', $qb->createNamedParameter($rowIds, IQueryBuilder::PARAM_INT_ARRAY, ':rowIds'))); + + $qb->select('row_id', 'column_id', 'created_by', 'created_at', 't1.last_edit_by', 't1.last_edit_at', 'value', 'table_id') + ->from($qb->createFunction('((' . $qbText->getSQL() . ') UNION ALL (' . $qbNumber->getSQL() . '))'), 't1') + ->innerJoin('t1', 'tables_row_sleeves', 'rowSleeve', 'rowSleeve.id = t1.row_id'); + + try { + $result = $this->db->executeQuery($qb->getSQL(), $qb->getParameters(), $qb->getParameterTypes()); + } catch (Exception $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage(), ); + } + + return $this->parseEntities($result); + } + + /** + * @throws InternalError + */ + private function addFilterToQuery(IQueryBuilder &$qb, array $filters, string $userId): void { + // TODO move this into service + $this->replaceMagicValues($filters, $userId); + + if (count($filters) > 0) { + $qb->andWhere( + $qb->expr()->orX( + ...$this->getFilterGroups($qb, $filters) + ) + ); + } + } + + private function replaceMagicValues(array &$filters, string $userId): void { + foreach ($filters as &$filterGroup) { + foreach ($filterGroup as &$filter) { + if(str_starts_with($filter['value'], '@')) { + $filter['value'] = $this->resolveSearchValue($filter['value'], $userId); + } + } + } + } + + /** + * @throws InternalError + */ + private function getFilterGroups(IQueryBuilder &$qb, array $filters): array { + $filterGroups = []; + foreach ($filters as $filterGroup) { + $tmp = $this->getFilter($qb, $filterGroup); + $filterGroups[] = $qb->expr()->andX(...$tmp); + } + return $filterGroups; + } + + /** + * @throws InternalError + */ + private function getFilter(IQueryBuilder &$qb, array $filterGroup): array { + $filterExpressions = []; + foreach ($filterGroup as $filter) { + $sql = $qb->expr()->in( + 'id', + $qb->createFunction($this->getFilterExpression($qb, $this->columns[$filter['columnId']], $filter['operator'], $filter['value'])->getSQL()) + ); + $filterExpressions[] = $sql; + } + return $filterExpressions; + } + + /** + * @throws InternalError + */ + private function getFilterExpression(IQueryBuilder $qb, Column $column, string $operator, string $value): IQueryBuilder { + if($column->getType() === 'number' && $column->getNumberDecimals() === 0) { + $paramType = IQueryBuilder::PARAM_INT; + $value = (int)$value; + } elseif ($column->getType() === 'datetime') { + $paramType = IQueryBuilder::PARAM_DATE; + } else { + $paramType = IQueryBuilder::PARAM_STR; + } + + $qb2 = $this->db->getQueryBuilder(); + $qb2->select('row_id'); + $qb2->where($qb->expr()->eq('column_id', $qb->createNamedParameter($column->getId()), IQueryBuilder::PARAM_INT)); + + switch($column->getType()) { + case 'text': + $qb2->from('tables_row_cells_text'); + break; + case 'number': + $qb2->from('tables_row_cells_number'); + break; + default: + throw new InternalError('column type unknown to match cell-table for it'); + } + + + switch ($operator) { + case 'begins-with': + return $qb2->andWhere($qb->expr()->like('value', $qb->createNamedParameter('%'.$value, $paramType))); + case 'ends-with': + return $qb2->andWhere($qb->expr()->like('value', $qb->createNamedParameter($value.'%', $paramType))); + case 'contains': + return $qb2->andWhere($qb->expr()->like('value', $qb->createNamedParameter('%'.$value.'%', $paramType))); + case 'is-equal': + return $qb2->andWhere($qb->expr()->eq('value', $qb->createNamedParameter($value, $paramType))); + case 'is-greater-than': + return $qb2->andWhere($qb->expr()->gt('value', $qb->createNamedParameter($value, $paramType))); + case 'is-greater-than-or-equal': + return $qb2->andWhere($qb->expr()->gte('value', $qb->createNamedParameter($value, $paramType))); + case 'is-lower-than': + return $qb2->andWhere($qb->expr()->lt('value', $qb->createNamedParameter($value, $paramType))); + case 'is-lower-than-or-equal': + return $qb2->andWhere($qb->expr()->lte('value', $qb->createNamedParameter($value, $paramType))); + case 'is-empty': + return $qb2->andWhere($qb->expr()->isNull('value')); + default: + throw new InternalError('Operator '.$operator.' is not supported.'); + } + } + + /** @noinspection DuplicatedCode */ + private function resolveSearchValue(string $magicValue, string $userId): string { + switch (ltrim($magicValue, '@')) { + case 'me': return $userId; + case 'my-name': return $this->userHelper->getUserDisplayName($userId); + case 'checked': return 'true'; + case 'unchecked': return 'false'; + case 'stars-0': return '0'; + case 'stars-1': return '1'; + case 'stars-2': return '2'; + case 'stars-3': return '3'; + case 'stars-4': return '4'; + case 'stars-5': return '5'; + case 'datetime-date-today': return date('Y-m-d') ? date('Y-m-d') : ''; + case 'datetime-date-start-of-year': return date('Y-01-01') ? date('Y-01-01') : ''; + case 'datetime-date-start-of-month': return date('Y-m-01') ? date('Y-m-01') : ''; + case 'datetime-date-start-of-week': + $day = date('w'); + $result = date('m-d-Y', strtotime('-'.$day.' days')); + return $result ?: ''; + case 'datetime-time-now': return date('H:i'); + case 'datetime-now': return date('Y-m-d H:i') ? date('Y-m-d H:i') : ''; + default: return $magicValue; + } + } + + /** + * @param IResult $result + * @return Row2[] + * @throws InternalError + */ + private function parseEntities(IResult $result): array { + $data = $result->fetchAll(); + + $rows = []; + foreach ($data as $rowData) { + $this->parseModel($rowData, $rows[$rowData['row_id']]); + } + + // format an array without keys + $return = []; + foreach ($rows as $row) { + $return[] = $row; + } + return $return; + } + + /** + * @throws InternalError + */ + public function isRowInViewPresent(int $rowId, View $view, string $userId): bool { + return in_array($rowId, $this->getWantedRowIds($userId, $view->getFilterArray())); + } + + /** + * @param IResult $result + * @return Row2 + * @throws InternalError + */ + private function parseEntity(IResult $result): Row2 { + $data = $result->fetchAll(); + + if(count($data) === 0) { + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': result was empty, expected one row'); + } + if(count($data) > 1) { + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': found more than one expected result'); + } + + return $this->parseModel($data[0]); + } + + + /** + * @param Row2 $row + * @param column[] $columns + * @return Row2 + * @throws InternalError + * @throws Exception + */ + public function insert(Row2 $row, array $columns): Row2 { + if(!$columns || count($columns) === 0) { + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': columns are missing'); + } + $this->setColumns($columns); + + // create a new row sleeve to get a new rowId + $rowSleeve = $this->createNewRowSleeve($row->getTableId()); + $row->setId($rowSleeve->getId()); + + // write all cells to its db-table + foreach ($row->getData() as $cell) { + $this->insertCell($rowSleeve->getId(), $cell['columnId'], $cell['value']); + } + + return $row; + } + + /** + * @throws InternalError + */ + public function update(Row2 $row, array $columns): Row2 { + if(!$columns || count($columns) === 0) { + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': columns are missing'); + } + $this->setColumns($columns); + + // if nothing has changed + if (count($row->getChangedCells()) === 0) { + return $row; + } + + // update meta data for sleeve + try { + $sleeve = $this->rowSleeveMapper->find($row->getId()); + $this->updateMetaData($sleeve); + $this->rowSleeveMapper->update($sleeve); + } catch (DoesNotExistException|MultipleObjectsReturnedException|Exception $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + } + + // write all changed cells to its db-table + foreach ($row->getChangedCells() as $cell) { + $this->insertOrUpdateCell($sleeve->getId(), $cell['columnId'], $cell['value']); + } + + return $row; + } + + /** + * @throws Exception + */ + private function createNewRowSleeve(int $tableId): RowSleeve { + $rowSleeve = new RowSleeve(); + $rowSleeve->setTableId($tableId); + $this->updateMetaData($rowSleeve, true); + return $this->rowSleeveMapper->insert($rowSleeve); + } + + /** + * Updates the last_edit_by and last_edit_at data + * optional adds the created_by and created_at data + * + * @param RowSleeve|IRowCell $entity + * @param bool $setCreate + * @return void + */ + private function updateMetaData($entity, bool $setCreate = false): void { + $time = new DateTime(); + if ($setCreate) { + $entity->setCreatedBy($this->userId); + $entity->setCreatedAt($time->format('Y-m-d H:i:s')); + } + $entity->setLastEditBy($this->userId); + $entity->setLastEditAt($time->format('Y-m-d H:i:s')); + } + + /** + * Insert a cell to its specific db-table + * + * @throws InternalError + */ + private function insertCell(int $rowId, int $columnId, string $value): void { + $cellClassName = 'OCA\Tables\Db\RowCell'.ucfirst($this->columns[$columnId]->getType()); + /** @var IRowCell $cell */ + $cell = new $cellClassName(); + + $cell->setRowIdWrapper($rowId); + $cell->setColumnIdWrapper($columnId); + $cell->setValueWrapper($value); + $this->updateMetaData($cell); + + // insert new cell + $cellMapperClassName = 'OCA\Tables\Db\RowCell'.ucfirst($this->columns[$columnId]->getType()).'Mapper'; + /** @var QBMapper $cellMapper */ + try { + $cellMapper = Server::get($cellMapperClassName); + } catch (NotFoundExceptionInterface|ContainerExceptionInterface $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + } + try { + $cellMapper->insert($cell); + } catch (Exception $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + } + } + + /** + * @param IRowCell $cell + * @param IRowCellMapper $mapper + * @param mixed $value the value should be parsed to the correct format within the row service + */ + private function updateCell(IRowCell $cell, IRowCellMapper $mapper, $value): void { + $cell->setValueWrapper($value); + $this->updateMetaData($cell); + $mapper->updateWrapper($cell); + } + + /** + * @throws InternalError + */ + private function insertOrUpdateCell(int $rowId, int $columnId, string $value): void { + $cellMapperClassName = 'OCA\Tables\Db\RowCell'.ucfirst($this->columns[$columnId]->getType()).'Mapper'; + /** @var IRowCellMapper $cellMapper */ + try { + $cellMapper = Server::get($cellMapperClassName); + } catch (NotFoundExceptionInterface|ContainerExceptionInterface $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + } + try { + $cell = $cellMapper->findByRowAndColumn($rowId, $columnId); + $this->updateCell($cell, $cellMapper, $value); + } catch (DoesNotExistException $e) { + $this->insertCell($rowId, $columnId, $value); + } catch (MultipleObjectsReturnedException|Exception $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + } + } + + /** + * @param Column[] $columns + */ + private function setColumns(array $columns): void { + foreach ($columns as $column) { + $this->columns[$column->getId()] = $column; + } + } + + /** + * @throws InternalError + */ + private function formatValue(Column $column, string $value) { + $cellMapperClassName = 'OCA\Tables\Db\RowCell'.ucfirst($column->getType()).'Mapper'; + /** @var RowCellMapperSuper $cellMapper */ + try { + $cellMapper = Server::get($cellMapperClassName); + } catch (NotFoundExceptionInterface|ContainerExceptionInterface $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + } + return $cellMapper->parseValueOutgoing($column, $value); + } + + /** + * @param array $data + * @param Row2|null $row + * @return Row2 + * @throws InternalError + */ + private function parseModel(array $data, ?Row2 &$row = null): Row2 { + if (!$row) { + $row = new Row2(); + $row->setId($data['row_id']); + $row->setTableId($data['table_id']); + $row->setCreatedBy($data['created_by']); + $row->setCreatedAt($data['created_at']); + $row->setLastEditBy($data['last_edit_by']); + $row->setLastEditAt($data['last_edit_at']); + } + $row->addCell($data['column_id'], $this->formatValue($this->columns[$data['column_id']], $data['value'])); + return $row; + } + +} diff --git a/lib/Db/RowCellMapperSuper.php b/lib/Db/RowCellMapperSuper.php new file mode 100644 index 000000000..2607c30e4 --- /dev/null +++ b/lib/Db/RowCellMapperSuper.php @@ -0,0 +1,73 @@ + */ +class RowCellMapperSuper extends QBMapper implements IRowCellMapper { + + public function __construct(IDBConnection $db, string $table, string $class) { + parent::__construct($db, $table, $class); + } + + public function parseValueOutgoing(Column $column, $value) { + return $value; + } + + /** + * @throws Exception + */ + public function deleteAllForRow(int $rowId) { + $qb = $this->db->getQueryBuilder(); + $qb->delete($this->tableName) + ->where( + $qb->expr()->eq('row_id', $qb->createNamedParameter($rowId, IQueryBuilder::PARAM_INT)) + ); + $qb->executeStatement(); + } + + /** + * @throws MultipleObjectsReturnedException + * @throws DoesNotExistException + * @throws Exception + */ + public function findByRowAndColumn(int $rowId, int $columnId): IRowCell { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->tableName) + ->where($qb->expr()->eq('row_id', $qb->createNamedParameter($rowId, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('column_id', $qb->createNamedParameter($columnId, IQueryBuilder::PARAM_INT))); + $item = $this->findEntity($qb); + return $item; + } + + /** + * @throws MultipleObjectsReturnedException + * @throws DoesNotExistException + * @throws Exception + */ + public function find(int $id): IRowCell { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->tableName) + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))); + return $this->findEntity($qb); + } + + /** + * @throws Exception + */ + public function updateWrapper(IRowCell $cell): IRowCell { + // TODO is this possible? + /** @var IRowCell $cell */ + $cell = $this->update($cell); + return $cell; + } + +} diff --git a/lib/Db/RowCellNumber.php b/lib/Db/RowCellNumber.php new file mode 100644 index 000000000..2e36071a7 --- /dev/null +++ b/lib/Db/RowCellNumber.php @@ -0,0 +1,11 @@ +value); + } +} diff --git a/lib/Db/RowCellNumberMapper.php b/lib/Db/RowCellNumberMapper.php new file mode 100644 index 000000000..526f9ff59 --- /dev/null +++ b/lib/Db/RowCellNumberMapper.php @@ -0,0 +1,27 @@ + */ +class RowCellNumberMapper extends RowCellMapperSuper implements IRowCellMapper { + protected string $table = 'tables_row_cells_number'; + + public function __construct(IDBConnection $db) { + parent::__construct($db, $this->table, RowCellNumber::class); + } + + public function parseValueOutgoing(Column $column, $value) { + if($value === '') { + return null; + } + $decimals = $column->getNumberDecimals() ?? 0; + if ($decimals === 0) { + return intval($value); + } else { + return round(floatval($value), $decimals); + } + } +} diff --git a/lib/Db/RowCellSuper.php b/lib/Db/RowCellSuper.php new file mode 100644 index 000000000..12ec06cbf --- /dev/null +++ b/lib/Db/RowCellSuper.php @@ -0,0 +1,64 @@ +addType('id', 'integer'); + $this->addType('columnId', 'integer'); + $this->addType('rowId', 'integer'); + } + + public function jsonSerializePreparation($value): array { + return [ + 'id' => $this->id, + 'columnId' => $this->columnId, + 'rowId' => $this->rowId, + 'lastEditBy' => $this->lastEditBy, + 'lastEditAt' => $this->lastEditAt, + 'value' => $value + ]; + } + + public function setRowIdWrapper(int $rowId) { + $this->setRowId($rowId); + } + + public function setColumnIdWrapper(int $columnId) { + $this->setColumnId($columnId); + } + + public function setValueWrapper($value) { + $this->setValue($value); + } + + public function jsonSerialize() { + // has to be overwritten + } +} diff --git a/lib/Db/RowCellText.php b/lib/Db/RowCellText.php new file mode 100644 index 000000000..60ec7cf16 --- /dev/null +++ b/lib/Db/RowCellText.php @@ -0,0 +1,11 @@ +value); + } +} diff --git a/lib/Db/RowCellTextMapper.php b/lib/Db/RowCellTextMapper.php new file mode 100644 index 000000000..1aee26899 --- /dev/null +++ b/lib/Db/RowCellTextMapper.php @@ -0,0 +1,15 @@ + */ +class RowCellTextMapper extends RowCellMapperSuper implements IRowCellMapper { + protected string $table = 'tables_row_cells_text'; + + public function __construct(IDBConnection $db) { + parent::__construct($db, $this->table, RowCellText::class); + } +} diff --git a/lib/Db/RowSleeve.php b/lib/Db/RowSleeve.php new file mode 100644 index 000000000..2cbffd70a --- /dev/null +++ b/lib/Db/RowSleeve.php @@ -0,0 +1,44 @@ +addType('id', 'integer'); + $this->addType('tableId', 'integer'); + } + + public function jsonSerialize(): array { + return [ + 'id' => $this->id, + 'tableId' => $this->tableId, + 'createdBy' => $this->createdBy, + 'createdAt' => $this->createdAt, + 'lastEditBy' => $this->lastEditBy, + 'lastEditAt' => $this->lastEditAt, + ]; + } +} diff --git a/lib/Db/RowSleeveMapper.php b/lib/Db/RowSleeveMapper.php new file mode 100644 index 000000000..4bc486e25 --- /dev/null +++ b/lib/Db/RowSleeveMapper.php @@ -0,0 +1,45 @@ + */ +class RowSleeveMapper extends QBMapper { + protected string $table = 'tables_row_sleeves'; + + public function __construct(IDBConnection $db) { + parent::__construct($db, $this->table, RowSleeve::class); + } + + /** + * @throws MultipleObjectsReturnedException + * @throws DoesNotExistException + * @throws Exception + */ + public function find(int $id): RowSleeve { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->table) + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))); + return $this->findEntity($qb); + } + + /** + * @param int $sleeveId + * @throws Exception + */ + public function deleteById(int $sleeveId) { + $qb = $this->db->getQueryBuilder(); + $qb->delete($this->tableName) + ->where( + $qb->expr()->eq('id', $qb->createNamedParameter($sleeveId, IQueryBuilder::PARAM_INT)) + ); + $qb->executeStatement(); + } +} From f86cb79cad7cea56ac51666d700e448601929566 Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Mon, 4 Dec 2023 12:48:31 +0100 Subject: [PATCH 04/73] basic integration of the new models (only for text-line and number columns yet) Signed-off-by: Florian Steffens --- lib/Service/RowService.php | 182 +++++++++++++++++++------------------ 1 file changed, 94 insertions(+), 88 deletions(-) diff --git a/lib/Service/RowService.php b/lib/Service/RowService.php index c9ff289bd..63ae25a52 100644 --- a/lib/Service/RowService.php +++ b/lib/Service/RowService.php @@ -2,10 +2,11 @@ namespace OCA\Tables\Service; -use DateTime; use OCA\Tables\Db\Column; use OCA\Tables\Db\ColumnMapper; use OCA\Tables\Db\Row; +use OCA\Tables\Db\Row2; +use OCA\Tables\Db\Row2Mapper; use OCA\Tables\Db\RowMapper; use OCA\Tables\Db\TableMapper; use OCA\Tables\Db\View; @@ -27,14 +28,17 @@ class RowService extends SuperService { private ColumnMapper $columnMapper; private ViewMapper $viewMapper; private TableMapper $tableMapper; + private Row2Mapper $row2Mapper; + private array $tmpRows = []; // holds already loaded rows as a small cache public function __construct(PermissionsService $permissionsService, LoggerInterface $logger, ?string $userId, - RowMapper $mapper, ColumnMapper $columnMapper, ViewMapper $viewMapper, TableMapper $tableMapper) { + RowMapper $mapper, ColumnMapper $columnMapper, ViewMapper $viewMapper, TableMapper $tableMapper, Row2Mapper $row2Mapper) { parent::__construct($logger, $userId, $permissionsService); $this->mapper = $mapper; $this->columnMapper = $columnMapper; $this->viewMapper = $viewMapper; $this->tableMapper = $tableMapper; + $this->row2Mapper = $row2Mapper; } /** @@ -49,7 +53,9 @@ public function __construct(PermissionsService $permissionsService, LoggerInterf public function findAllByTable(int $tableId, string $userId, ?int $limit = null, ?int $offset = null): array { try { if ($this->permissionsService->canReadRowsByElementId($tableId, 'table', $userId)) { - return $this->mapper->findAllByTable($tableId, $limit, $offset); + return $this->row2Mapper->findAll($this->columnMapper->findAllByTable($tableId), $limit, $offset, null, null, $userId); + + // return $this->mapper->findAllByTable($tableId, $limit, $offset); } else { throw new PermissionError('no read access to table id = '.$tableId); } @@ -73,7 +79,12 @@ public function findAllByTable(int $tableId, string $userId, ?int $limit = null, public function findAllByView(int $viewId, string $userId, ?int $limit = null, ?int $offset = null): array { try { if ($this->permissionsService->canReadRowsByElementId($viewId, 'view', $userId)) { - return $this->mapper->findAllByView($this->viewMapper->find($viewId), $userId, $limit, $offset); + $view = $this->viewMapper->find($viewId); + $columnsArray = $view->getColumnsArray(); + $columns = $this->columnMapper->find($columnsArray); + return $this->row2Mapper->findAll($columns, $limit, $offset, $view->getFilterArray(), $view->getSortArray(), $userId); + + // return $this->mapper->findAllByView($this->viewMapper->find($viewId), $userId, $limit, $offset); } else { throw new PermissionError('no read access to view id = '.$viewId); } @@ -114,7 +125,7 @@ public function find(int $id): Row { * @param int|null $tableId * @param int|null $viewId * @param list $data - * @return Row + * @return Row2 * * @throws NotFoundError * @throws PermissionError @@ -168,19 +179,26 @@ public function create(?int $tableId, ?int $viewId, array $data):Row { $data = $this->cleanupData($data, $columns, $tableId, $viewId); - $time = new DateTime(); + // perf + $row2 = new Row2(); + $row2->setTableId($tableId); + $row2->setData($data); + try { + return $this->row2Mapper->insert($row2, $this->columnMapper->findAllByTable($tableId)); + } catch (InternalError|Exception $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + } + + /*$time = new DateTime(); $item = new Row(); $item->setDataArray($data); - if ($tableId) { - $item->setTableId($tableId); - } elseif (isset($view) && $view) { - $item->setTableId($view->getTableId()); - } + $item->setTableId($viewId ? $view->getTableId() : $tableId); $item->setCreatedBy($this->userId); $item->setCreatedAt($time->format('Y-m-d H:i:s')); $item->setLastEditBy($this->userId); $item->setLastEditAt($time->format('Y-m-d H:i:s')); - return $this->mapper->insert($item); + return $this->mapper->insert($item);*/ } /** @@ -254,6 +272,29 @@ private function getColumnFromColumnsArray(int $columnId, array $columns): ?Colu return null; } + /** + * @throws NotFoundError + * @throws InternalError + */ + private function getRowById(int $rowId): Row2 { + if (isset($this->tmpRows[$rowId])) { + return $this->tmpRows[$rowId]; + } + + try { + $row = $this->row2Mapper->find($rowId, $this->columnMapper->findAllByTable($this->row2Mapper->getTableIdForRow($rowId))); + $row->markAsLoaded(); + } catch (InternalError|DoesNotExistException|MultipleObjectsReturnedException|Exception $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + } catch (NotFoundError $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new NotFoundError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + } + $this->tmpRows[$rowId] = $row; + return $row; + } + /** * Update multiple cells in a row * @@ -261,7 +302,7 @@ private function getColumnFromColumnsArray(int $columnId, array $columns): ?Colu * @param int|null $viewId * @param list $data * @param string $userId - * @return Row + * @return Row2 * * @throws InternalError * @throws PermissionError @@ -273,13 +314,13 @@ public function updateSet( ?int $viewId, array $data, string $userId - ):Row { + ): Row2 { try { - $item = $this->mapper->find($id); - } catch (MultipleObjectsReturnedException|Exception $e) { + $item = $this->getRowById($id); + } catch (InternalError $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); - } catch (DoesNotExistException $e) { + } catch (NotFoundError $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); throw new NotFoundError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); } @@ -300,11 +341,12 @@ public function updateSet( throw new NotFoundError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); } - $rowIds = $this->mapper->getRowIdsOfView($view, $userId); - if(!in_array($id, $rowIds)) { + // is row in view? + if($this->row2Mapper->isRowInViewPresent($id, $view, $userId)) { throw new PermissionError('update row id = '.$item->getId().' is not allowed.'); } + // fetch all needed columns try { $columns = $this->columnMapper->findMultiple($view->getColumnsArray()); } catch (Exception $e) { @@ -329,100 +371,64 @@ public function updateSet( $data = $this->cleanupData($data, $columns, $item->getTableId(), $viewId); - $time = new DateTime(); - $oldData = $item->getDataArray(); foreach ($data as $entry) { // Check whether the column of which the value should change is part of the table / view $column = $this->getColumnFromColumnsArray($entry['columnId'], $columns); if ($column) { - $oldData = $this->replaceOrAddData($oldData, $entry); + $item->insertOrUpdateCell($entry); } else { $this->logger->warning("Column to update row not found, will continue and ignore this."); } } - $item->setDataArray($oldData); - $item->setLastEditBy($userId); - $item->setLastEditAt($time->format('Y-m-d H:i:s')); - try { - $row = $this->mapper->update($item); - } catch (Exception $e) { - $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); - } - - if ($viewId) { - try { - $row = $this->mapper->findByView($row->getId(), $view); - } catch (DoesNotExistException|MultipleObjectsReturnedException|Exception $e) { - $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); - } - } - - return $row; - } - - private function replaceOrAddData(array $dataArray, array $newDataObject): array { - $columnId = (int) $newDataObject['columnId']; - $value = $newDataObject['value']; - - $columnFound = false; - foreach ($dataArray as $key => $c) { - if ($c['columnId'] == $columnId) { - $dataArray[$key]['value'] = $value; - $columnFound = true; - break; - } - } - // if the value was not set, add it - if (!$columnFound) { - $dataArray[] = [ - "columnId" => $columnId, - "value" => $value - ]; - } - return $dataArray; + return $this->row2Mapper->update($item, $columns); } /** * @param int $id * @param int|null $viewId * @param string $userId - * @return Row + * @return Row2 * * @throws InternalError * @throws NotFoundError * @throws PermissionError + * @noinspection DuplicatedCode */ - public function delete(int $id, ?int $viewId, string $userId): Row { + public function delete(int $id, ?int $viewId, string $userId): Row2 { try { - $item = $this->mapper->find($id); + $item = $this->getRowById($id); + } catch (InternalError $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + } catch (NotFoundError $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new NotFoundError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + } - if ($viewId) { - // security - if (!$this->permissionsService->canDeleteRowsByViewId($viewId)) { - throw new PermissionError('update row id = '.$item->getId().' is not allowed.'); - } + if ($viewId) { + // security + if (!$this->permissionsService->canDeleteRowsByViewId($viewId)) { + throw new PermissionError('update row id = '.$item->getId().' is not allowed.'); + } + try { $view = $this->viewMapper->find($viewId); - $rowIds = $this->mapper->getRowIdsOfView($view, $userId); - if(!in_array($id, $rowIds)) { - throw new PermissionError('update row id = '.$item->getId().' is not allowed.'); - } - } else { - // security - if (!$this->permissionsService->canDeleteRowsByTableId($item->getTableId())) { - throw new PermissionError('update row id = '.$item->getId().' is not allowed.'); - } + } catch (InternalError|DoesNotExistException|MultipleObjectsReturnedException|Exception $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + } + $rowIds = $this->mapper->getRowIdsOfView($view, $userId); + if(!in_array($id, $rowIds)) { + throw new PermissionError('update row id = '.$item->getId().' is not allowed.'); + } + } else { + // security + if (!$this->permissionsService->canDeleteRowsByTableId($item->getTableId())) { + throw new PermissionError('update row id = '.$item->getId().' is not allowed.'); } - - $this->mapper->delete($item); - return $item; - } catch (DoesNotExistException $e) { - throw new NotFoundError($e->getMessage()); - } catch (MultipleObjectsReturnedException|Exception $e) { - throw new InternalError($e->getMessage()); } + + return $this->row2Mapper->delete($item); } /** From b217de25c73ed957637151e9f0f43266d19497bf Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Tue, 5 Dec 2023 16:36:46 +0100 Subject: [PATCH 05/73] WIP psalm and super classes Signed-off-by: Florian Steffens --- lib/Db/IRowCellMapper.php | 6 +++--- lib/Db/Row2.php | 2 +- lib/Db/Row2Mapper.php | 18 +++++++++++++----- lib/Db/RowCellMapperSuper.php | 13 ++++++++----- lib/Db/RowCellNumberMapper.php | 2 +- lib/Db/RowCellSuper.php | 6 +----- lib/Db/RowCellTextMapper.php | 2 +- lib/Db/RowSleeve.php | 2 +- 8 files changed, 29 insertions(+), 22 deletions(-) diff --git a/lib/Db/IRowCellMapper.php b/lib/Db/IRowCellMapper.php index 59361111f..b37076ce2 100644 --- a/lib/Db/IRowCellMapper.php +++ b/lib/Db/IRowCellMapper.php @@ -18,7 +18,7 @@ public function deleteAllForRow(int $rowId); * @throws DoesNotExistException * @throws Exception */ - public function findByRowAndColumn(int $rowId, int $columnId): IRowCell; + public function findByRowAndColumn(int $rowId, int $columnId): RowCellSuper; public function getTableName(): string; @@ -30,7 +30,7 @@ public function insertOrUpdate(Entity $entity): Entity; public function update(Entity $entity): Entity; - public function find(int $id): IRowCell; + public function find(int $id): RowCellSuper; - public function updateWrapper(IRowCell $cell): IRowCell; + public function updateWrapper(RowCellSuper $cell): RowCellSuper; } diff --git a/lib/Db/Row2.php b/lib/Db/Row2.php index 6c2e173d6..6f945a19c 100644 --- a/lib/Db/Row2.php +++ b/lib/Db/Row2.php @@ -16,7 +16,7 @@ class Row2 implements JsonSerializable { private bool $loaded = false; // set to true if model is loaded, after that changed column ids will be collected - public function getId(): int { + public function getId(): ?int { return $this->id; } public function setId(int $id): void { diff --git a/lib/Db/Row2Mapper.php b/lib/Db/Row2Mapper.php index f9a8bdf9a..a1f81204b 100644 --- a/lib/Db/Row2Mapper.php +++ b/lib/Db/Row2Mapper.php @@ -96,7 +96,7 @@ public function find(int $id, array $columns): Row2 { * @throws MultipleObjectsReturnedException * @throws Exception */ - public function getTableIdForRow(int $rowId): int { + public function getTableIdForRow(int $rowId): ?int { $rowSleeve = $this->rowSleeveMapper->find($rowId); return $rowSleeve->getTableId(); } @@ -330,7 +330,15 @@ private function parseEntities(IResult $result): array { $rows = []; foreach ($data as $rowData) { - $this->parseModel($rowData, $rows[$rowData['row_id']]); + if (!isset($rowData['row_id'])) { + break; + } + $rowId = $rowData['row_id']; + if (!isset($rows[$rowId])) { + $rows[$rowId] = new Row2(); + } + /* @var array $rowData */ + $this->parseModel($rowData, $rows[$rowId]); } // format an array without keys @@ -369,13 +377,13 @@ private function parseEntity(IResult $result): Row2 { /** * @param Row2 $row - * @param column[] $columns + * @param Column[] $columns * @return Row2 * @throws InternalError * @throws Exception */ public function insert(Row2 $row, array $columns): Row2 { - if(!$columns || count($columns) === 0) { + if(!$columns) { throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': columns are missing'); } $this->setColumns($columns); @@ -396,7 +404,7 @@ public function insert(Row2 $row, array $columns): Row2 { * @throws InternalError */ public function update(Row2 $row, array $columns): Row2 { - if(!$columns || count($columns) === 0) { + if(!$columns) { throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': columns are missing'); } $this->setColumns($columns); diff --git a/lib/Db/RowCellMapperSuper.php b/lib/Db/RowCellMapperSuper.php index 2607c30e4..afa964b1a 100644 --- a/lib/Db/RowCellMapperSuper.php +++ b/lib/Db/RowCellMapperSuper.php @@ -9,7 +9,10 @@ use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; -/** @template-extends QBMapper */ +/** + * @template-extends QBMapper + * @template T of RowCellSuper + */ class RowCellMapperSuper extends QBMapper implements IRowCellMapper { public function __construct(IDBConnection $db, string $table, string $class) { @@ -37,7 +40,7 @@ public function deleteAllForRow(int $rowId) { * @throws DoesNotExistException * @throws Exception */ - public function findByRowAndColumn(int $rowId, int $columnId): IRowCell { + public function findByRowAndColumn(int $rowId, int $columnId): RowCellSuper { $qb = $this->db->getQueryBuilder(); $qb->select('*') ->from($this->tableName) @@ -52,7 +55,7 @@ public function findByRowAndColumn(int $rowId, int $columnId): IRowCell { * @throws DoesNotExistException * @throws Exception */ - public function find(int $id): IRowCell { + public function find(int $id): RowCellSuper { $qb = $this->db->getQueryBuilder(); $qb->select('*') ->from($this->tableName) @@ -61,11 +64,11 @@ public function find(int $id): IRowCell { } /** + * @psalm-param T $cell * @throws Exception */ - public function updateWrapper(IRowCell $cell): IRowCell { + public function updateWrapper(RowCellSuper $cell): RowCellSuper { // TODO is this possible? - /** @var IRowCell $cell */ $cell = $this->update($cell); return $cell; } diff --git a/lib/Db/RowCellNumberMapper.php b/lib/Db/RowCellNumberMapper.php index 526f9ff59..a5216f5b0 100644 --- a/lib/Db/RowCellNumberMapper.php +++ b/lib/Db/RowCellNumberMapper.php @@ -5,7 +5,7 @@ use OCP\AppFramework\Db\QBMapper; use OCP\IDBConnection; -/** @template-extends QBMapper */ +/** @template-extends RowCellMapperSuper */ class RowCellNumberMapper extends RowCellMapperSuper implements IRowCellMapper { protected string $table = 'tables_row_cells_number'; diff --git a/lib/Db/RowCellSuper.php b/lib/Db/RowCellSuper.php index 12ec06cbf..0a7ba61f4 100644 --- a/lib/Db/RowCellSuper.php +++ b/lib/Db/RowCellSuper.php @@ -23,7 +23,7 @@ * @method getLastEditAt(): string * @method setLastEditAt(string $lastEditAt) */ -class RowCellSuper extends Entity implements JsonSerializable, IRowCell { +abstract class RowCellSuper extends Entity implements JsonSerializable { protected ?int $columnId = null; protected ?int $rowId = null; protected ?string $lastEditBy = null; @@ -57,8 +57,4 @@ public function setColumnIdWrapper(int $columnId) { public function setValueWrapper($value) { $this->setValue($value); } - - public function jsonSerialize() { - // has to be overwritten - } } diff --git a/lib/Db/RowCellTextMapper.php b/lib/Db/RowCellTextMapper.php index 1aee26899..f84a43392 100644 --- a/lib/Db/RowCellTextMapper.php +++ b/lib/Db/RowCellTextMapper.php @@ -5,7 +5,7 @@ use OCP\AppFramework\Db\QBMapper; use OCP\IDBConnection; -/** @template-extends QBMapper */ +/** @template-extends RowCellMapperSuper */ class RowCellTextMapper extends RowCellMapperSuper implements IRowCellMapper { protected string $table = 'tables_row_cells_text'; diff --git a/lib/Db/RowSleeve.php b/lib/Db/RowSleeve.php index 2cbffd70a..24bdc5c22 100644 --- a/lib/Db/RowSleeve.php +++ b/lib/Db/RowSleeve.php @@ -8,7 +8,7 @@ /** * @psalm-suppress PropertyNotSetInConstructor - * @method getTableId(): string + * @method getTableId(): ?int * @method setTableId(int $columnId) * @method getCreatedBy(): string * @method setCreatedBy(string $createdBy) From 52337ee7702f87dad6f057c6c35ec0e7904a85c4 Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Fri, 8 Dec 2023 13:58:06 +0100 Subject: [PATCH 06/73] WIP try to adjust types to pass the psalm tests Signed-off-by: Florian Steffens --- lib/Db/Row2Mapper.php | 9 +++++---- lib/Db/RowCellMapperSuper.php | 12 +++++++++--- lib/Db/RowCellNumberMapper.php | 5 +++++ lib/Db/RowSleeveMapper.php | 2 +- 4 files changed, 20 insertions(+), 8 deletions(-) diff --git a/lib/Db/Row2Mapper.php b/lib/Db/Row2Mapper.php index a1f81204b..f631cdc73 100644 --- a/lib/Db/Row2Mapper.php +++ b/lib/Db/Row2Mapper.php @@ -446,7 +446,7 @@ private function createNewRowSleeve(int $tableId): RowSleeve { * Updates the last_edit_by and last_edit_at data * optional adds the created_by and created_at data * - * @param RowSleeve|IRowCell $entity + * @param RowSleeve|RowCellSuper $entity * @param bool $setCreate * @return void */ @@ -467,7 +467,7 @@ private function updateMetaData($entity, bool $setCreate = false): void { */ private function insertCell(int $rowId, int $columnId, string $value): void { $cellClassName = 'OCA\Tables\Db\RowCell'.ucfirst($this->columns[$columnId]->getType()); - /** @var IRowCell $cell */ + /** @var RowCellSuper $cell */ $cell = new $cellClassName(); $cell->setRowIdWrapper($rowId); @@ -493,11 +493,11 @@ private function insertCell(int $rowId, int $columnId, string $value): void { } /** - * @param IRowCell $cell + * @param RowCellSuper $cell * @param IRowCellMapper $mapper * @param mixed $value the value should be parsed to the correct format within the row service */ - private function updateCell(IRowCell $cell, IRowCellMapper $mapper, $value): void { + private function updateCell(RowCellSuper $cell, IRowCellMapper $mapper, $value): void { $cell->setValueWrapper($value); $this->updateMetaData($cell); $mapper->updateWrapper($cell); @@ -537,6 +537,7 @@ private function setColumns(array $columns): void { /** * @throws InternalError + * @return mixed */ private function formatValue(Column $column, string $value) { $cellMapperClassName = 'OCA\Tables\Db\RowCell'.ucfirst($column->getType()).'Mapper'; diff --git a/lib/Db/RowCellMapperSuper.php b/lib/Db/RowCellMapperSuper.php index afa964b1a..63af1dabd 100644 --- a/lib/Db/RowCellMapperSuper.php +++ b/lib/Db/RowCellMapperSuper.php @@ -8,6 +8,7 @@ use OCP\DB\Exception; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; +use phpDocumentor\Reflection\Types\ClassString; /** * @template-extends QBMapper @@ -15,10 +16,15 @@ */ class RowCellMapperSuper extends QBMapper implements IRowCellMapper { - public function __construct(IDBConnection $db, string $table, string $class) { + public function __construct(IDBConnection $db, string $table, ClassString $class) { parent::__construct($db, $table, $class); } + /** + * @param Column $column + * @param mixed $value + * @return mixed + */ public function parseValueOutgoing(Column $column, $value) { return $value; } @@ -26,7 +32,7 @@ public function parseValueOutgoing(Column $column, $value) { /** * @throws Exception */ - public function deleteAllForRow(int $rowId) { + public function deleteAllForRow(int $rowId): void { $qb = $this->db->getQueryBuilder(); $qb->delete($this->tableName) ->where( @@ -64,7 +70,7 @@ public function find(int $id): RowCellSuper { } /** - * @psalm-param T $cell + * @psalm-param RowCellSuper $cell * @throws Exception */ public function updateWrapper(RowCellSuper $cell): RowCellSuper { diff --git a/lib/Db/RowCellNumberMapper.php b/lib/Db/RowCellNumberMapper.php index a5216f5b0..75c06c2ae 100644 --- a/lib/Db/RowCellNumberMapper.php +++ b/lib/Db/RowCellNumberMapper.php @@ -13,6 +13,11 @@ public function __construct(IDBConnection $db) { parent::__construct($db, $this->table, RowCellNumber::class); } + /** + * @param Column $column + * @param $value + * @return float|int|null + */ public function parseValueOutgoing(Column $column, $value) { if($value === '') { return null; diff --git a/lib/Db/RowSleeveMapper.php b/lib/Db/RowSleeveMapper.php index 4bc486e25..23bcb741d 100644 --- a/lib/Db/RowSleeveMapper.php +++ b/lib/Db/RowSleeveMapper.php @@ -9,7 +9,7 @@ use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; -/** @template-extends QBMapper */ +/** @template-extends QBMapper */ class RowSleeveMapper extends QBMapper { protected string $table = 'tables_row_sleeves'; From 1a45682129bf45c7ffa1ac9224fae6cd821d0d0a Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Fri, 8 Dec 2023 16:24:27 +0100 Subject: [PATCH 07/73] WIP try to adjust types to pass the psalm tests - part 2 - just one error left Signed-off-by: Florian Steffens --- lib/Db/Row2Mapper.php | 1 - lib/Db/RowCellMapperSuper.php | 3 +-- lib/Db/RowCellNumber.php | 1 + lib/Db/RowCellSuper.php | 2 ++ lib/Db/RowCellText.php | 1 + 5 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/Db/Row2Mapper.php b/lib/Db/Row2Mapper.php index f631cdc73..58b726adf 100644 --- a/lib/Db/Row2Mapper.php +++ b/lib/Db/Row2Mapper.php @@ -7,7 +7,6 @@ use OCA\Tables\Errors\NotFoundError; use OCA\Tables\Helper\UserHelper; use OCP\AppFramework\Db\DoesNotExistException; -use OCP\AppFramework\Db\Entity; use OCP\AppFramework\Db\MultipleObjectsReturnedException; use OCP\AppFramework\Db\QBMapper; use OCP\DB\Exception; diff --git a/lib/Db/RowCellMapperSuper.php b/lib/Db/RowCellMapperSuper.php index 63af1dabd..6dd61625b 100644 --- a/lib/Db/RowCellMapperSuper.php +++ b/lib/Db/RowCellMapperSuper.php @@ -8,7 +8,6 @@ use OCP\DB\Exception; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; -use phpDocumentor\Reflection\Types\ClassString; /** * @template-extends QBMapper @@ -16,7 +15,7 @@ */ class RowCellMapperSuper extends QBMapper implements IRowCellMapper { - public function __construct(IDBConnection $db, string $table, ClassString $class) { + public function __construct(IDBConnection $db, string $table, string $class) { parent::__construct($db, $table, $class); } diff --git a/lib/Db/RowCellNumber.php b/lib/Db/RowCellNumber.php index 2e36071a7..c1d33f7a6 100644 --- a/lib/Db/RowCellNumber.php +++ b/lib/Db/RowCellNumber.php @@ -2,6 +2,7 @@ namespace OCA\Tables\Db; +/** @template-extends RowCellSuper */ class RowCellNumber extends RowCellSuper { protected ?float $value = null; diff --git a/lib/Db/RowCellSuper.php b/lib/Db/RowCellSuper.php index 0a7ba61f4..b1932576d 100644 --- a/lib/Db/RowCellSuper.php +++ b/lib/Db/RowCellSuper.php @@ -5,8 +5,10 @@ use JsonSerializable; use OCP\AppFramework\Db\Entity; +use OCP\AppFramework\Db\QBMapper; /** + * @template T of Entity * @psalm-suppress PropertyNotSetInConstructor * @method getColumnId(): string * @method setColumnId(int $columnId) diff --git a/lib/Db/RowCellText.php b/lib/Db/RowCellText.php index 60ec7cf16..0650d7d8f 100644 --- a/lib/Db/RowCellText.php +++ b/lib/Db/RowCellText.php @@ -2,6 +2,7 @@ namespace OCA\Tables\Db; +/** @template-extends RowCellSuper */ class RowCellText extends RowCellSuper { protected ?string $value = null; From cf9fd4585695cee54e68a0658328ab576c26570f Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Fri, 15 Dec 2023 13:50:47 +0100 Subject: [PATCH 08/73] fix: add the id to the new rows Signed-off-by: Florian Steffens --- lib/Db/Row2Mapper.php | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/Db/Row2Mapper.php b/lib/Db/Row2Mapper.php index 58b726adf..8b646c8c7 100644 --- a/lib/Db/Row2Mapper.php +++ b/lib/Db/Row2Mapper.php @@ -335,6 +335,7 @@ private function parseEntities(IResult $result): array { $rowId = $rowData['row_id']; if (!isset($rows[$rowId])) { $rows[$rowId] = new Row2(); + $rows[$rowId]->setId($rowId); } /* @var array $rowData */ $this->parseModel($rowData, $rows[$rowId]); @@ -425,7 +426,7 @@ public function update(Row2 $row, array $columns): Row2 { // write all changed cells to its db-table foreach ($row->getChangedCells() as $cell) { - $this->insertOrUpdateCell($sleeve->getId(), $cell['columnId'], $cell['value']); + $this->insertOrUpdateCell($sleeve->getId(), $cell['columnId'], $cell['value']); } return $row; @@ -559,13 +560,13 @@ private function formatValue(Column $column, string $value) { private function parseModel(array $data, ?Row2 &$row = null): Row2 { if (!$row) { $row = new Row2(); - $row->setId($data['row_id']); - $row->setTableId($data['table_id']); - $row->setCreatedBy($data['created_by']); - $row->setCreatedAt($data['created_at']); - $row->setLastEditBy($data['last_edit_by']); - $row->setLastEditAt($data['last_edit_at']); } + $row->setId($data['row_id']); + $row->setTableId($data['table_id']); + $row->setCreatedBy($data['created_by']); + $row->setCreatedAt($data['created_at']); + $row->setLastEditBy($data['last_edit_by']); + $row->setLastEditAt($data['last_edit_at']); $row->addCell($data['column_id'], $this->formatValue($this->columns[$data['column_id']], $data['value'])); return $row; } From abf262ce917c38bc679002b49e18e572b87f9495 Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Fri, 15 Dec 2023 13:50:59 +0100 Subject: [PATCH 09/73] code cleanup Signed-off-by: Florian Steffens --- lib/Db/IRowCell.php | 2 -- lib/Db/RowCellMapperSuper.php | 2 +- lib/Db/RowCellNumberMapper.php | 1 - lib/Db/RowCellSuper.php | 1 - lib/Db/RowCellTextMapper.php | 1 - lib/Service/RowService.php | 6 +++--- 6 files changed, 4 insertions(+), 9 deletions(-) diff --git a/lib/Db/IRowCell.php b/lib/Db/IRowCell.php index 2524b3bc4..b3b2d4906 100644 --- a/lib/Db/IRowCell.php +++ b/lib/Db/IRowCell.php @@ -2,8 +2,6 @@ namespace OCA\Tables\Db; -use OCP\AppFramework\Db\Entity; - interface IRowCell { public function setRowIdWrapper(int $rowId); diff --git a/lib/Db/RowCellMapperSuper.php b/lib/Db/RowCellMapperSuper.php index 6dd61625b..76ed70bc4 100644 --- a/lib/Db/RowCellMapperSuper.php +++ b/lib/Db/RowCellMapperSuper.php @@ -72,7 +72,7 @@ public function find(int $id): RowCellSuper { * @psalm-param RowCellSuper $cell * @throws Exception */ - public function updateWrapper(RowCellSuper $cell): RowCellSuper { + public function updateWrapper(RowCellSuper $cell): RowCellSuper { // TODO is this possible? $cell = $this->update($cell); return $cell; diff --git a/lib/Db/RowCellNumberMapper.php b/lib/Db/RowCellNumberMapper.php index 75c06c2ae..1cf84da34 100644 --- a/lib/Db/RowCellNumberMapper.php +++ b/lib/Db/RowCellNumberMapper.php @@ -2,7 +2,6 @@ namespace OCA\Tables\Db; -use OCP\AppFramework\Db\QBMapper; use OCP\IDBConnection; /** @template-extends RowCellMapperSuper */ diff --git a/lib/Db/RowCellSuper.php b/lib/Db/RowCellSuper.php index b1932576d..87a232b3e 100644 --- a/lib/Db/RowCellSuper.php +++ b/lib/Db/RowCellSuper.php @@ -5,7 +5,6 @@ use JsonSerializable; use OCP\AppFramework\Db\Entity; -use OCP\AppFramework\Db\QBMapper; /** * @template T of Entity diff --git a/lib/Db/RowCellTextMapper.php b/lib/Db/RowCellTextMapper.php index f84a43392..cb3ce1d43 100644 --- a/lib/Db/RowCellTextMapper.php +++ b/lib/Db/RowCellTextMapper.php @@ -2,7 +2,6 @@ namespace OCA\Tables\Db; -use OCP\AppFramework\Db\QBMapper; use OCP\IDBConnection; /** @template-extends RowCellMapperSuper */ diff --git a/lib/Service/RowService.php b/lib/Service/RowService.php index 63ae25a52..3bc23dd31 100644 --- a/lib/Service/RowService.php +++ b/lib/Service/RowService.php @@ -55,7 +55,7 @@ public function findAllByTable(int $tableId, string $userId, ?int $limit = null, if ($this->permissionsService->canReadRowsByElementId($tableId, 'table', $userId)) { return $this->row2Mapper->findAll($this->columnMapper->findAllByTable($tableId), $limit, $offset, null, null, $userId); - // return $this->mapper->findAllByTable($tableId, $limit, $offset); + // return $this->mapper->findAllByTable($tableId, $limit, $offset); } else { throw new PermissionError('no read access to table id = '.$tableId); } @@ -84,7 +84,7 @@ public function findAllByView(int $viewId, string $userId, ?int $limit = null, ? $columns = $this->columnMapper->find($columnsArray); return $this->row2Mapper->findAll($columns, $limit, $offset, $view->getFilterArray(), $view->getSortArray(), $userId); - // return $this->mapper->findAllByView($this->viewMapper->find($viewId), $userId, $limit, $offset); + // return $this->mapper->findAllByView($this->viewMapper->find($viewId), $userId, $limit, $offset); } else { throw new PermissionError('no read access to view id = '.$viewId); } @@ -132,7 +132,7 @@ public function find(int $id): Row { * @throws Exception * @throws InternalError */ - public function create(?int $tableId, ?int $viewId, array $data):Row { + public function create(?int $tableId, ?int $viewId, array $data): Row2 { if ($this->userId === null || $this->userId === '') { $e = new \Exception('No user id in context, but needed.'); $this->logger->error($e->getMessage(), ['exception' => $e]); From 420da4cfd98a33a747b86b5b07852b60347a870f Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Fri, 15 Dec 2023 16:19:13 +0100 Subject: [PATCH 10/73] remove data from db if a column gets deleted Signed-off-by: Florian Steffens --- lib/Db/RowCellMapperSuper.php | 12 ++++++++++++ lib/Service/ColumnService.php | 4 ++-- lib/Service/RowService.php | 33 +++++++++------------------------ 3 files changed, 23 insertions(+), 26 deletions(-) diff --git a/lib/Db/RowCellMapperSuper.php b/lib/Db/RowCellMapperSuper.php index 76ed70bc4..52a6490d9 100644 --- a/lib/Db/RowCellMapperSuper.php +++ b/lib/Db/RowCellMapperSuper.php @@ -40,6 +40,18 @@ public function deleteAllForRow(int $rowId): void { $qb->executeStatement(); } + /** + * @throws Exception + */ + public function deleteAllForColumn(int $columnId): void { + $qb = $this->db->getQueryBuilder(); + $qb->delete($this->tableName) + ->where( + $qb->expr()->eq('column_id', $qb->createNamedParameter($columnId, IQueryBuilder::PARAM_INT)) + ); + $qb->executeStatement(); + } + /** * @throws MultipleObjectsReturnedException * @throws DoesNotExistException diff --git a/lib/Service/ColumnService.php b/lib/Service/ColumnService.php index 22c3c23a4..be8c03e73 100644 --- a/lib/Service/ColumnService.php +++ b/lib/Service/ColumnService.php @@ -451,8 +451,8 @@ public function delete(int $id, bool $skipRowCleanup = false, ?string $userId = if (!$skipRowCleanup) { try { - $this->rowService->deleteColumnDataFromRows($id); - } catch (PermissionError|\OCP\DB\Exception $e) { + $this->rowService->deleteColumnDataFromRows($item); + } catch (InternalError $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); } diff --git a/lib/Service/RowService.php b/lib/Service/RowService.php index 3bc23dd31..a96939589 100644 --- a/lib/Service/RowService.php +++ b/lib/Service/RowService.php @@ -449,32 +449,17 @@ public function deleteAllByTable(int $tableId, ?string $userId = null): int { } /** - * @param int $columnId + * This deletes all data for a column, eg if the columns gets removed * - * @throws PermissionError - * @throws Exception + * >>> SECURITY <<< + * We do not check if you are allowed to remove this data. That has to be done before! + * Why? Mostly this check will have be run before and we can pass this here due to performance reasons. + * + * @param Column $column + * @throws InternalError */ - public function deleteColumnDataFromRows(int $columnId):void { - $rows = $this->mapper->findAllWithColumn($columnId); - - // security - if (count($rows) > 0) { - if (!$this->permissionsService->canUpdateRowsByTableId($rows[0]->getTableId())) { - throw new PermissionError('update row id = '.$rows[0]->getId().' within '.__FUNCTION__.' is not allowed.'); - } - } - - foreach ($rows as $row) { - /* @var $row Row */ - $data = $row->getDataArray(); - foreach ($data as $key => $col) { - if ($col['columnId'] == $columnId) { - unset($data[$key]); - } - } - $row->setDataArray($data); - $this->mapper->update($row); - } + public function deleteColumnDataFromRows(Column $column):void { + $this->row2Mapper->deleteDataForColumn($column); } /** From 3346054d34c9cfe862c497142bb8df6cdd5df853 Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Fri, 15 Dec 2023 16:20:10 +0100 Subject: [PATCH 11/73] fix: even get results if the rows don't have data Signed-off-by: Florian Steffens --- lib/Db/Row2Mapper.php | 81 ++++++++++++++++++-------------------- lib/Db/RowSleeveMapper.php | 13 ++++++ 2 files changed, 51 insertions(+), 43 deletions(-) diff --git a/lib/Db/Row2Mapper.php b/lib/Db/Row2Mapper.php index 8b646c8c7..6c3ac96bb 100644 --- a/lib/Db/Row2Mapper.php +++ b/lib/Db/Row2Mapper.php @@ -184,7 +184,14 @@ private function getRows(array $rowIds, array $columnIds): array { throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage(), ); } - return $this->parseEntities($result); + try { + $sleeves = $this->rowSleeveMapper->findMultiple($rowIds); + } catch (Exception $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + } + + return $this->parseEntities($result, $sleeves); } /** @@ -321,24 +328,31 @@ private function resolveSearchValue(string $magicValue, string $userId): string /** * @param IResult $result + * @param RowSleeve[] $sleeves * @return Row2[] * @throws InternalError */ - private function parseEntities(IResult $result): array { + private function parseEntities(IResult $result, array $sleeves): array { $data = $result->fetchAll(); $rows = []; + foreach ($sleeves as $sleeve) { + $rows[$sleeve->getId()] = new Row2(); + $rows[$sleeve->getId()]->setId($sleeve->getId()); + $rows[$sleeve->getId()]->setCreatedBy($sleeve->getCreatedBy()); + $rows[$sleeve->getId()]->setCreatedAt($sleeve->getCreatedAt()); + $rows[$sleeve->getId()]->setLastEditBy($sleeve->getLastEditBy()); + $rows[$sleeve->getId()]->setLastEditAt($sleeve->getLastEditAt()); + $rows[$sleeve->getId()]->setTableId($sleeve->getTableId()); + } + foreach ($data as $rowData) { - if (!isset($rowData['row_id'])) { + if (!isset($rowData['row_id']) || !isset($rows[$rowData['row_id']])) { break; } - $rowId = $rowData['row_id']; - if (!isset($rows[$rowId])) { - $rows[$rowId] = new Row2(); - $rows[$rowId]->setId($rowId); - } + /* @var array $rowData */ - $this->parseModel($rowData, $rows[$rowId]); + $rows[$rowData['row_id']]->addCell($rowData['column_id'], $this->formatValue($this->columns[$rowData['column_id']], $rowData['value'])); } // format an array without keys @@ -356,25 +370,6 @@ public function isRowInViewPresent(int $rowId, View $view, string $userId): bool return in_array($rowId, $this->getWantedRowIds($userId, $view->getFilterArray())); } - /** - * @param IResult $result - * @return Row2 - * @throws InternalError - */ - private function parseEntity(IResult $result): Row2 { - $data = $result->fetchAll(); - - if(count($data) === 0) { - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': result was empty, expected one row'); - } - if(count($data) > 1) { - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': found more than one expected result'); - } - - return $this->parseModel($data[0]); - } - - /** * @param Row2 $row * @param Column[] $columns @@ -552,23 +547,23 @@ private function formatValue(Column $column, string $value) { } /** - * @param array $data - * @param Row2|null $row - * @return Row2 * @throws InternalError */ - private function parseModel(array $data, ?Row2 &$row = null): Row2 { - if (!$row) { - $row = new Row2(); - } - $row->setId($data['row_id']); - $row->setTableId($data['table_id']); - $row->setCreatedBy($data['created_by']); - $row->setCreatedAt($data['created_at']); - $row->setLastEditBy($data['last_edit_by']); - $row->setLastEditAt($data['last_edit_at']); - $row->addCell($data['column_id'], $this->formatValue($this->columns[$data['column_id']], $data['value'])); - return $row; + public function deleteDataForColumn(Column $column): void { + $cellMapperClassName = 'OCA\Tables\Db\RowCell' . ucfirst($column->getType()) . 'Mapper'; + /** @var RowCellMapperSuper $cellMapper */ + try { + $cellMapper = Server::get($cellMapperClassName); + } catch (NotFoundExceptionInterface|ContainerExceptionInterface $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage()); + } + try { + $cellMapper->deleteAllForColumn($column->getId()); + } catch (Exception $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + } } } diff --git a/lib/Db/RowSleeveMapper.php b/lib/Db/RowSleeveMapper.php index 23bcb741d..693f9c79b 100644 --- a/lib/Db/RowSleeveMapper.php +++ b/lib/Db/RowSleeveMapper.php @@ -30,6 +30,19 @@ public function find(int $id): RowSleeve { return $this->findEntity($qb); } + /** + * @param int[] $ids + * @return RowSleeve[] + * @throws Exception + */ + public function findMultiple(array $ids): array { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->table) + ->where($qb->expr()->in('id', $qb->createNamedParameter($ids, IQueryBuilder::PARAM_INT_ARRAY))); + return $this->findEntities($qb); + } + /** * @param int $sleeveId * @throws Exception From f58f352c4387355b3c3145c3d43154c8b745d9d1 Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Mon, 18 Dec 2023 16:00:47 +0100 Subject: [PATCH 12/73] adjust and fix types to make psalm happy Signed-off-by: Florian Steffens --- lib/Db/IRowCellMapper.php | 36 ---------------------------------- lib/Db/Row2Mapper.php | 7 ++++--- lib/Db/RowCellMapperSuper.php | 5 +++-- lib/Db/RowCellNumberMapper.php | 2 +- lib/Db/RowCellTextMapper.php | 2 +- 5 files changed, 9 insertions(+), 43 deletions(-) delete mode 100644 lib/Db/IRowCellMapper.php diff --git a/lib/Db/IRowCellMapper.php b/lib/Db/IRowCellMapper.php deleted file mode 100644 index b37076ce2..000000000 --- a/lib/Db/IRowCellMapper.php +++ /dev/null @@ -1,36 +0,0 @@ -setValueWrapper($value); $this->updateMetaData($cell); $mapper->updateWrapper($cell); @@ -503,7 +504,7 @@ private function updateCell(RowCellSuper $cell, IRowCellMapper $mapper, $value): */ private function insertOrUpdateCell(int $rowId, int $columnId, string $value): void { $cellMapperClassName = 'OCA\Tables\Db\RowCell'.ucfirst($this->columns[$columnId]->getType()).'Mapper'; - /** @var IRowCellMapper $cellMapper */ + /** @var RowCellMapperSuper $cellMapper */ try { $cellMapper = Server::get($cellMapperClassName); } catch (NotFoundExceptionInterface|ContainerExceptionInterface $e) { diff --git a/lib/Db/RowCellMapperSuper.php b/lib/Db/RowCellMapperSuper.php index 52a6490d9..7b79714cb 100644 --- a/lib/Db/RowCellMapperSuper.php +++ b/lib/Db/RowCellMapperSuper.php @@ -13,7 +13,7 @@ * @template-extends QBMapper * @template T of RowCellSuper */ -class RowCellMapperSuper extends QBMapper implements IRowCellMapper { +class RowCellMapperSuper extends QBMapper { public function __construct(IDBConnection $db, string $table, string $class) { parent::__construct($db, $table, $class); @@ -81,7 +81,8 @@ public function find(int $id): RowCellSuper { } /** - * @psalm-param RowCellSuper $cell + * @psalm-param T $cell + * @psalm-return T * @throws Exception */ public function updateWrapper(RowCellSuper $cell): RowCellSuper { diff --git a/lib/Db/RowCellNumberMapper.php b/lib/Db/RowCellNumberMapper.php index 1cf84da34..fc9e54abc 100644 --- a/lib/Db/RowCellNumberMapper.php +++ b/lib/Db/RowCellNumberMapper.php @@ -5,7 +5,7 @@ use OCP\IDBConnection; /** @template-extends RowCellMapperSuper */ -class RowCellNumberMapper extends RowCellMapperSuper implements IRowCellMapper { +class RowCellNumberMapper extends RowCellMapperSuper { protected string $table = 'tables_row_cells_number'; public function __construct(IDBConnection $db) { diff --git a/lib/Db/RowCellTextMapper.php b/lib/Db/RowCellTextMapper.php index cb3ce1d43..d016717d5 100644 --- a/lib/Db/RowCellTextMapper.php +++ b/lib/Db/RowCellTextMapper.php @@ -5,7 +5,7 @@ use OCP\IDBConnection; /** @template-extends RowCellMapperSuper */ -class RowCellTextMapper extends RowCellMapperSuper implements IRowCellMapper { +class RowCellTextMapper extends RowCellMapperSuper { protected string $table = 'tables_row_cells_text'; public function __construct(IDBConnection $db) { From a61e2f123c71a3c28167bb23b386c9501586583d Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Tue, 19 Dec 2023 09:28:14 +0100 Subject: [PATCH 13/73] code clean up - fix psalm errors - adjust types - clean up code Signed-off-by: Florian Steffens --- lib/Api/V1Api.php | 2 +- lib/Controller/Api1Controller.php | 4 +-- lib/Db/ColumnMapper.php | 6 ++-- lib/Db/Row2.php | 7 +++++ lib/Db/Row2Mapper.php | 46 +++++++++++++++++++++++++++++- lib/Db/RowCellMapperSuper.php | 7 ++--- lib/Db/RowMapper.php | 4 +-- lib/Db/View.php | 6 ++-- lib/Service/PermissionsService.php | 7 +++-- lib/Service/RowService.php | 46 ++++++++++++++++++++++++------ lib/Service/ViewService.php | 24 ++++++++++++---- 11 files changed, 128 insertions(+), 31 deletions(-) diff --git a/lib/Api/V1Api.php b/lib/Api/V1Api.php index 1b68f37c8..2c9a7b8c4 100644 --- a/lib/Api/V1Api.php +++ b/lib/Api/V1Api.php @@ -58,7 +58,7 @@ public function getData(int $nodeId, ?int $limit, ?int $offset, ?string $userId, // now add the rows foreach ($rows as $row) { - $rowData = $row->getDataArray(); + $rowData = $row->getData(); $line = []; foreach ($columns as $column) { $value = ''; diff --git a/lib/Controller/Api1Controller.php b/lib/Controller/Api1Controller.php index 13d1f7bff..a9d9a8566 100644 --- a/lib/Controller/Api1Controller.php +++ b/lib/Controller/Api1Controller.php @@ -937,7 +937,7 @@ public function indexTableRowsSimple(int $tableId, ?int $limit, ?int $offset): D */ public function indexTableRows(int $tableId, ?int $limit, ?int $offset): DataResponse { try { - return new DataResponse($this->rowService->findAllByTable($tableId, $this->userId, $limit, $offset)); + return new DataResponse($this->rowService->formatRows($this->rowService->findAllByTable($tableId, $this->userId, $limit, $offset))); } catch (PermissionError $e) { $this->logger->warning('A permission error occurred: ' . $e->getMessage()); $message = ['message' => $e->getMessage()]; @@ -967,7 +967,7 @@ public function indexTableRows(int $tableId, ?int $limit, ?int $offset): DataRes */ public function indexViewRows(int $viewId, ?int $limit, ?int $offset): DataResponse { try { - return new DataResponse($this->rowService->findAllByView($viewId, $this->userId, $limit, $offset)); + return new DataResponse($this->rowService->formatRows($this->rowService->findAllByView($viewId, $this->userId, $limit, $offset))); } catch (PermissionError $e) { $this->logger->warning('A permission error occurred: ' . $e->getMessage()); $message = ['message' => $e->getMessage()]; diff --git a/lib/Db/ColumnMapper.php b/lib/Db/ColumnMapper.php index e61fb3528..0f471a2f6 100644 --- a/lib/Db/ColumnMapper.php +++ b/lib/Db/ColumnMapper.php @@ -57,15 +57,15 @@ public function findMultiple(array $ids): array { } /** - * @param integer $tableID + * @param integer $tableId * @return array * @throws Exception */ - public function findAllByTable(int $tableID): array { + public function findAllByTable(int $tableId): array { $qb = $this->db->getQueryBuilder(); $qb->select('*') ->from($this->table) - ->where($qb->expr()->eq('table_id', $qb->createNamedParameter($tableID))); + ->where($qb->expr()->eq('table_id', $qb->createNamedParameter($tableId))); return $this->findEntities($qb); } diff --git a/lib/Db/Row2.php b/lib/Db/Row2.php index 6f945a19c..e95f08046 100644 --- a/lib/Db/Row2.php +++ b/lib/Db/Row2.php @@ -3,7 +3,11 @@ namespace OCA\Tables\Db; use JsonSerializable; +use OCA\Tables\ResponseDefinitions; +/** + * @psalm-import-type TablesRow from ResponseDefinitions + */ class Row2 implements JsonSerializable { private ?int $id = null; private ?int $tableId = null; @@ -107,6 +111,9 @@ public function removeCell(int $columnId): void { // TODO } + /** + * @psalm-return TablesRow + */ public function jsonSerialize(): array { return [ 'id' => $this->id, diff --git a/lib/Db/Row2Mapper.php b/lib/Db/Row2Mapper.php index f37fc10e0..8191e49c9 100644 --- a/lib/Db/Row2Mapper.php +++ b/lib/Db/Row2Mapper.php @@ -213,7 +213,7 @@ private function addFilterToQuery(IQueryBuilder &$qb, array $filters, string $us private function replaceMagicValues(array &$filters, string $userId): void { foreach ($filters as &$filterGroup) { foreach ($filterGroup as &$filter) { - if(str_starts_with($filter['value'], '@')) { + if(substr($filter['value'], 0, 1) === '@') { $filter['value'] = $this->resolveSearchValue($filter['value'], $userId); } } @@ -567,4 +567,48 @@ public function deleteDataForColumn(Column $column): void { } } + /** + * @param int $tableId + * @param Column[] $columns + * @return void + */ + public function deleteAllForTable(int $tableId, array $columns): void { + foreach ($columns as $column) { + try { + $this->deleteDataForColumn($column); + } catch (InternalError $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + } + } + try { + $this->rowSleeveMapper->deleteAllForTable($tableId); + } catch (Exception $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + } + } + + public function countRowsForTable(int $tableId): int { + return $this->rowSleeveMapper->countRows($tableId); + } + + /** + * @param View $view + * @param string $userId + * @param Column[] $columns + * @return int + */ + public function countRowsForView(View $view, string $userId, array $columns): int { + $this->setColumns($columns); + + $filter = $view->getFilterArray(); + try { + $rowIds = $this->getWantedRowIds($userId, $filter); + } catch (InternalError $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + $rowIds = []; + } + return count($rowIds); + } + + } diff --git a/lib/Db/RowCellMapperSuper.php b/lib/Db/RowCellMapperSuper.php index 7b79714cb..cf439acbe 100644 --- a/lib/Db/RowCellMapperSuper.php +++ b/lib/Db/RowCellMapperSuper.php @@ -63,8 +63,7 @@ public function findByRowAndColumn(int $rowId, int $columnId): RowCellSuper { ->from($this->tableName) ->where($qb->expr()->eq('row_id', $qb->createNamedParameter($rowId, IQueryBuilder::PARAM_INT))) ->andWhere($qb->expr()->eq('column_id', $qb->createNamedParameter($columnId, IQueryBuilder::PARAM_INT))); - $item = $this->findEntity($qb); - return $item; + return $this->findEntity($qb); } /** @@ -86,9 +85,7 @@ public function find(int $id): RowCellSuper { * @throws Exception */ public function updateWrapper(RowCellSuper $cell): RowCellSuper { - // TODO is this possible? - $cell = $this->update($cell); - return $cell; + return $this->update($cell); } } diff --git a/lib/Db/RowMapper.php b/lib/Db/RowMapper.php index 51cb35ae1..6c88752d9 100644 --- a/lib/Db/RowMapper.php +++ b/lib/Db/RowMapper.php @@ -234,8 +234,8 @@ private function addFilterToQuery(IQueryBuilder $qb, View $view, array $neededCo foreach ($filterGroup as &$filter) { $filter['columnType'] = $neededColumnTypes[$filter['columnId']]; // TODO move resolution for magic fields to service layer - if(str_starts_with($filter['value'], '@')) { - $filter['value'] = $this->resolveSearchValue($filter['value'], $userId); + if(str_starts_with((string) $filter['value'], '@')) { + $filter['value'] = $this->resolveSearchValue((string) $filter['value'], $userId); } } } diff --git a/lib/Db/View.php b/lib/Db/View.php index 8eccd6fbf..3e72d6240 100644 --- a/lib/Db/View.php +++ b/lib/Db/View.php @@ -11,6 +11,8 @@ * * @psalm-import-type TablesView from ResponseDefinitions * + * @method getId(): int + * @method setId(int $id) * @method getTitle(): string * @method setTitle(string $title) * @method getTableId(): int @@ -75,7 +77,7 @@ public function getColumnsArray(): array { /** * @psalm-suppress MismatchingDocblockReturnType - * @return array{array-key, array{columnId: int, mode: 'ASC'|'DESC'}}|null + * @return list */ public function getSortArray(): array { return $this->getArray($this->getSort()); @@ -83,7 +85,7 @@ public function getSortArray(): array { /** * @psalm-suppress MismatchingDocblockReturnType - * @return array|null + * @return list> */ public function getFilterArray():array { return $this->getArray($this->getFilter()); diff --git a/lib/Service/PermissionsService.php b/lib/Service/PermissionsService.php index 36c1fa054..8b3c851de 100644 --- a/lib/Service/PermissionsService.php +++ b/lib/Service/PermissionsService.php @@ -280,11 +280,14 @@ public function canDeleteRowsByViewId(int $viewId, ?string $userId = null): bool } /** - * @param int $tableId + * @param int|null $tableId * @param string|null $userId * @return bool */ - public function canDeleteRowsByTableId(int $tableId, ?string $userId = null): bool { + public function canDeleteRowsByTableId(int $tableId = null, ?string $userId = null): bool { + if ($tableId === null) { + return false; + } return $this->checkPermissionById($tableId, 'table', 'delete', $userId); } diff --git a/lib/Service/RowService.php b/lib/Service/RowService.php index a96939589..bbda78f37 100644 --- a/lib/Service/RowService.php +++ b/lib/Service/RowService.php @@ -8,12 +8,14 @@ use OCA\Tables\Db\Row2; use OCA\Tables\Db\Row2Mapper; use OCA\Tables\Db\RowMapper; +use OCA\Tables\Db\Table; use OCA\Tables\Db\TableMapper; use OCA\Tables\Db\View; use OCA\Tables\Db\ViewMapper; use OCA\Tables\Errors\InternalError; use OCA\Tables\Errors\NotFoundError; use OCA\Tables\Errors\PermissionError; +use OCA\Tables\ResponseDefinitions; use OCA\Tables\Service\ColumnTypes\IColumnTypeBusiness; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\MultipleObjectsReturnedException; @@ -23,6 +25,9 @@ use Psr\Container\NotFoundExceptionInterface; use Psr\Log\LoggerInterface; +/** + * @psalm-import-type TablesRow from ResponseDefinitions + */ class RowService extends SuperService { private RowMapper $mapper; private ColumnMapper $columnMapper; @@ -41,12 +46,20 @@ public function __construct(PermissionsService $permissionsService, LoggerInterf $this->row2Mapper = $row2Mapper; } + /** + * @param Row2[] $rows + * @psalm-return TablesRow[] + */ + public function formatRows(array $rows): array { + return array_map(fn (Row2 $row) => $row->jsonSerialize(), $rows); + } + /** * @param int $tableId * @param string $userId * @param ?int $limit * @param ?int $offset - * @return array + * @return Row2[] * @throws InternalError * @throws PermissionError */ @@ -70,7 +83,7 @@ public function findAllByTable(int $tableId, string $userId, ?int $limit = null, * @param string $userId * @param int|null $limit * @param int|null $offset - * @return array + * @return Row2[] * @throws DoesNotExistException * @throws InternalError * @throws MultipleObjectsReturnedException @@ -282,6 +295,11 @@ private function getRowById(int $rowId): Row2 { } try { + if ($this->row2Mapper->getTableIdForRow($rowId) === null) { + $e = new \Exception('No table id in row, but needed.'); + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + } $row = $this->row2Mapper->find($rowId, $this->columnMapper->findAllByTable($this->row2Mapper->getTableIdForRow($rowId))); $row->markAsLoaded(); } catch (InternalError|DoesNotExistException|MultipleObjectsReturnedException|Exception $e) { @@ -328,7 +346,9 @@ public function updateSet( if ($viewId) { // security if (!$this->permissionsService->canUpdateRowsByViewId($viewId)) { - throw new PermissionError('update row id = '.$item->getId().' is not allowed.'); + $e = new \Exception('Update row is not allowed.'); + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); } try { @@ -343,7 +363,9 @@ public function updateSet( // is row in view? if($this->row2Mapper->isRowInViewPresent($id, $view, $userId)) { - throw new PermissionError('update row id = '.$item->getId().' is not allowed.'); + $e = new \Exception('Update row is not allowed.'); + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); } // fetch all needed columns @@ -359,7 +381,9 @@ public function updateSet( // security if (!$this->permissionsService->canUpdateRowsByTableId($tableId)) { - throw new PermissionError('update row id = '.$tableId.' is not allowed.'); + $e = new \Exception('Update row is not allowed.'); + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); } try { $columns = $this->columnMapper->findAllByTable($tableId); @@ -409,7 +433,9 @@ public function delete(int $id, ?int $viewId, string $userId): Row2 { if ($viewId) { // security if (!$this->permissionsService->canDeleteRowsByViewId($viewId)) { - throw new PermissionError('update row id = '.$item->getId().' is not allowed.'); + $e = new \Exception('Update row is not allowed.'); + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); } try { $view = $this->viewMapper->find($viewId); @@ -419,12 +445,16 @@ public function delete(int $id, ?int $viewId, string $userId): Row2 { } $rowIds = $this->mapper->getRowIdsOfView($view, $userId); if(!in_array($id, $rowIds)) { - throw new PermissionError('update row id = '.$item->getId().' is not allowed.'); + $e = new \Exception('Update row is not allowed.'); + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); } } else { // security if (!$this->permissionsService->canDeleteRowsByTableId($item->getTableId())) { - throw new PermissionError('update row id = '.$item->getId().' is not allowed.'); + $e = new \Exception('Update row is not allowed.'); + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new PermissionError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); } } diff --git a/lib/Service/ViewService.php b/lib/Service/ViewService.php index 453be6b9d..5dd6297fd 100644 --- a/lib/Service/ViewService.php +++ b/lib/Service/ViewService.php @@ -432,11 +432,25 @@ public function deleteColumnDataFromViews(int $columnId, Table $table) { return $sort['columnId'] !== $columnId; }); $filteredSortingRules = array_values($filteredSortingRules); - $filteredFilters = array_filter(array_map(function (array $filterGroup) use ($columnId) { - return array_filter($filterGroup, function (array $filter) use ($columnId) { - return $filter['columnId'] !== $columnId; - }); - }, $view->getFilterArray()), fn ($filterGroup) => !empty($filterGroup)); + + $filteredFilters = array_filter( + + array_map( + function (array $filterGroup) use ($columnId) { + return array_filter( + $filterGroup, + function (array $filter) use ($columnId) { + return $filter['columnId'] !== $columnId; + } + ); + }, + $view->getFilterArray() + ), + + fn ($filterGroup) => !empty($filterGroup) + + ); + $data = [ 'columns' => json_encode(array_values(array_diff($view->getColumnsArray(), [$columnId]))), 'sort' => json_encode($filteredSortingRules), From 8be2541f540c2a51058edbe73322269d09127926 Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Tue, 19 Dec 2023 12:03:54 +0100 Subject: [PATCH 14/73] update rowService to use Row2 everywhere - adjust RowSleeveMapper.php for it Signed-off-by: Florian Steffens --- lib/Db/RowSleeveMapper.php | 40 ++++++++++++++++++++++++++++++- lib/Service/RowService.php | 48 +++++++++++++++++++++----------------- 2 files changed, 66 insertions(+), 22 deletions(-) diff --git a/lib/Db/RowSleeveMapper.php b/lib/Db/RowSleeveMapper.php index 693f9c79b..110e9359e 100644 --- a/lib/Db/RowSleeveMapper.php +++ b/lib/Db/RowSleeveMapper.php @@ -8,13 +8,16 @@ use OCP\DB\Exception; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; +use Psr\Log\LoggerInterface; /** @template-extends QBMapper */ class RowSleeveMapper extends QBMapper { protected string $table = 'tables_row_sleeves'; + protected LoggerInterface $logger; - public function __construct(IDBConnection $db) { + public function __construct(IDBConnection $db, LoggerInterface $logger) { parent::__construct($db, $this->table, RowSleeve::class); + $this->logger = $logger; } /** @@ -55,4 +58,39 @@ public function deleteById(int $sleeveId) { ); $qb->executeStatement(); } + + /** + * @param int $tableId + * @return int Effected rows + * @throws Exception + */ + public function deleteAllForTable(int $tableId): int { + $qb = $this->db->getQueryBuilder(); + $qb->delete($this->tableName) + ->where( + $qb->expr()->eq('table_id', $qb->createNamedParameter($tableId, IQueryBuilder::PARAM_INT)) + ); + return $qb->executeStatement(); + } + + /** + * @param int $tableId + * @return int + */ + public function countRows(int $tableId): int { + $qb = $this->db->getQueryBuilder(); + $qb->select($qb->func()->count('*', 'counter')); + $qb->from($this->table, 't1'); + $qb->where( + $qb->expr()->eq('table_id', $qb->createNamedParameter($tableId)) + ); + + try { + $result = $this->findOneQuery($qb); + return (int)$result['counter']; + } catch (DoesNotExistException|MultipleObjectsReturnedException|Exception $e) { + $this->logger->warning('Exception occurred: '.$e->getMessage().' Will return 0.'); + return 0; + } + } } diff --git a/lib/Service/RowService.php b/lib/Service/RowService.php index bbda78f37..a9594b7cc 100644 --- a/lib/Service/RowService.php +++ b/lib/Service/RowService.php @@ -110,28 +110,35 @@ public function findAllByView(int $viewId, string $userId, ?int $limit = null, ? /** * @param int $id - * @return Row + * @return Row2 * @throws InternalError * @throws NotFoundError * @throws PermissionError */ - public function find(int $id): Row { + public function find(int $id): Row2 { try { - $row = $this->mapper->find($id); + $columns = $this->columnMapper->findAllByTable($id); + } catch (Exception $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + } - // security - if (!$this->permissionsService->canReadRowsByElementId($row->getTableId(), 'table')) { - throw new PermissionError('PermissionError: can not read row with id '.$id); - } + try { + $row = $this->row2Mapper->find($id, $columns); + } catch (InternalError $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + } catch (NotFoundError $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new NotFoundError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + } - return $row; - } catch (DoesNotExistException $e) { - $this->logger->warning($e->getMessage()); - throw new NotFoundError($e->getMessage()); - } catch (MultipleObjectsReturnedException|Exception $e) { - $this->logger->error($e->getMessage()); - throw new InternalError($e->getMessage()); + // security + if (!$this->permissionsService->canReadRowsByElementId($row->getTableId(), 'table')) { + throw new PermissionError('PermissionError: can not read row with id '.$id); } + + return $row; } /** @@ -323,7 +330,6 @@ private function getRowById(int $rowId): Row2 { * @return Row2 * * @throws InternalError - * @throws PermissionError * @throws NotFoundError * @noinspection DuplicatedCode */ @@ -464,18 +470,19 @@ public function delete(int $id, ?int $viewId, string $userId): Row2 { /** * @param int $tableId * @param null|string $userId - * @return int * * @throws PermissionError * @throws Exception */ - public function deleteAllByTable(int $tableId, ?string $userId = null): int { + public function deleteAllByTable(int $tableId, ?string $userId = null): void { // security if (!$this->permissionsService->canDeleteRowsByTableId($tableId, $userId)) { throw new PermissionError('delete all rows for table id = '.$tableId.' is not allowed.'); } - return $this->mapper->deleteAllByTable($tableId); + $columns = $this->columnMapper->findAllByTable($tableId); + + $this->row2Mapper->deleteAllForTable($tableId, $columns); } /** @@ -500,7 +507,7 @@ public function deleteColumnDataFromRows(Column $column):void { */ public function getRowsCount(int $tableId): int { if ($this->permissionsService->canReadRowsByElementId($tableId, 'table')) { - return $this->mapper->countRows($tableId); + return $this->row2Mapper->countRowsForTable($tableId); } else { throw new PermissionError('no read access for counting to table id = '.$tableId); } @@ -511,12 +518,11 @@ public function getRowsCount(int $tableId): int { * @param string $userId * @return int * - * @throws InternalError * @throws PermissionError */ public function getViewRowsCount(View $view, string $userId): int { if ($this->permissionsService->canReadRowsByElementId($view->getId(), 'view', $userId)) { - return $this->mapper->countRowsForView($view, $userId); + return $this->row2Mapper->countRowsForView($view, $userId, $this->columnMapper->findMultiple($view->getColumnsArray())); } else { throw new PermissionError('no read access for counting to view id = '.$view->getId()); } From c8b1402d6218d787a5584aa2d273c03d3be1f95b Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Wed, 20 Dec 2023 11:14:51 +0100 Subject: [PATCH 15/73] add a helper class to define all the existing column-types Signed-off-by: Florian Steffens --- lib/Db/Row2Mapper.php | 104 +++++++++++------- lib/Db/RowCellMapperSuper.php | 24 +++- lib/Db/RowCellNumberMapper.php | 26 ++++- lib/Db/RowSleeveMapper.php | 2 +- lib/Helper/ColumnsHelper.php | 51 +++++++++ .../Version000700Date20230916000000.php | 26 +++-- lib/Service/RowService.php | 15 ++- 7 files changed, 186 insertions(+), 62 deletions(-) create mode 100644 lib/Helper/ColumnsHelper.php diff --git a/lib/Db/Row2Mapper.php b/lib/Db/Row2Mapper.php index 8191e49c9..6214888a5 100644 --- a/lib/Db/Row2Mapper.php +++ b/lib/Db/Row2Mapper.php @@ -5,6 +5,7 @@ use DateTime; use OCA\Tables\Errors\InternalError; use OCA\Tables\Errors\NotFoundError; +use OCA\Tables\Helper\ColumnsHelper; use OCA\Tables\Helper\UserHelper; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\MultipleObjectsReturnedException; @@ -28,19 +29,22 @@ class Row2Mapper { /* @var Column[] $columns */ private array $columns = []; - public function __construct(?string $userId, IDBConnection $db, LoggerInterface $logger, UserHelper $userHelper, RowSleeveMapper $rowSleeveMapper) { + private ColumnsHelper $columnsHelper; + + public function __construct(?string $userId, IDBConnection $db, LoggerInterface $logger, UserHelper $userHelper, RowSleeveMapper $rowSleeveMapper, ColumnsHelper $columnsHelper) { $this->rowSleeveMapper = $rowSleeveMapper; $this->userId = $userId; $this->db = $db; $this->logger = $logger; $this->userHelper = $userHelper; + $this->columnsHelper = $columnsHelper; } /** * @throws InternalError */ public function delete(Row2 $row): Row2 { - foreach (['text', 'number'] as $columnType) { + foreach ($this->columnsHelper->get(['name']) as $columnType) { $cellMapperClassName = 'OCA\Tables\Db\RowCell' . ucfirst($columnType) . 'Mapper'; /** @var RowCellMapperSuper $cellMapper */ try { @@ -102,18 +106,19 @@ public function getTableIdForRow(int $rowId): ?int { /** * @param string $userId + * @param int $tableId * @param array|null $filter * @param int|null $limit * @param int|null $offset * @return int[] * @throws InternalError */ - private function getWantedRowIds(string $userId, ?array $filter = null, ?int $limit = null, ?int $offset = null): array { + private function getWantedRowIds(string $userId, int $tableId, ?array $filter = null, ?int $limit = null, ?int $offset = null): array { $qb = $this->db->getQueryBuilder(); $qb->select('id') - ->from('tables_row_sleeves', 'sleeves'); - // ->where($qb->expr()->eq('table_id', $qb->createNamedParameter($tableId, IQueryBuilder::PARAM_INT))); + ->from('tables_row_sleeves', 'sleeves') + ->where($qb->expr()->eq('table_id', $qb->createNamedParameter($tableId, IQueryBuilder::PARAM_INT))); if($filter) { $this->addFilterToQuery($qb, $filter, $userId); } @@ -136,16 +141,20 @@ private function getWantedRowIds(string $userId, ?array $filter = null, ?int $li /** * @param Column[] $columns + * @param int $tableId * @param int|null $limit * @param int|null $offset + * @param array|null $filter + * @param array|null $sort + * @param string|null $userId * @return Row2[] * @throws InternalError */ - public function findAll(array $columns, int $limit = null, int $offset = null, array $filter = null, array $sort = null, string $userId = null): array { + public function findAll(array $columns, int $tableId, int $limit = null, int $offset = null, array $filter = null, array $sort = null, string $userId = null): array { $this->setColumns($columns); $columnIdsArray = array_map(fn (Column $column) => $column->getId(), $columns); - $wantedRowIdsArray = $this->getWantedRowIds($userId, $filter, $limit, $offset); + $wantedRowIdsArray = $this->getWantedRowIds($userId, $tableId, $filter, $limit, $offset); // TODO add sorting @@ -161,20 +170,24 @@ public function findAll(array $columns, int $limit = null, int $offset = null, a private function getRows(array $rowIds, array $columnIds): array { $qb = $this->db->getQueryBuilder(); - $qbText = $this->db->getQueryBuilder(); - $qbText->select('*') - ->from('tables_row_cells_text') - ->where($qb->expr()->in('column_id', $qb->createNamedParameter($columnIds, IQueryBuilder::PARAM_INT_ARRAY, ':columnIds'))) - ->andWhere($qb->expr()->in('row_id', $qb->createNamedParameter($rowIds, IQueryBuilder::PARAM_INT_ARRAY, ':rowsIds'))); - - $qbNumber = $this->db->getQueryBuilder(); - $qbNumber->select('*') - ->from('tables_row_cells_number') - ->where($qb->expr()->in('column_id', $qb->createNamedParameter($columnIds, IQueryBuilder::PARAM_INT_ARRAY, ':columnIds'))) - ->andWhere($qb->expr()->in('row_id', $qb->createNamedParameter($rowIds, IQueryBuilder::PARAM_INT_ARRAY, ':rowIds'))); + $qbSqlForColumnTypes = null; + foreach ($this->columnsHelper->get(['name']) as $columnType) { + $qbTmp = $this->db->getQueryBuilder(); + $qbTmp->select('*') + ->from('tables_row_cells_'.$columnType) + ->where($qb->expr()->in('column_id', $qb->createNamedParameter($columnIds, IQueryBuilder::PARAM_INT_ARRAY, ':columnIds'))) + ->andWhere($qb->expr()->in('row_id', $qb->createNamedParameter($rowIds, IQueryBuilder::PARAM_INT_ARRAY, ':rowsIds'))); + + if ($qbSqlForColumnTypes) { + $qbSqlForColumnTypes .= ' UNION ALL (' . $qbTmp->getSQL() . ') '; + } else { + $qbSqlForColumnTypes = '((' . $qbTmp->getSQL() . ')'; + } + } + $qbSqlForColumnTypes .= ')'; $qb->select('row_id', 'column_id', 'created_by', 'created_at', 't1.last_edit_by', 't1.last_edit_at', 'value', 'table_id') - ->from($qb->createFunction('((' . $qbText->getSQL() . ') UNION ALL (' . $qbNumber->getSQL() . '))'), 't1') + ->from($qb->createFunction($qbSqlForColumnTypes), 't1') ->innerJoin('t1', 'tables_row_sleeves', 'rowSleeve', 'rowSleeve.id = t1.row_id'); try { @@ -251,30 +264,22 @@ private function getFilter(IQueryBuilder &$qb, array $filterGroup): array { * @throws InternalError */ private function getFilterExpression(IQueryBuilder $qb, Column $column, string $operator, string $value): IQueryBuilder { - if($column->getType() === 'number' && $column->getNumberDecimals() === 0) { + /*if($column->getType() === 'number' && $column->getNumberDecimals() === 0) { $paramType = IQueryBuilder::PARAM_INT; $value = (int)$value; } elseif ($column->getType() === 'datetime') { $paramType = IQueryBuilder::PARAM_DATE; } else { $paramType = IQueryBuilder::PARAM_STR; - } + }*/ + + $paramType = $this->getColumnDbParamType($column); + $value = $this->formatValue($column, $value, 'in'); $qb2 = $this->db->getQueryBuilder(); $qb2->select('row_id'); $qb2->where($qb->expr()->eq('column_id', $qb->createNamedParameter($column->getId()), IQueryBuilder::PARAM_INT)); - - switch($column->getType()) { - case 'text': - $qb2->from('tables_row_cells_text'); - break; - case 'number': - $qb2->from('tables_row_cells_number'); - break; - default: - throw new InternalError('column type unknown to match cell-table for it'); - } - + $qb2->from('tables_row_cells_' . $column->getType()); switch ($operator) { case 'begins-with': @@ -367,7 +372,7 @@ private function parseEntities(IResult $result, array $sleeves): array { * @throws InternalError */ public function isRowInViewPresent(int $rowId, View $view, string $userId): bool { - return in_array($rowId, $this->getWantedRowIds($userId, $view->getFilterArray())); + return in_array($rowId, $this->getWantedRowIds($userId, $view->getTableId(), $view->getFilterArray())); } /** @@ -532,10 +537,13 @@ private function setColumns(array $columns): void { } /** - * @throws InternalError + * @param Column $column + * @param string $value + * @param 'out'|'in' $mode Parse the value for incoming requests that get send to the db or outgoing, from the db to the services * @return mixed + * @throws InternalError */ - private function formatValue(Column $column, string $value) { + private function formatValue(Column $column, string $value, string $mode = 'out') { $cellMapperClassName = 'OCA\Tables\Db\RowCell'.ucfirst($column->getType()).'Mapper'; /** @var RowCellMapperSuper $cellMapper */ try { @@ -544,7 +552,26 @@ private function formatValue(Column $column, string $value) { $this->logger->error($e->getMessage(), ['exception' => $e]); throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); } - return $cellMapper->parseValueOutgoing($column, $value); + if ($mode === 'out') { + return $cellMapper->parseValueOutgoing($column, $value); + } else { + return $cellMapper->parseValueIncoming($column, $value); + } + } + + /** + * @throws InternalError + */ + private function getColumnDbParamType(Column $column) { + $cellMapperClassName = 'OCA\Tables\Db\RowCell'.ucfirst($column->getType()).'Mapper'; + /** @var RowCellMapperSuper $cellMapper */ + try { + $cellMapper = Server::get($cellMapperClassName); + } catch (NotFoundExceptionInterface|ContainerExceptionInterface $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + } + return $cellMapper->getDbParamType(); } /** @@ -602,7 +629,7 @@ public function countRowsForView(View $view, string $userId, array $columns): in $filter = $view->getFilterArray(); try { - $rowIds = $this->getWantedRowIds($userId, $filter); + $rowIds = $this->getWantedRowIds($userId, $view->getTableId(), $filter); } catch (InternalError $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); $rowIds = []; @@ -610,5 +637,4 @@ public function countRowsForView(View $view, string $userId, array $columns): in return count($rowIds); } - } diff --git a/lib/Db/RowCellMapperSuper.php b/lib/Db/RowCellMapperSuper.php index cf439acbe..edcfbf965 100644 --- a/lib/Db/RowCellMapperSuper.php +++ b/lib/Db/RowCellMapperSuper.php @@ -20,14 +20,34 @@ public function __construct(IDBConnection $db, string $table, string $class) { } /** + * Parse value for db results (after send request) + * eg for filtering + * * @param Column $column - * @param mixed $value - * @return mixed + * @param T $value + * @return T + * @template T */ public function parseValueOutgoing(Column $column, $value) { return $value; } + /** + * Parse value for db requests (before send request) + * + * @param Column $column + * @param T $value + * @return T + * @template T + */ + public function parseValueIncoming(Column $column, $value) { + return $value; + } + + public function getDbParamType() { + return IQueryBuilder::PARAM_STR; + } + /** * @throws Exception */ diff --git a/lib/Db/RowCellNumberMapper.php b/lib/Db/RowCellNumberMapper.php index fc9e54abc..244f4f78c 100644 --- a/lib/Db/RowCellNumberMapper.php +++ b/lib/Db/RowCellNumberMapper.php @@ -2,6 +2,7 @@ namespace OCA\Tables\Db; +use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; /** @template-extends RowCellMapperSuper */ @@ -13,9 +14,9 @@ public function __construct(IDBConnection $db) { } /** - * @param Column $column - * @param $value - * @return float|int|null + * @inheritDoc + * + * @extends RowCellSuper */ public function parseValueOutgoing(Column $column, $value) { if($value === '') { @@ -23,9 +24,26 @@ public function parseValueOutgoing(Column $column, $value) { } $decimals = $column->getNumberDecimals() ?? 0; if ($decimals === 0) { - return intval($value); + return (int) $value; } else { return round(floatval($value), $decimals); } } + + /** + * @inheritDoc + * + * @extends RowCellSuper + */ + public function parseValueIncoming(Column $column, $value): ?float { + if($value === '') { + return null; + } + return (float) $value; + } + + public function getDbParamType() { + // seems to be a string for float/double values + return IQueryBuilder::PARAM_STR; + } } diff --git a/lib/Db/RowSleeveMapper.php b/lib/Db/RowSleeveMapper.php index 110e9359e..c86f7e387 100644 --- a/lib/Db/RowSleeveMapper.php +++ b/lib/Db/RowSleeveMapper.php @@ -38,7 +38,7 @@ public function find(int $id): RowSleeve { * @return RowSleeve[] * @throws Exception */ - public function findMultiple(array $ids): array { + public function findMultiple(array $ids): array { $qb = $this->db->getQueryBuilder(); $qb->select('*') ->from($this->table) diff --git a/lib/Helper/ColumnsHelper.php b/lib/Helper/ColumnsHelper.php new file mode 100644 index 000000000..a52740025 --- /dev/null +++ b/lib/Helper/ColumnsHelper.php @@ -0,0 +1,51 @@ +columns = [ + [ + 'name' => 'text', + 'db_type' => Types::TEXT, + ], + [ + 'name' => 'number', + 'db_type' => Types::FLOAT, + ], + ]; + } + + /** + * @param string[] $keys Keys that should be returned + * @return array + */ + public function get(array $keys): array { + $arr = []; + foreach ($this->columns as $column) { + $c = []; + foreach ($keys as $key) { + if (isset($column[$key])) { + $c[$key] = $column[$key]; + } else { + $c[$key] = null; + } + } + $arr[] = $c; + } + + if (count($keys) <= 1) { + $out = []; + foreach ($arr as $item) { + $out[] = $item[$keys[0]]; + } + return $out; + } + return $arr; + } +} diff --git a/lib/Migration/Version000700Date20230916000000.php b/lib/Migration/Version000700Date20230916000000.php index 2c54d439d..4ba2c654e 100644 --- a/lib/Migration/Version000700Date20230916000000.php +++ b/lib/Migration/Version000700Date20230916000000.php @@ -7,17 +7,24 @@ namespace OCA\Tables\Migration; use Closure; +use OCA\Tables\Helper\ColumnsHelper; +use OCP\DB\Exception; use OCP\DB\ISchemaWrapper; use OCP\DB\Types; use OCP\Migration\IOutput; use OCP\Migration\SimpleMigrationStep; +use OCP\Server; +use Psr\Container\ContainerExceptionInterface; +use Psr\Container\NotFoundExceptionInterface; class Version000700Date20230916000000 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 */ @@ -25,19 +32,16 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt $this->createRowSleevesTable($schema); - $rowTypeSchema = [ - [ - 'name' => 'text', - 'type' => Types::TEXT, - ], - [ - 'name' => 'number', - 'type' => Types::FLOAT, - ], - ] ; + try { + $columnsHelper = Server::get(ColumnsHelper::class); + } catch (NotFoundExceptionInterface|ContainerExceptionInterface $e) { + throw new Exception('Could not fetch columns helper which is needed to setup all the tables.'); + } + + $rowTypeSchema = $columnsHelper->get(['name', 'db_type']); foreach ($rowTypeSchema as $colType) { - $this->createRowValueTable($schema, $colType['name'], $colType['type']); + $this->createRowValueTable($schema, $colType['name'], $colType['db_type']); } return $schema; diff --git a/lib/Service/RowService.php b/lib/Service/RowService.php index a9594b7cc..c23ca09a3 100644 --- a/lib/Service/RowService.php +++ b/lib/Service/RowService.php @@ -66,9 +66,9 @@ public function formatRows(array $rows): array { public function findAllByTable(int $tableId, string $userId, ?int $limit = null, ?int $offset = null): array { try { if ($this->permissionsService->canReadRowsByElementId($tableId, 'table', $userId)) { - return $this->row2Mapper->findAll($this->columnMapper->findAllByTable($tableId), $limit, $offset, null, null, $userId); + return $this->row2Mapper->findAll($this->columnMapper->findAllByTable($tableId), $tableId, $limit, $offset, null, null, $userId); - // return $this->mapper->findAllByTable($tableId, $limit, $offset); + // return $this->mapper->findAllByTable($tableId, $limit, $offset); } else { throw new PermissionError('no read access to table id = '.$tableId); } @@ -95,9 +95,9 @@ public function findAllByView(int $viewId, string $userId, ?int $limit = null, ? $view = $this->viewMapper->find($viewId); $columnsArray = $view->getColumnsArray(); $columns = $this->columnMapper->find($columnsArray); - return $this->row2Mapper->findAll($columns, $limit, $offset, $view->getFilterArray(), $view->getSortArray(), $userId); + return $this->row2Mapper->findAll($columns, $view->getTableId(), $limit, $offset, $view->getFilterArray(), $view->getSortArray(), $userId); - // return $this->mapper->findAllByView($this->viewMapper->find($viewId), $userId, $limit, $offset); + // return $this->mapper->findAllByView($this->viewMapper->find($viewId), $userId, $limit, $offset); } else { throw new PermissionError('no read access to view id = '.$viewId); } @@ -522,7 +522,12 @@ public function getRowsCount(int $tableId): int { */ public function getViewRowsCount(View $view, string $userId): int { if ($this->permissionsService->canReadRowsByElementId($view->getId(), 'view', $userId)) { - return $this->row2Mapper->countRowsForView($view, $userId, $this->columnMapper->findMultiple($view->getColumnsArray())); + try { + return $this->row2Mapper->countRowsForView($view, $userId, $this->columnMapper->findMultiple($view->getColumnsArray())); + } catch (Exception $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + return 0; + } } else { throw new PermissionError('no read access for counting to view id = '.$view->getId()); } From 07a52356958a2aa1dd3c1fe92f1aa4e2ff953f1b Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Wed, 20 Dec 2023 11:15:11 +0100 Subject: [PATCH 16/73] add command to makefile for psalm check errors only Signed-off-by: Florian Steffens --- Makefile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Makefile b/Makefile index c40dc60d9..0016427cb 100644 --- a/Makefile +++ b/Makefile @@ -103,6 +103,9 @@ lint: lint-php lint-js lint-css lint-xml lint-php: lint-php-lint lint-php-cs-fixer lint-php-psalm +lint-fast: + composer run psalm -- --show-info=false + lint-php-lint: # Check PHP syntax errors @! find $(php_dirs) -name "*.php" | xargs -I{} php -l '{}' | grep -v "No syntax errors detected" From bc662cd78d0834ab800f5f3a725cdb3c24c52eb5 Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Wed, 20 Dec 2023 13:17:35 +0100 Subject: [PATCH 17/73] add all missing column types - add to definition - generate db classes Signed-off-by: Florian Steffens --- lib/Db/RowCellDatetime.php | 12 ++++++++++++ lib/Db/RowCellDatetimeMapper.php | 14 ++++++++++++++ lib/Db/RowCellSelection.php | 12 ++++++++++++ lib/Db/RowCellSelectionMapper.php | 32 +++++++++++++++++++++++++++++++ lib/Helper/ColumnsHelper.php | 8 ++++++++ 5 files changed, 78 insertions(+) create mode 100644 lib/Db/RowCellDatetime.php create mode 100644 lib/Db/RowCellDatetimeMapper.php create mode 100644 lib/Db/RowCellSelection.php create mode 100644 lib/Db/RowCellSelectionMapper.php diff --git a/lib/Db/RowCellDatetime.php b/lib/Db/RowCellDatetime.php new file mode 100644 index 000000000..5ee46201b --- /dev/null +++ b/lib/Db/RowCellDatetime.php @@ -0,0 +1,12 @@ + */ +class RowCellDatetime extends RowCellSuper { + protected ?string $value = null; + + public function jsonSerialize(): array { + return parent::jsonSerializePreparation($this->value); + } +} diff --git a/lib/Db/RowCellDatetimeMapper.php b/lib/Db/RowCellDatetimeMapper.php new file mode 100644 index 000000000..52fae4955 --- /dev/null +++ b/lib/Db/RowCellDatetimeMapper.php @@ -0,0 +1,14 @@ + */ +class RowCellDatetimeMapper extends RowCellMapperSuper { + protected string $table = 'tables_row_cells_datetime'; + + public function __construct(IDBConnection $db) { + parent::__construct($db, $this->table, RowCellDatetime::class); + } +} diff --git a/lib/Db/RowCellSelection.php b/lib/Db/RowCellSelection.php new file mode 100644 index 000000000..42cc14202 --- /dev/null +++ b/lib/Db/RowCellSelection.php @@ -0,0 +1,12 @@ + */ +class RowCellSelection extends RowCellSuper { + protected ?string $value = null; + + public function jsonSerialize(): array { + return parent::jsonSerializePreparation($this->value); + } +} diff --git a/lib/Db/RowCellSelectionMapper.php b/lib/Db/RowCellSelectionMapper.php new file mode 100644 index 000000000..28c34880f --- /dev/null +++ b/lib/Db/RowCellSelectionMapper.php @@ -0,0 +1,32 @@ + */ +class RowCellSelectionMapper extends RowCellMapperSuper { + protected string $table = 'tables_row_cells_selection'; + + public function __construct(IDBConnection $db) { + parent::__construct($db, $this->table, RowCellSelection::class); + } + + /** + * @inheritDoc + * + * @extends RowCellSuper + */ + public function parseValueIncoming(Column $column, $value): string { + return json_encode($value); + } + + /** + * @inheritDoc + * + * @extends RowCellSuper + */ + public function parseValueOutgoing(Column $column, $value) { + return json_decode($value); + } +} diff --git a/lib/Helper/ColumnsHelper.php b/lib/Helper/ColumnsHelper.php index a52740025..b0d4b5d64 100644 --- a/lib/Helper/ColumnsHelper.php +++ b/lib/Helper/ColumnsHelper.php @@ -18,6 +18,14 @@ public function __construct() { 'name' => 'number', 'db_type' => Types::FLOAT, ], + [ + 'name' => 'datetime', + 'db_type' => Types::TEXT, + ], + [ + 'name' => 'selection', + 'db_type' => Types::TEXT, + ], ]; } From 98a4449d8e13ae4978abaa4694fdbc8171148dab Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Wed, 20 Dec 2023 13:17:54 +0100 Subject: [PATCH 18/73] fix: formatting and types Signed-off-by: Florian Steffens --- lib/Db/Row2Mapper.php | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/lib/Db/Row2Mapper.php b/lib/Db/Row2Mapper.php index 6214888a5..c0bcc9b61 100644 --- a/lib/Db/Row2Mapper.php +++ b/lib/Db/Row2Mapper.php @@ -465,14 +465,13 @@ private function updateMetaData($entity, bool $setCreate = false): void { * * @throws InternalError */ - private function insertCell(int $rowId, int $columnId, string $value): void { + private function insertCell(int $rowId, int $columnId, $value): void { $cellClassName = 'OCA\Tables\Db\RowCell'.ucfirst($this->columns[$columnId]->getType()); /** @var RowCellSuper $cell */ $cell = new $cellClassName(); $cell->setRowIdWrapper($rowId); $cell->setColumnIdWrapper($columnId); - $cell->setValueWrapper($value); $this->updateMetaData($cell); // insert new cell @@ -484,6 +483,10 @@ private function insertCell(int $rowId, int $columnId, string $value): void { $this->logger->error($e->getMessage(), ['exception' => $e]); throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); } + + $v = $this->formatValue($this->columns[$columnId], $value, 'in'); + $cell->setValueWrapper($v); + try { $cellMapper->insert($cell); } catch (Exception $e) { @@ -496,10 +499,12 @@ private function insertCell(int $rowId, int $columnId, string $value): void { * @param RowCellSuper $cell * @param RowCellMapperSuper $mapper * @param mixed $value the value should be parsed to the correct format within the row service - * @throws Exception + * @param Column $column + * @throws InternalError */ - private function updateCell(RowCellSuper $cell, RowCellMapperSuper $mapper, $value): void { - $cell->setValueWrapper($value); + private function updateCell(RowCellSuper $cell, RowCellMapperSuper $mapper, $value, Column $column): void { + $v = $this->formatValue($column, $value, 'in'); + $cell->setValueWrapper($v); $this->updateMetaData($cell); $mapper->updateWrapper($cell); } @@ -507,7 +512,7 @@ private function updateCell(RowCellSuper $cell, RowCellMapperSuper $mapper, $val /** * @throws InternalError */ - private function insertOrUpdateCell(int $rowId, int $columnId, string $value): void { + private function insertOrUpdateCell(int $rowId, int $columnId, $value): void { $cellMapperClassName = 'OCA\Tables\Db\RowCell'.ucfirst($this->columns[$columnId]->getType()).'Mapper'; /** @var RowCellMapperSuper $cellMapper */ try { @@ -518,7 +523,7 @@ private function insertOrUpdateCell(int $rowId, int $columnId, string $value): v } try { $cell = $cellMapper->findByRowAndColumn($rowId, $columnId); - $this->updateCell($cell, $cellMapper, $value); + $this->updateCell($cell, $cellMapper, $value, $this->columns[$columnId]); } catch (DoesNotExistException $e) { $this->insertCell($rowId, $columnId, $value); } catch (MultipleObjectsReturnedException|Exception $e) { @@ -538,12 +543,12 @@ private function setColumns(array $columns): void { /** * @param Column $column - * @param string $value + * @param mixed $value * @param 'out'|'in' $mode Parse the value for incoming requests that get send to the db or outgoing, from the db to the services * @return mixed * @throws InternalError */ - private function formatValue(Column $column, string $value, string $mode = 'out') { + private function formatValue(Column $column, $value, string $mode = 'out') { $cellMapperClassName = 'OCA\Tables\Db\RowCell'.ucfirst($column->getType()).'Mapper'; /** @var RowCellMapperSuper $cellMapper */ try { From 7b7818d9457c5ef05e5d4aeea52e7ad5e7c49d2d Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Wed, 20 Dec 2023 15:30:31 +0100 Subject: [PATCH 19/73] fix(sqlite): remove unnecessary brackets Signed-off-by: Florian Steffens --- lib/Db/Row2Mapper.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Db/Row2Mapper.php b/lib/Db/Row2Mapper.php index c0bcc9b61..a4fb43827 100644 --- a/lib/Db/Row2Mapper.php +++ b/lib/Db/Row2Mapper.php @@ -179,9 +179,9 @@ private function getRows(array $rowIds, array $columnIds): array { ->andWhere($qb->expr()->in('row_id', $qb->createNamedParameter($rowIds, IQueryBuilder::PARAM_INT_ARRAY, ':rowsIds'))); if ($qbSqlForColumnTypes) { - $qbSqlForColumnTypes .= ' UNION ALL (' . $qbTmp->getSQL() . ') '; + $qbSqlForColumnTypes .= ' UNION ALL ' . $qbTmp->getSQL() . ' '; } else { - $qbSqlForColumnTypes = '((' . $qbTmp->getSQL() . ')'; + $qbSqlForColumnTypes = '(' . $qbTmp->getSQL(); } } $qbSqlForColumnTypes .= ')'; From 42eb83b563dcdfc714842757e1734b58fa58d2e0 Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Thu, 21 Dec 2023 13:32:02 +0100 Subject: [PATCH 20/73] =?UTF-8?q?fix(psalm):=20add=20template=20f=C3=BCr?= =?UTF-8?q?=20dynamic=20types?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Florian Steffens --- lib/Db/RowCellDatetimeMapper.php | 2 +- lib/Db/RowCellMapperSuper.php | 12 ++++++------ lib/Db/RowCellNumberMapper.php | 6 +----- lib/Db/RowCellSelectionMapper.php | 8 +++----- lib/Db/RowCellTextMapper.php | 2 +- 5 files changed, 12 insertions(+), 18 deletions(-) diff --git a/lib/Db/RowCellDatetimeMapper.php b/lib/Db/RowCellDatetimeMapper.php index 52fae4955..00d6ed62f 100644 --- a/lib/Db/RowCellDatetimeMapper.php +++ b/lib/Db/RowCellDatetimeMapper.php @@ -4,7 +4,7 @@ use OCP\IDBConnection; -/** @template-extends RowCellMapperSuper */ +/** @template-extends RowCellMapperSuper */ class RowCellDatetimeMapper extends RowCellMapperSuper { protected string $table = 'tables_row_cells_datetime'; diff --git a/lib/Db/RowCellMapperSuper.php b/lib/Db/RowCellMapperSuper.php index edcfbf965..86f76509e 100644 --- a/lib/Db/RowCellMapperSuper.php +++ b/lib/Db/RowCellMapperSuper.php @@ -12,6 +12,8 @@ /** * @template-extends QBMapper * @template T of RowCellSuper + * @template TIncoming + * @template TOutgoing */ class RowCellMapperSuper extends QBMapper { @@ -24,9 +26,8 @@ public function __construct(IDBConnection $db, string $table, string $class) { * eg for filtering * * @param Column $column - * @param T $value - * @return T - * @template T + * @param TOutgoing $value + * @return TOutgoing */ public function parseValueOutgoing(Column $column, $value) { return $value; @@ -36,9 +37,8 @@ public function parseValueOutgoing(Column $column, $value) { * Parse value for db requests (before send request) * * @param Column $column - * @param T $value - * @return T - * @template T + * @param TIncoming $value + * @return TIncoming */ public function parseValueIncoming(Column $column, $value) { return $value; diff --git a/lib/Db/RowCellNumberMapper.php b/lib/Db/RowCellNumberMapper.php index 244f4f78c..94b9d9f2e 100644 --- a/lib/Db/RowCellNumberMapper.php +++ b/lib/Db/RowCellNumberMapper.php @@ -5,7 +5,7 @@ use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; -/** @template-extends RowCellMapperSuper */ +/** @template-extends RowCellMapperSuper */ class RowCellNumberMapper extends RowCellMapperSuper { protected string $table = 'tables_row_cells_number'; @@ -15,8 +15,6 @@ public function __construct(IDBConnection $db) { /** * @inheritDoc - * - * @extends RowCellSuper */ public function parseValueOutgoing(Column $column, $value) { if($value === '') { @@ -32,8 +30,6 @@ public function parseValueOutgoing(Column $column, $value) { /** * @inheritDoc - * - * @extends RowCellSuper */ public function parseValueIncoming(Column $column, $value): ?float { if($value === '') { diff --git a/lib/Db/RowCellSelectionMapper.php b/lib/Db/RowCellSelectionMapper.php index 28c34880f..b74cd083b 100644 --- a/lib/Db/RowCellSelectionMapper.php +++ b/lib/Db/RowCellSelectionMapper.php @@ -4,7 +4,9 @@ use OCP\IDBConnection; -/** @template-extends RowCellMapperSuper */ +/** + * @template-extends RowCellMapperSuper + */ class RowCellSelectionMapper extends RowCellMapperSuper { protected string $table = 'tables_row_cells_selection'; @@ -14,8 +16,6 @@ public function __construct(IDBConnection $db) { /** * @inheritDoc - * - * @extends RowCellSuper */ public function parseValueIncoming(Column $column, $value): string { return json_encode($value); @@ -23,8 +23,6 @@ public function parseValueIncoming(Column $column, $value): string { /** * @inheritDoc - * - * @extends RowCellSuper */ public function parseValueOutgoing(Column $column, $value) { return json_decode($value); diff --git a/lib/Db/RowCellTextMapper.php b/lib/Db/RowCellTextMapper.php index d016717d5..2e1b8785e 100644 --- a/lib/Db/RowCellTextMapper.php +++ b/lib/Db/RowCellTextMapper.php @@ -4,7 +4,7 @@ use OCP\IDBConnection; -/** @template-extends RowCellMapperSuper */ +/** @template-extends RowCellMapperSuper */ class RowCellTextMapper extends RowCellMapperSuper { protected string $table = 'tables_row_cells_text'; From 1ec223c389502e55c8e4b7eb68e8a85522424384 Mon Sep 17 00:00:00 2001 From: Florian Date: Tue, 2 Jan 2024 13:24:32 +0100 Subject: [PATCH 21/73] Update lib/Helper/ColumnsHelper.php MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Julius Härtl Signed-off-by: Florian --- lib/Helper/ColumnsHelper.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/Helper/ColumnsHelper.php b/lib/Helper/ColumnsHelper.php index b0d4b5d64..17f6a146e 100644 --- a/lib/Helper/ColumnsHelper.php +++ b/lib/Helper/ColumnsHelper.php @@ -6,10 +6,7 @@ class ColumnsHelper { - private array $columns; - - public function __construct() { - $this->columns = [ + private array $columns = [ [ 'name' => 'text', 'db_type' => Types::TEXT, @@ -27,7 +24,6 @@ public function __construct() { 'db_type' => Types::TEXT, ], ]; - } /** * @param string[] $keys Keys that should be returned From d87645998bfe5d0b7340357513aacd5ee9303f7a Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Tue, 2 Jan 2024 13:48:03 +0100 Subject: [PATCH 22/73] code cleanup Signed-off-by: Florian Steffens --- lib/Helper/ColumnsHelper.php | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/lib/Helper/ColumnsHelper.php b/lib/Helper/ColumnsHelper.php index 17f6a146e..720e82260 100644 --- a/lib/Helper/ColumnsHelper.php +++ b/lib/Helper/ColumnsHelper.php @@ -7,23 +7,23 @@ class ColumnsHelper { private array $columns = [ - [ - 'name' => 'text', - 'db_type' => Types::TEXT, - ], - [ - 'name' => 'number', - 'db_type' => Types::FLOAT, - ], - [ - 'name' => 'datetime', - 'db_type' => Types::TEXT, - ], - [ - 'name' => 'selection', - 'db_type' => Types::TEXT, - ], - ]; + [ + 'name' => 'text', + 'db_type' => Types::TEXT, + ], + [ + 'name' => 'number', + 'db_type' => Types::FLOAT, + ], + [ + 'name' => 'datetime', + 'db_type' => Types::TEXT, + ], + [ + 'name' => 'selection', + 'db_type' => Types::TEXT, + ], + ]; /** * @param string[] $keys Keys that should be returned From 9799b0c5c28177e7138b5c0649fa407c1e3b2d24 Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Tue, 2 Jan 2024 13:48:18 +0100 Subject: [PATCH 23/73] db migration - add combined index Signed-off-by: Florian Steffens --- lib/Migration/Version000700Date20230916000000.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/Migration/Version000700Date20230916000000.php b/lib/Migration/Version000700Date20230916000000.php index 4ba2c654e..1d0dece2b 100644 --- a/lib/Migration/Version000700Date20230916000000.php +++ b/lib/Migration/Version000700Date20230916000000.php @@ -60,6 +60,7 @@ private function createRowValueTable(ISchemaWrapper $schema, string $name, strin // we will write this data to use it one day to extract versions of rows based on the timestamp $table->addColumn('last_edit_at', Types::DATETIME, ['notnull' => true]); $table->addColumn('last_edit_by', Types::STRING, ['notnull' => true, 'length' => 64]); + $table->addIndex(['column_id', 'row_id']); $table->setPrimaryKey(['id']); } } @@ -76,6 +77,7 @@ private function createRowSleevesTable(ISchemaWrapper $schema) { $table->addColumn('created_at', Types::DATETIME, ['notnull' => true]); $table->addColumn('last_edit_by', Types::STRING, ['notnull' => true, 'length' => 64]); $table->addColumn('last_edit_at', Types::DATETIME, ['notnull' => true]); + $table->setPrimaryKey(['id']); } } From 12cc779dc43328ac91d0ed2b921e5a43872378ab Mon Sep 17 00:00:00 2001 From: Florian Date: Wed, 3 Jan 2024 11:52:22 +0100 Subject: [PATCH 24/73] Update lib/Db/Row2Mapper.php MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Julius Härtl Signed-off-by: Florian --- lib/Db/Row2Mapper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Db/Row2Mapper.php b/lib/Db/Row2Mapper.php index a4fb43827..92af75eb6 100644 --- a/lib/Db/Row2Mapper.php +++ b/lib/Db/Row2Mapper.php @@ -287,7 +287,7 @@ private function getFilterExpression(IQueryBuilder $qb, Column $column, string $ case 'ends-with': return $qb2->andWhere($qb->expr()->like('value', $qb->createNamedParameter($value.'%', $paramType))); case 'contains': - return $qb2->andWhere($qb->expr()->like('value', $qb->createNamedParameter('%'.$value.'%', $paramType))); + return $qb2->andWhere($qb->expr()->like('value', $qb->createNamedParameter('%'.$this->db->escapeLikeParameter($value).'%', $paramType))); case 'is-equal': return $qb2->andWhere($qb->expr()->eq('value', $qb->createNamedParameter($value, $paramType))); case 'is-greater-than': From b01cbafcc8f0bc344bcb930f93ffd8a00f79910e Mon Sep 17 00:00:00 2001 From: Florian Date: Wed, 3 Jan 2024 11:52:43 +0100 Subject: [PATCH 25/73] Update lib/Db/Row2Mapper.php MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Julius Härtl Signed-off-by: Florian --- lib/Db/Row2Mapper.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Db/Row2Mapper.php b/lib/Db/Row2Mapper.php index 92af75eb6..62522c4b5 100644 --- a/lib/Db/Row2Mapper.php +++ b/lib/Db/Row2Mapper.php @@ -283,9 +283,9 @@ private function getFilterExpression(IQueryBuilder $qb, Column $column, string $ switch ($operator) { case 'begins-with': - return $qb2->andWhere($qb->expr()->like('value', $qb->createNamedParameter('%'.$value, $paramType))); + return $qb2->andWhere($qb->expr()->like('value', $qb->createNamedParameter('%'.$this->db->escapeLikeParameter($value), $paramType))); case 'ends-with': - return $qb2->andWhere($qb->expr()->like('value', $qb->createNamedParameter($value.'%', $paramType))); + return $qb2->andWhere($qb->expr()->like('value', $qb->createNamedParameter($this->db->escapeLikeParameter($value).'%', $paramType))); case 'contains': return $qb2->andWhere($qb->expr()->like('value', $qb->createNamedParameter('%'.$this->db->escapeLikeParameter($value).'%', $paramType))); case 'is-equal': From 526d944d5896befdddbeb7bb9f321eab2706106e Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Wed, 3 Jan 2024 12:04:36 +0100 Subject: [PATCH 26/73] remove unnecessary interface Signed-off-by: Florian Steffens --- lib/Db/IRowCell.php | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 lib/Db/IRowCell.php diff --git a/lib/Db/IRowCell.php b/lib/Db/IRowCell.php deleted file mode 100644 index b3b2d4906..000000000 --- a/lib/Db/IRowCell.php +++ /dev/null @@ -1,16 +0,0 @@ - Date: Wed, 3 Jan 2024 12:04:57 +0100 Subject: [PATCH 27/73] add index for new db table Signed-off-by: Florian Steffens --- lib/Migration/Version000700Date20230916000000.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Migration/Version000700Date20230916000000.php b/lib/Migration/Version000700Date20230916000000.php index 1d0dece2b..909214e3d 100644 --- a/lib/Migration/Version000700Date20230916000000.php +++ b/lib/Migration/Version000700Date20230916000000.php @@ -77,7 +77,7 @@ private function createRowSleevesTable(ISchemaWrapper $schema) { $table->addColumn('created_at', Types::DATETIME, ['notnull' => true]); $table->addColumn('last_edit_by', Types::STRING, ['notnull' => true, 'length' => 64]); $table->addColumn('last_edit_at', Types::DATETIME, ['notnull' => true]); - + $table->addIndex(['id']); $table->setPrimaryKey(['id']); } } From 0f3aee2196e1830e01dec1a5639c94bce0f6bf28 Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Wed, 3 Jan 2024 15:06:36 +0100 Subject: [PATCH 28/73] Renaming rows - the old (original) "rows" to "LegacyRow" - the new temporary "row2" to new "row" Signed-off-by: Florian Steffens --- lib/Command/Clean.php | 10 +- lib/Db/LegacyRow.php | 65 +++ lib/Db/LegacyRowMapper.php | 427 ++++++++++++++++++++ lib/Db/Row.php | 172 ++++++-- lib/Db/Row2.php | 163 -------- lib/Db/Row2Mapper.php | 645 ------------------------------ lib/Db/RowMapper.php | 792 +++++++++++++++++++++++-------------- lib/Service/RowService.php | 38 +- 8 files changed, 1156 insertions(+), 1156 deletions(-) create mode 100644 lib/Db/LegacyRow.php create mode 100644 lib/Db/LegacyRowMapper.php delete mode 100644 lib/Db/Row2.php delete mode 100644 lib/Db/Row2Mapper.php diff --git a/lib/Command/Clean.php b/lib/Command/Clean.php index 901a2189f..c08e6dacd 100644 --- a/lib/Command/Clean.php +++ b/lib/Command/Clean.php @@ -23,8 +23,8 @@ namespace OCA\Tables\Command; -use OCA\Tables\Db\Row; -use OCA\Tables\Db\RowMapper; +use OCA\Tables\Db\LegacyRow; +use OCA\Tables\Db\LegacyRowMapper; use OCA\Tables\Errors\InternalError; use OCA\Tables\Errors\NotFoundError; use OCA\Tables\Errors\PermissionError; @@ -50,17 +50,17 @@ class Clean extends Command { protected RowService $rowService; protected TableService $tableService; protected LoggerInterface $logger; - protected RowMapper $rowMapper; + protected LegacyRowMapper $rowMapper; private bool $dry = false; private int $truncateLength = 20; - private ?Row $row = null; + private ?LegacyRow $row = null; private int $offset = -1; private OutputInterface $output; - public function __construct(LoggerInterface $logger, ColumnService $columnService, RowService $rowService, TableService $tableService, RowMapper $rowMapper) { + public function __construct(LoggerInterface $logger, ColumnService $columnService, RowService $rowService, TableService $tableService, LegacyRowMapper $rowMapper) { parent::__construct(); $this->logger = $logger; $this->columnService = $columnService; diff --git a/lib/Db/LegacyRow.php b/lib/Db/LegacyRow.php new file mode 100644 index 000000000..045921853 --- /dev/null +++ b/lib/Db/LegacyRow.php @@ -0,0 +1,65 @@ +addType('id', 'integer'); + $this->addType('tableId', 'integer'); + } + + public function jsonSerialize(): array { + return [ + 'id' => $this->id, + 'tableId' => $this->tableId, + 'createdBy' => $this->createdBy, + 'createdAt' => $this->createdAt, + 'lastEditBy' => $this->lastEditBy, + 'lastEditAt' => $this->lastEditAt, + 'data' => $this->getDataArray(), + ]; + } + + public function getDataArray():array { + return \json_decode($this->getData(), true); + } + + public function setDataArray(array $array):void { + $new = []; + foreach ($array as $a) { + $new[] = [ + 'columnId' => (int) $a['columnId'], + 'value' => $a['value'] + ]; + } + $json = \json_encode($new); + $this->setData($json); + } +} diff --git a/lib/Db/LegacyRowMapper.php b/lib/Db/LegacyRowMapper.php new file mode 100644 index 000000000..0df2da5d2 --- /dev/null +++ b/lib/Db/LegacyRowMapper.php @@ -0,0 +1,427 @@ + */ +class LegacyRowMapper extends QBMapper { + protected string $table = 'tables_rows'; + protected TextColumnQB $textColumnQB; + protected SelectionColumnQB $selectionColumnQB; + protected NumberColumnQB $numberColumnQB; + protected DatetimeColumnQB $datetimeColumnQB; + protected SuperColumnQB $genericColumnQB; + protected ColumnMapper $columnMapper; + protected LoggerInterface $logger; + protected UserHelper $userHelper; + + protected int $platform; + + public function __construct(IDBConnection $db, LoggerInterface $logger, TextColumnQB $textColumnQB, SelectionColumnQB $selectionColumnQB, NumberColumnQB $numberColumnQB, DatetimeColumnQB $datetimeColumnQB, SuperColumnQB $columnQB, ColumnMapper $columnMapper, UserHelper $userHelper) { + parent::__construct($db, $this->table, LegacyRow::class); + $this->logger = $logger; + $this->textColumnQB = $textColumnQB; + $this->numberColumnQB = $numberColumnQB; + $this->selectionColumnQB = $selectionColumnQB; + $this->datetimeColumnQB = $datetimeColumnQB; + $this->genericColumnQB = $columnQB; + $this->columnMapper = $columnMapper; + $this->userHelper = $userHelper; + $this->setPlatform(); + } + + private function setPlatform() { + if (str_contains(strtolower(get_class($this->db->getDatabasePlatform())), 'postgres')) { + $this->platform = IColumnTypeQB::DB_PLATFORM_PGSQL; + } elseif (str_contains(strtolower(get_class($this->db->getDatabasePlatform())), 'sqlite')) { + $this->platform = IColumnTypeQB::DB_PLATFORM_SQLITE; + } else { + $this->platform = IColumnTypeQB::DB_PLATFORM_MYSQL; + } + $this->genericColumnQB->setPlatform($this->platform); + $this->textColumnQB->setPlatform($this->platform); + $this->numberColumnQB->setPlatform($this->platform); + $this->selectionColumnQB->setPlatform($this->platform); + $this->datetimeColumnQB->setPlatform($this->platform); + } + + /** + * @param int $id + * + * @return LegacyRow + * @throws DoesNotExistException + * @throws Exception + * @throws MultipleObjectsReturnedException + */ + public function find(int $id): LegacyRow { + $qb = $this->db->getQueryBuilder(); + $qb->select('t1.*') + ->from($this->table, 't1') + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))); + return $this->findEntity($qb); + } + + private function buildFilterByColumnType($qb, array $filter, string $filterId): ?IQueryFunction { + try { + $columnQbClassName = 'OCA\Tables\Db\ColumnTypes\\'; + $type = explode("-", $filter['columnType'])[0]; + + $columnQbClassName .= ucfirst($type).'ColumnQB'; + + /** @var IColumnTypeQB $columnQb */ + $columnQb = Server::get($columnQbClassName); + return $columnQb->addWhereFilterExpression($qb, $filter, $filterId); + } catch (NotFoundExceptionInterface|ContainerExceptionInterface $e) { + $this->logger->debug('Column type query builder class not found'); + } + return null; + } + + private function getInnerFilterExpressions($qb, $filterGroup, int $groupIndex): array { + $innerFilterExpressions = []; + foreach ($filterGroup as $index => $filter) { + $innerFilterExpressions[] = $this->buildFilterByColumnType($qb, $filter, $groupIndex.$index); + } + return $innerFilterExpressions; + } + + private function getFilterGroups($qb, $filters): array { + $filterGroups = []; + foreach ($filters as $groupIndex => $filterGroup) { + $filterGroups[] = $qb->expr()->andX(...$this->getInnerFilterExpressions($qb, $filterGroup, $groupIndex)); + } + return $filterGroups; + } + + private function resolveSearchValue(string $unresolvedSearchValue, string $userId): string { + switch (ltrim($unresolvedSearchValue, '@')) { + case 'me': return $userId; + case 'my-name': return $this->userHelper->getUserDisplayName($userId); + case 'checked': return 'true'; + case 'unchecked': return 'false'; + case 'stars-0': return '0'; + case 'stars-1': return '1'; + case 'stars-2': return '2'; + case 'stars-3': return '3'; + case 'stars-4': return '4'; + case 'stars-5': return '5'; + case 'datetime-date-today': return date('Y-m-d') ? date('Y-m-d') : ''; + case 'datetime-date-start-of-year': return date('Y-01-01') ? date('Y-01-01') : ''; + case 'datetime-date-start-of-month': return date('Y-m-01') ? date('Y-m-01') : ''; + case 'datetime-date-start-of-week': + $day = date('w'); + $result = date('m-d-Y', strtotime('-'.$day.' days')); + return $result ?: ''; + case 'datetime-time-now': return date('H:i'); + case 'datetime-now': return date('Y-m-d H:i') ? date('Y-m-d H:i') : ''; + default: return $unresolvedSearchValue; + } + } + + private function addOrderByRules(IQueryBuilder $qb, $sortArray) { + foreach ($sortArray as $index => $sortRule) { + $sortMode = $sortRule['mode']; + if (!in_array($sortMode, ['ASC', 'DESC'])) { + continue; + } + $sortColumnPlaceholder = 'sortColumn'.$index; + if ($sortRule['columnId'] < 0) { + try { + $orderString = SuperColumnQB::getMetaColumnName($sortRule['columnId']); + } catch (InternalError $e) { + return; + } + } else { + if ($this->platform === IColumnTypeQB::DB_PLATFORM_PGSQL) { + $orderString = 'c'.$sortRule['columnId'].'->>\'value\''; + } elseif ($this->platform === IColumnTypeQB::DB_PLATFORM_SQLITE) { + // here is an error for (multiple) sorting, works only for the first column at the moment + $orderString = 'json_extract(t2.value, "$.value")'; + } else { // mariadb / mysql + $orderString = 'JSON_EXTRACT(data, CONCAT( JSON_UNQUOTE(JSON_SEARCH(JSON_EXTRACT(data, \'$[*].columnId\'), \'one\', :'.$sortColumnPlaceholder.')), \'.value\'))'; + } + if (str_starts_with($sortRule['columnType'], 'number')) { + $orderString = 'CAST('.$orderString.' as decimal)'; + } + } + + $qb->addOrderBy($qb->createFunction($orderString), $sortMode); + $qb->setParameter($sortColumnPlaceholder, $sortRule['columnId'], IQueryBuilder::PARAM_INT); + } + } + + /** + * @param View $view + * @param $userId + * @return int + * @throws InternalError + */ + public function countRowsForView(View $view, $userId): int { + $qb = $this->db->getQueryBuilder(); + $qb->select($qb->func()->count('*', 'counter')) + ->from($this->table, 't1') + ->where($qb->expr()->eq('table_id', $qb->createNamedParameter($view->getTableId(), IQueryBuilder::PARAM_INT))); + + $neededColumnIds = $this->getAllColumnIdsFromView($view, $qb); + try { + $neededColumns = $this->columnMapper->getColumnTypes($neededColumnIds); + } catch (Exception $e) { + throw new InternalError('Could not get column types to count rows'); + } + + // Filter + + $this->addFilterToQuery($qb, $view, $neededColumns, $userId); + + try { + $result = $this->findOneQuery($qb); + return (int)$result['counter']; + } catch (DoesNotExistException|MultipleObjectsReturnedException|Exception $e) { + $this->logger->warning('Exception occurred: '.$e->getMessage().' Returning 0.'); + return 0; + } + } + + + public function getRowIdsOfView(View $view, $userId): array { + $qb = $this->db->getQueryBuilder(); + $qb->select('t1.id') + ->from($this->table, 't1') + ->where($qb->expr()->eq('table_id', $qb->createNamedParameter($view->getTableId(), IQueryBuilder::PARAM_INT))); + + $neededColumnIds = $this->getAllColumnIdsFromView($view, $qb); + $neededColumns = $this->columnMapper->getColumnTypes($neededColumnIds); + + // Filter + + $this->addFilterToQuery($qb, $view, $neededColumns, $userId); + $result = $qb->executeQuery(); + try { + $ids = []; + while ($row = $result->fetch()) { + $ids[] = $row['id']; + } + return $ids; + } finally { + $result->closeCursor(); + } + } + + + private function addFilterToQuery(IQueryBuilder $qb, View $view, array $neededColumnTypes, string $userId): void { + $enrichedFilters = $view->getFilterArray(); + if (count($enrichedFilters) > 0) { + foreach ($enrichedFilters as &$filterGroup) { + foreach ($filterGroup as &$filter) { + $filter['columnType'] = $neededColumnTypes[$filter['columnId']]; + // TODO move resolution for magic fields to service layer + if(str_starts_with((string) $filter['value'], '@')) { + $filter['value'] = $this->resolveSearchValue((string) $filter['value'], $userId); + } + } + } + $qb->andWhere( + $qb->expr()->orX( + ...$this->getFilterGroups($qb, $enrichedFilters) + ) + ); + } + } + + /** + * @param int $tableId + * @param int|null $limit + * @param int|null $offset + * @return array + * @throws Exception + */ + public function findAllByTable(int $tableId, ?int $limit = null, ?int $offset = null): array { + $qb = $this->db->getQueryBuilder(); + $qb->select('t1.*') + ->from($this->table, 't1') + ->where($qb->expr()->eq('table_id', $qb->createNamedParameter($tableId))); + + if ($limit !== null) { + $qb->setMaxResults($limit); + } + if ($offset !== null) { + $qb->setFirstResult($offset); + } + + return $this->findEntities($qb); + } + + /** + * @param View $view + * @param string $userId + * @param int|null $limit + * @param int|null $offset + * @return array + * @throws Exception + */ + public function findAllByView(View $view, string $userId, ?int $limit = null, ?int $offset = null): array { + $qb = $this->db->getQueryBuilder(); + $qb->select('t1.*') + ->from($this->table, 't1') + ->where($qb->expr()->eq('table_id', $qb->createNamedParameter($view->getTableId(), IQueryBuilder::PARAM_INT))); + + + $neededColumnIds = $this->getAllColumnIdsFromView($view, $qb); + $neededColumnsTypes = $this->columnMapper->getColumnTypes($neededColumnIds); + + // Filter + + $this->addFilterToQuery($qb, $view, $neededColumnsTypes, $userId); + + // Sorting + + $enrichedSort = $view->getSortArray(); + foreach ($enrichedSort as &$sort) { + $sort['columnType'] = $neededColumnsTypes[$sort['columnId']]; + } + $this->addOrderByRules($qb, $enrichedSort); + + if ($limit !== null) { + $qb->setMaxResults($limit); + } + if ($offset !== null) { + $qb->setFirstResult($offset); + } + $rows = $this->findEntities($qb); + foreach ($rows as &$row) { + $row->setDataArray(array_filter($row->getDataArray(), function ($item) use ($view) { + return in_array($item['columnId'], $view->getColumnsArray()); + })); + } + return $rows; + } + + private function getAllColumnIdsFromView(View $view, IQueryBuilder $qb): array { + $neededColumnIds = []; + $filters = $view->getFilterArray(); + $sorts = $view->getSortArray(); + foreach ($filters as $filterGroup) { + foreach ($filterGroup as $filter) { + $neededColumnIds[] = $filter['columnId']; + } + } + foreach ($sorts as $sortRule) { + $neededColumnIds[] = $sortRule['columnId']; + } + $neededColumnIds = array_unique($neededColumnIds); + if ($this->platform === IColumnTypeQB::DB_PLATFORM_PGSQL) { + foreach ($neededColumnIds as $columnId) { + if ($columnId >= 0) { + /** @psalm-suppress ImplicitToStringCast */ + $qb->leftJoin("t1", $qb->createFunction('json_array_elements(t1.data)'), 'c' . intval($columnId), $qb->createFunction("CAST(c".intval($columnId).".value->>'columnId' AS int) = ".$columnId)); + // TODO Security + } + } + } + return $neededColumnIds; + } + + /** + * @throws MultipleObjectsReturnedException + * @throws DoesNotExistException + * @throws Exception + */ + public function findNext(int $offsetId = -1): LegacyRow { + $qb = $this->db->getQueryBuilder(); + $qb->select('t1.*') + ->from($this->table, 't1') + ->where($qb->expr()->gt('id', $qb->createNamedParameter($offsetId))) + ->setMaxResults(1) + ->orderBy('id', 'ASC'); + + return $this->findEntity($qb); + } + + /** + * @return int affected rows + * @throws Exception + */ + public function deleteAllByTable(int $tableId): int { + $qb = $this->db->getQueryBuilder(); + $qb->delete($this->tableName) + ->where( + $qb->expr()->eq('table_id', $qb->createNamedParameter($tableId)) + ); + return $qb->executeStatement(); + } + + /** + * @throws Exception + */ + public function findAllWithColumn(int $columnId): array { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->table, 't1'); + + $this->genericColumnQB->addWhereForFindAllWithColumn($qb, $columnId); + + return $this->findEntities($qb); + } + + /** + * @param int $tableId + * @return int + */ + public function countRows(int $tableId): int { + $qb = $this->db->getQueryBuilder(); + $qb->select($qb->func()->count('*', 'counter')); + $qb->from($this->table, 't1'); + $qb->where( + $qb->expr()->eq('table_id', $qb->createNamedParameter($tableId)) + ); + + try { + $result = $this->findOneQuery($qb); + return (int)$result['counter']; + } catch (DoesNotExistException|MultipleObjectsReturnedException|Exception $e) { + $this->logger->warning('Exception occurred: '.$e->getMessage().' Returning 0.'); + return 0; + } + } + + /** + * @param int $id + * @param View $view + * @return LegacyRow + * @throws DoesNotExistException + * @throws Exception + * @throws MultipleObjectsReturnedException + */ + public function findByView(int $id, View $view): LegacyRow { + $qb = $this->db->getQueryBuilder(); + $qb->select('t1.*') + ->from($this->table, 't1') + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))); + $row = $this->findEntity($qb); + + $row->setDataArray(array_filter($row->getDataArray(), function ($item) use ($view) { + return in_array($item['columnId'], $view->getColumnsArray()); + })); + + return $row; + } +} diff --git a/lib/Db/Row.php b/lib/Db/Row.php index 0e85589f7..03ec606bc 100644 --- a/lib/Db/Row.php +++ b/lib/Db/Row.php @@ -3,63 +3,161 @@ namespace OCA\Tables\Db; use JsonSerializable; - -use OCP\AppFramework\Db\Entity; +use OCA\Tables\ResponseDefinitions; /** - * @psalm-suppress PropertyNotSetInConstructor - * @method getTableId(): int - * @method setTableId(int $tableId) - * @method getCreatedBy(): string - * @method setCreatedBy(string $createdBy) - * @method getCreatedAt(): string - * @method setCreatedAt(string $createdAt) - * @method getLastEditBy(): string - * @method setLastEditBy(string $lastEditBy) - * @method getLastEditAt(): string - * @method setLastEditAt(string $lastEditAt) - * @method getData(): string - * @method setData(string $data) + * @psalm-import-type TablesRow from ResponseDefinitions */ -class Row extends Entity implements JsonSerializable { - protected ?int $tableId = null; - protected ?string $createdBy = null; - protected ?string $createdAt = null; - protected ?string $lastEditBy = null; - protected ?string $lastEditAt = null; +class Row implements JsonSerializable { + private ?int $id = null; + private ?int $tableId = null; + private ?string $createdBy = null; + private ?string $createdAt = null; + private ?string $lastEditBy = null; + private ?string $lastEditAt = null; + private ?array $data = []; + private array $changedColumnIds = []; // collect column ids that have changed after $loaded = true - protected ?string $data = null; + private bool $loaded = false; // set to true if model is loaded, after that changed column ids will be collected - public function __construct() { - $this->addType('id', 'integer'); - $this->addType('tableId', 'integer'); + public function getId(): ?int { + return $this->id; + } + public function setId(int $id): void { + $this->id = $id; + } + + public function getTableId(): ?int { + return $this->tableId; + } + public function setTableId(int $tableId): void { + $this->tableId = $tableId; + } + + public function getCreatedBy(): ?string { + return $this->createdBy; + } + public function setCreatedBy(string $userId): void { + $this->createdBy = $userId; + } + + public function getCreatedAt(): ?string { + return $this->createdAt; + } + public function setCreatedAt(string $time): void { + $this->createdAt = $time; + } + + public function getLastEditBy(): ?string { + return $this->lastEditBy; + } + public function setLastEditBy(string $userId): void { + $this->lastEditBy = $userId; + } + + public function getLastEditAt(): ?string { + return $this->lastEditAt; + } + public function setLastEditAt(string $time): void { + $this->lastEditAt = $time; } + public function getData(): ?array { + return $this->data; + } + + /** + * @param list $data + * @return void + */ + public function setData(array $data): void { + foreach ($data as $cell) { + $this->insertOrUpdateCell($cell); + } + } + + /** + * @param int $columnId + * @param int|float|string $value + * @return void + */ + public function addCell(int $columnId, $value) { + $this->data[] = ['columnId' => $columnId, 'value' => $value]; + $this->addChangedColumnId($columnId); + } + + /** + * @param array{columnId: int, value: mixed} $entry + * @return string + */ + public function insertOrUpdateCell(array $entry): string { + $columnId = $entry['columnId']; + $value = $entry['value']; + foreach ($this->data as &$cell) { + if($cell['columnId'] === $columnId) { + if ($cell['value'] != $value) { // yes, no type safety here + $cell['value'] = $value; + $this->addChangedColumnId($columnId); + } + return 'updated'; + } + } + $this->data[] = ['columnId' => $columnId, 'value' => $value]; + $this->addChangedColumnId($columnId); + return 'inserted'; + } + + public function removeCell(int $columnId): void { + // TODO + } + + /** + * @psalm-return TablesRow + */ public function jsonSerialize(): array { return [ 'id' => $this->id, 'tableId' => $this->tableId, + 'data' => $this->data, 'createdBy' => $this->createdBy, 'createdAt' => $this->createdAt, 'lastEditBy' => $this->lastEditBy, 'lastEditAt' => $this->lastEditAt, - 'data' => $this->getDataArray(), ]; } - public function getDataArray():array { - return \json_decode($this->getData(), true); + /** + * Can only be changed by private methods + * @param int $columnId + * @return void + */ + private function addChangedColumnId(int $columnId): void { + if ($this->loaded && !in_array($columnId, $this->changedColumnIds)) { + $this->changedColumnIds[] = $columnId; + } } - public function setDataArray(array $array):void { - $new = []; - foreach ($array as $a) { - $new[] = [ - 'columnId' => (int) $a['columnId'], - 'value' => $a['value'] - ]; + /** + * @return list + */ + public function getChangedCells(): array { + $out = []; + foreach ($this->data as $cell) { + if (in_array($cell['columnId'], $this->changedColumnIds)) { + $out[] = $cell; + } } - $json = \json_encode($new); - $this->setData($json); + return $out; + } + + /** + * Set loaded status to true + * starting now changes will be tracked + * + * @return void + */ + public function markAsLoaded(): void { + $this->loaded = true; } + } diff --git a/lib/Db/Row2.php b/lib/Db/Row2.php deleted file mode 100644 index e95f08046..000000000 --- a/lib/Db/Row2.php +++ /dev/null @@ -1,163 +0,0 @@ -id; - } - public function setId(int $id): void { - $this->id = $id; - } - - public function getTableId(): ?int { - return $this->tableId; - } - public function setTableId(int $tableId): void { - $this->tableId = $tableId; - } - - public function getCreatedBy(): ?string { - return $this->createdBy; - } - public function setCreatedBy(string $userId): void { - $this->createdBy = $userId; - } - - public function getCreatedAt(): ?string { - return $this->createdAt; - } - public function setCreatedAt(string $time): void { - $this->createdAt = $time; - } - - public function getLastEditBy(): ?string { - return $this->lastEditBy; - } - public function setLastEditBy(string $userId): void { - $this->lastEditBy = $userId; - } - - public function getLastEditAt(): ?string { - return $this->lastEditAt; - } - public function setLastEditAt(string $time): void { - $this->lastEditAt = $time; - } - - public function getData(): ?array { - return $this->data; - } - - /** - * @param list $data - * @return void - */ - public function setData(array $data): void { - foreach ($data as $cell) { - $this->insertOrUpdateCell($cell); - } - } - - /** - * @param int $columnId - * @param int|float|string $value - * @return void - */ - public function addCell(int $columnId, $value) { - $this->data[] = ['columnId' => $columnId, 'value' => $value]; - $this->addChangedColumnId($columnId); - } - - /** - * @param array{columnId: int, value: mixed} $entry - * @return string - */ - public function insertOrUpdateCell(array $entry): string { - $columnId = $entry['columnId']; - $value = $entry['value']; - foreach ($this->data as &$cell) { - if($cell['columnId'] === $columnId) { - if ($cell['value'] != $value) { // yes, no type safety here - $cell['value'] = $value; - $this->addChangedColumnId($columnId); - } - return 'updated'; - } - } - $this->data[] = ['columnId' => $columnId, 'value' => $value]; - $this->addChangedColumnId($columnId); - return 'inserted'; - } - - public function removeCell(int $columnId): void { - // TODO - } - - /** - * @psalm-return TablesRow - */ - public function jsonSerialize(): array { - return [ - 'id' => $this->id, - 'tableId' => $this->tableId, - 'data' => $this->data, - 'createdBy' => $this->createdBy, - 'createdAt' => $this->createdAt, - 'lastEditBy' => $this->lastEditBy, - 'lastEditAt' => $this->lastEditAt, - ]; - } - - /** - * Can only be changed by private methods - * @param int $columnId - * @return void - */ - private function addChangedColumnId(int $columnId): void { - if ($this->loaded && !in_array($columnId, $this->changedColumnIds)) { - $this->changedColumnIds[] = $columnId; - } - } - - /** - * @return list - */ - public function getChangedCells(): array { - $out = []; - foreach ($this->data as $cell) { - if (in_array($cell['columnId'], $this->changedColumnIds)) { - $out[] = $cell; - } - } - return $out; - } - - /** - * Set loaded status to true - * starting now changes will be tracked - * - * @return void - */ - public function markAsLoaded(): void { - $this->loaded = true; - } - -} diff --git a/lib/Db/Row2Mapper.php b/lib/Db/Row2Mapper.php deleted file mode 100644 index 62522c4b5..000000000 --- a/lib/Db/Row2Mapper.php +++ /dev/null @@ -1,645 +0,0 @@ -rowSleeveMapper = $rowSleeveMapper; - $this->userId = $userId; - $this->db = $db; - $this->logger = $logger; - $this->userHelper = $userHelper; - $this->columnsHelper = $columnsHelper; - } - - /** - * @throws InternalError - */ - public function delete(Row2 $row): Row2 { - foreach ($this->columnsHelper->get(['name']) as $columnType) { - $cellMapperClassName = 'OCA\Tables\Db\RowCell' . ucfirst($columnType) . 'Mapper'; - /** @var RowCellMapperSuper $cellMapper */ - try { - $cellMapper = Server::get($cellMapperClassName); - } catch (NotFoundExceptionInterface|ContainerExceptionInterface $e) { - $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage()); - } - try { - $cellMapper->deleteAllForRow($row->getId()); - } catch (Exception $e) { - $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); - } - } - try { - $this->rowSleeveMapper->deleteById($row->getId()); - } catch (Exception $e) { - $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); - } - - return $row; - } - - /** - * @param int $id - * @param Column[] $columns - * @return Row2 - * @throws InternalError - * @throws NotFoundError - */ - public function find(int $id, array $columns): Row2 { - $this->setColumns($columns); - $columnIdsArray = array_map(fn (Column $column) => $column->getId(), $columns); - $rows = $this->getRows([$id], $columnIdsArray); - if (count($rows) === 1) { - return $rows[0]; - } elseif (count($rows) === 0) { - $e = new Exception('Wanted row not found.'); - $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new NotFoundError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); - } else { - $e = new Exception('Too many results for one wanted row.'); - $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); - } - } - - /** - * @throws DoesNotExistException - * @throws MultipleObjectsReturnedException - * @throws Exception - */ - public function getTableIdForRow(int $rowId): ?int { - $rowSleeve = $this->rowSleeveMapper->find($rowId); - return $rowSleeve->getTableId(); - } - - /** - * @param string $userId - * @param int $tableId - * @param array|null $filter - * @param int|null $limit - * @param int|null $offset - * @return int[] - * @throws InternalError - */ - private function getWantedRowIds(string $userId, int $tableId, ?array $filter = null, ?int $limit = null, ?int $offset = null): array { - $qb = $this->db->getQueryBuilder(); - - $qb->select('id') - ->from('tables_row_sleeves', 'sleeves') - ->where($qb->expr()->eq('table_id', $qb->createNamedParameter($tableId, IQueryBuilder::PARAM_INT))); - if($filter) { - $this->addFilterToQuery($qb, $filter, $userId); - } - if ($limit !== null) { - $qb->setMaxResults($limit); - } - if ($offset !== null) { - $qb->setFirstResult($offset); - } - - try { - $result = $this->db->executeQuery($qb->getSQL(), $qb->getParameters(), $qb->getParameterTypes()); - } catch (Exception $e) { - $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage(), ); - } - - return array_map(fn (array $item) => $item['id'], $result->fetchAll()); - } - - /** - * @param Column[] $columns - * @param int $tableId - * @param int|null $limit - * @param int|null $offset - * @param array|null $filter - * @param array|null $sort - * @param string|null $userId - * @return Row2[] - * @throws InternalError - */ - public function findAll(array $columns, int $tableId, int $limit = null, int $offset = null, array $filter = null, array $sort = null, string $userId = null): array { - $this->setColumns($columns); - $columnIdsArray = array_map(fn (Column $column) => $column->getId(), $columns); - - $wantedRowIdsArray = $this->getWantedRowIds($userId, $tableId, $filter, $limit, $offset); - - // TODO add sorting - - return $this->getRows($wantedRowIdsArray, $columnIdsArray); - } - - /** - * @param array $rowIds - * @param array $columnIds - * @return Row2[] - * @throws InternalError - */ - private function getRows(array $rowIds, array $columnIds): array { - $qb = $this->db->getQueryBuilder(); - - $qbSqlForColumnTypes = null; - foreach ($this->columnsHelper->get(['name']) as $columnType) { - $qbTmp = $this->db->getQueryBuilder(); - $qbTmp->select('*') - ->from('tables_row_cells_'.$columnType) - ->where($qb->expr()->in('column_id', $qb->createNamedParameter($columnIds, IQueryBuilder::PARAM_INT_ARRAY, ':columnIds'))) - ->andWhere($qb->expr()->in('row_id', $qb->createNamedParameter($rowIds, IQueryBuilder::PARAM_INT_ARRAY, ':rowsIds'))); - - if ($qbSqlForColumnTypes) { - $qbSqlForColumnTypes .= ' UNION ALL ' . $qbTmp->getSQL() . ' '; - } else { - $qbSqlForColumnTypes = '(' . $qbTmp->getSQL(); - } - } - $qbSqlForColumnTypes .= ')'; - - $qb->select('row_id', 'column_id', 'created_by', 'created_at', 't1.last_edit_by', 't1.last_edit_at', 'value', 'table_id') - ->from($qb->createFunction($qbSqlForColumnTypes), 't1') - ->innerJoin('t1', 'tables_row_sleeves', 'rowSleeve', 'rowSleeve.id = t1.row_id'); - - try { - $result = $this->db->executeQuery($qb->getSQL(), $qb->getParameters(), $qb->getParameterTypes()); - } catch (Exception $e) { - $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage(), ); - } - - try { - $sleeves = $this->rowSleeveMapper->findMultiple($rowIds); - } catch (Exception $e) { - $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); - } - - return $this->parseEntities($result, $sleeves); - } - - /** - * @throws InternalError - */ - private function addFilterToQuery(IQueryBuilder &$qb, array $filters, string $userId): void { - // TODO move this into service - $this->replaceMagicValues($filters, $userId); - - if (count($filters) > 0) { - $qb->andWhere( - $qb->expr()->orX( - ...$this->getFilterGroups($qb, $filters) - ) - ); - } - } - - private function replaceMagicValues(array &$filters, string $userId): void { - foreach ($filters as &$filterGroup) { - foreach ($filterGroup as &$filter) { - if(substr($filter['value'], 0, 1) === '@') { - $filter['value'] = $this->resolveSearchValue($filter['value'], $userId); - } - } - } - } - - /** - * @throws InternalError - */ - private function getFilterGroups(IQueryBuilder &$qb, array $filters): array { - $filterGroups = []; - foreach ($filters as $filterGroup) { - $tmp = $this->getFilter($qb, $filterGroup); - $filterGroups[] = $qb->expr()->andX(...$tmp); - } - return $filterGroups; - } - - /** - * @throws InternalError - */ - private function getFilter(IQueryBuilder &$qb, array $filterGroup): array { - $filterExpressions = []; - foreach ($filterGroup as $filter) { - $sql = $qb->expr()->in( - 'id', - $qb->createFunction($this->getFilterExpression($qb, $this->columns[$filter['columnId']], $filter['operator'], $filter['value'])->getSQL()) - ); - $filterExpressions[] = $sql; - } - return $filterExpressions; - } - - /** - * @throws InternalError - */ - private function getFilterExpression(IQueryBuilder $qb, Column $column, string $operator, string $value): IQueryBuilder { - /*if($column->getType() === 'number' && $column->getNumberDecimals() === 0) { - $paramType = IQueryBuilder::PARAM_INT; - $value = (int)$value; - } elseif ($column->getType() === 'datetime') { - $paramType = IQueryBuilder::PARAM_DATE; - } else { - $paramType = IQueryBuilder::PARAM_STR; - }*/ - - $paramType = $this->getColumnDbParamType($column); - $value = $this->formatValue($column, $value, 'in'); - - $qb2 = $this->db->getQueryBuilder(); - $qb2->select('row_id'); - $qb2->where($qb->expr()->eq('column_id', $qb->createNamedParameter($column->getId()), IQueryBuilder::PARAM_INT)); - $qb2->from('tables_row_cells_' . $column->getType()); - - switch ($operator) { - case 'begins-with': - return $qb2->andWhere($qb->expr()->like('value', $qb->createNamedParameter('%'.$this->db->escapeLikeParameter($value), $paramType))); - case 'ends-with': - return $qb2->andWhere($qb->expr()->like('value', $qb->createNamedParameter($this->db->escapeLikeParameter($value).'%', $paramType))); - case 'contains': - return $qb2->andWhere($qb->expr()->like('value', $qb->createNamedParameter('%'.$this->db->escapeLikeParameter($value).'%', $paramType))); - case 'is-equal': - return $qb2->andWhere($qb->expr()->eq('value', $qb->createNamedParameter($value, $paramType))); - case 'is-greater-than': - return $qb2->andWhere($qb->expr()->gt('value', $qb->createNamedParameter($value, $paramType))); - case 'is-greater-than-or-equal': - return $qb2->andWhere($qb->expr()->gte('value', $qb->createNamedParameter($value, $paramType))); - case 'is-lower-than': - return $qb2->andWhere($qb->expr()->lt('value', $qb->createNamedParameter($value, $paramType))); - case 'is-lower-than-or-equal': - return $qb2->andWhere($qb->expr()->lte('value', $qb->createNamedParameter($value, $paramType))); - case 'is-empty': - return $qb2->andWhere($qb->expr()->isNull('value')); - default: - throw new InternalError('Operator '.$operator.' is not supported.'); - } - } - - /** @noinspection DuplicatedCode */ - private function resolveSearchValue(string $magicValue, string $userId): string { - switch (ltrim($magicValue, '@')) { - case 'me': return $userId; - case 'my-name': return $this->userHelper->getUserDisplayName($userId); - case 'checked': return 'true'; - case 'unchecked': return 'false'; - case 'stars-0': return '0'; - case 'stars-1': return '1'; - case 'stars-2': return '2'; - case 'stars-3': return '3'; - case 'stars-4': return '4'; - case 'stars-5': return '5'; - case 'datetime-date-today': return date('Y-m-d') ? date('Y-m-d') : ''; - case 'datetime-date-start-of-year': return date('Y-01-01') ? date('Y-01-01') : ''; - case 'datetime-date-start-of-month': return date('Y-m-01') ? date('Y-m-01') : ''; - case 'datetime-date-start-of-week': - $day = date('w'); - $result = date('m-d-Y', strtotime('-'.$day.' days')); - return $result ?: ''; - case 'datetime-time-now': return date('H:i'); - case 'datetime-now': return date('Y-m-d H:i') ? date('Y-m-d H:i') : ''; - default: return $magicValue; - } - } - - /** - * @param IResult $result - * @param RowSleeve[] $sleeves - * @return Row2[] - * @throws InternalError - */ - private function parseEntities(IResult $result, array $sleeves): array { - $data = $result->fetchAll(); - - $rows = []; - foreach ($sleeves as $sleeve) { - $rows[$sleeve->getId()] = new Row2(); - $rows[$sleeve->getId()]->setId($sleeve->getId()); - $rows[$sleeve->getId()]->setCreatedBy($sleeve->getCreatedBy()); - $rows[$sleeve->getId()]->setCreatedAt($sleeve->getCreatedAt()); - $rows[$sleeve->getId()]->setLastEditBy($sleeve->getLastEditBy()); - $rows[$sleeve->getId()]->setLastEditAt($sleeve->getLastEditAt()); - $rows[$sleeve->getId()]->setTableId($sleeve->getTableId()); - } - - foreach ($data as $rowData) { - if (!isset($rowData['row_id']) || !isset($rows[$rowData['row_id']])) { - break; - } - - /* @var array $rowData */ - $rows[$rowData['row_id']]->addCell($rowData['column_id'], $this->formatValue($this->columns[$rowData['column_id']], $rowData['value'])); - } - - // format an array without keys - $return = []; - foreach ($rows as $row) { - $return[] = $row; - } - return $return; - } - - /** - * @throws InternalError - */ - public function isRowInViewPresent(int $rowId, View $view, string $userId): bool { - return in_array($rowId, $this->getWantedRowIds($userId, $view->getTableId(), $view->getFilterArray())); - } - - /** - * @param Row2 $row - * @param Column[] $columns - * @return Row2 - * @throws InternalError - * @throws Exception - */ - public function insert(Row2 $row, array $columns): Row2 { - if(!$columns) { - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': columns are missing'); - } - $this->setColumns($columns); - - // create a new row sleeve to get a new rowId - $rowSleeve = $this->createNewRowSleeve($row->getTableId()); - $row->setId($rowSleeve->getId()); - - // write all cells to its db-table - foreach ($row->getData() as $cell) { - $this->insertCell($rowSleeve->getId(), $cell['columnId'], $cell['value']); - } - - return $row; - } - - /** - * @throws InternalError - */ - public function update(Row2 $row, array $columns): Row2 { - if(!$columns) { - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': columns are missing'); - } - $this->setColumns($columns); - - // if nothing has changed - if (count($row->getChangedCells()) === 0) { - return $row; - } - - // update meta data for sleeve - try { - $sleeve = $this->rowSleeveMapper->find($row->getId()); - $this->updateMetaData($sleeve); - $this->rowSleeveMapper->update($sleeve); - } catch (DoesNotExistException|MultipleObjectsReturnedException|Exception $e) { - $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); - } - - // write all changed cells to its db-table - foreach ($row->getChangedCells() as $cell) { - $this->insertOrUpdateCell($sleeve->getId(), $cell['columnId'], $cell['value']); - } - - return $row; - } - - /** - * @throws Exception - */ - private function createNewRowSleeve(int $tableId): RowSleeve { - $rowSleeve = new RowSleeve(); - $rowSleeve->setTableId($tableId); - $this->updateMetaData($rowSleeve, true); - return $this->rowSleeveMapper->insert($rowSleeve); - } - - /** - * Updates the last_edit_by and last_edit_at data - * optional adds the created_by and created_at data - * - * @param RowSleeve|RowCellSuper $entity - * @param bool $setCreate - * @return void - */ - private function updateMetaData($entity, bool $setCreate = false): void { - $time = new DateTime(); - if ($setCreate) { - $entity->setCreatedBy($this->userId); - $entity->setCreatedAt($time->format('Y-m-d H:i:s')); - } - $entity->setLastEditBy($this->userId); - $entity->setLastEditAt($time->format('Y-m-d H:i:s')); - } - - /** - * Insert a cell to its specific db-table - * - * @throws InternalError - */ - private function insertCell(int $rowId, int $columnId, $value): void { - $cellClassName = 'OCA\Tables\Db\RowCell'.ucfirst($this->columns[$columnId]->getType()); - /** @var RowCellSuper $cell */ - $cell = new $cellClassName(); - - $cell->setRowIdWrapper($rowId); - $cell->setColumnIdWrapper($columnId); - $this->updateMetaData($cell); - - // insert new cell - $cellMapperClassName = 'OCA\Tables\Db\RowCell'.ucfirst($this->columns[$columnId]->getType()).'Mapper'; - /** @var QBMapper $cellMapper */ - try { - $cellMapper = Server::get($cellMapperClassName); - } catch (NotFoundExceptionInterface|ContainerExceptionInterface $e) { - $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); - } - - $v = $this->formatValue($this->columns[$columnId], $value, 'in'); - $cell->setValueWrapper($v); - - try { - $cellMapper->insert($cell); - } catch (Exception $e) { - $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); - } - } - - /** - * @param RowCellSuper $cell - * @param RowCellMapperSuper $mapper - * @param mixed $value the value should be parsed to the correct format within the row service - * @param Column $column - * @throws InternalError - */ - private function updateCell(RowCellSuper $cell, RowCellMapperSuper $mapper, $value, Column $column): void { - $v = $this->formatValue($column, $value, 'in'); - $cell->setValueWrapper($v); - $this->updateMetaData($cell); - $mapper->updateWrapper($cell); - } - - /** - * @throws InternalError - */ - private function insertOrUpdateCell(int $rowId, int $columnId, $value): void { - $cellMapperClassName = 'OCA\Tables\Db\RowCell'.ucfirst($this->columns[$columnId]->getType()).'Mapper'; - /** @var RowCellMapperSuper $cellMapper */ - try { - $cellMapper = Server::get($cellMapperClassName); - } catch (NotFoundExceptionInterface|ContainerExceptionInterface $e) { - $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); - } - try { - $cell = $cellMapper->findByRowAndColumn($rowId, $columnId); - $this->updateCell($cell, $cellMapper, $value, $this->columns[$columnId]); - } catch (DoesNotExistException $e) { - $this->insertCell($rowId, $columnId, $value); - } catch (MultipleObjectsReturnedException|Exception $e) { - $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); - } - } - - /** - * @param Column[] $columns - */ - private function setColumns(array $columns): void { - foreach ($columns as $column) { - $this->columns[$column->getId()] = $column; - } - } - - /** - * @param Column $column - * @param mixed $value - * @param 'out'|'in' $mode Parse the value for incoming requests that get send to the db or outgoing, from the db to the services - * @return mixed - * @throws InternalError - */ - private function formatValue(Column $column, $value, string $mode = 'out') { - $cellMapperClassName = 'OCA\Tables\Db\RowCell'.ucfirst($column->getType()).'Mapper'; - /** @var RowCellMapperSuper $cellMapper */ - try { - $cellMapper = Server::get($cellMapperClassName); - } catch (NotFoundExceptionInterface|ContainerExceptionInterface $e) { - $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); - } - if ($mode === 'out') { - return $cellMapper->parseValueOutgoing($column, $value); - } else { - return $cellMapper->parseValueIncoming($column, $value); - } - } - - /** - * @throws InternalError - */ - private function getColumnDbParamType(Column $column) { - $cellMapperClassName = 'OCA\Tables\Db\RowCell'.ucfirst($column->getType()).'Mapper'; - /** @var RowCellMapperSuper $cellMapper */ - try { - $cellMapper = Server::get($cellMapperClassName); - } catch (NotFoundExceptionInterface|ContainerExceptionInterface $e) { - $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); - } - return $cellMapper->getDbParamType(); - } - - /** - * @throws InternalError - */ - public function deleteDataForColumn(Column $column): void { - $cellMapperClassName = 'OCA\Tables\Db\RowCell' . ucfirst($column->getType()) . 'Mapper'; - /** @var RowCellMapperSuper $cellMapper */ - try { - $cellMapper = Server::get($cellMapperClassName); - } catch (NotFoundExceptionInterface|ContainerExceptionInterface $e) { - $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage()); - } - try { - $cellMapper->deleteAllForColumn($column->getId()); - } catch (Exception $e) { - $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); - } - } - - /** - * @param int $tableId - * @param Column[] $columns - * @return void - */ - public function deleteAllForTable(int $tableId, array $columns): void { - foreach ($columns as $column) { - try { - $this->deleteDataForColumn($column); - } catch (InternalError $e) { - $this->logger->error($e->getMessage(), ['exception' => $e]); - } - } - try { - $this->rowSleeveMapper->deleteAllForTable($tableId); - } catch (Exception $e) { - $this->logger->error($e->getMessage(), ['exception' => $e]); - } - } - - public function countRowsForTable(int $tableId): int { - return $this->rowSleeveMapper->countRows($tableId); - } - - /** - * @param View $view - * @param string $userId - * @param Column[] $columns - * @return int - */ - public function countRowsForView(View $view, string $userId, array $columns): int { - $this->setColumns($columns); - - $filter = $view->getFilterArray(); - try { - $rowIds = $this->getWantedRowIds($userId, $view->getTableId(), $filter); - } catch (InternalError $e) { - $this->logger->error($e->getMessage(), ['exception' => $e]); - $rowIds = []; - } - return count($rowIds); - } - -} diff --git a/lib/Db/RowMapper.php b/lib/Db/RowMapper.php index 6c88752d9..2905c351b 100644 --- a/lib/Db/RowMapper.php +++ b/lib/Db/RowMapper.php @@ -2,118 +2,312 @@ namespace OCA\Tables\Db; -use OCA\Tables\Db\ColumnTypes\DatetimeColumnQB; -use OCA\Tables\Db\ColumnTypes\IColumnTypeQB; -use OCA\Tables\Db\ColumnTypes\NumberColumnQB; -use OCA\Tables\Db\ColumnTypes\SelectionColumnQB; -use OCA\Tables\Db\ColumnTypes\SuperColumnQB; -use OCA\Tables\Db\ColumnTypes\TextColumnQB; +use DateTime; use OCA\Tables\Errors\InternalError; +use OCA\Tables\Errors\NotFoundError; +use OCA\Tables\Helper\ColumnsHelper; use OCA\Tables\Helper\UserHelper; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\MultipleObjectsReturnedException; use OCP\AppFramework\Db\QBMapper; use OCP\DB\Exception; +use OCP\DB\IResult; use OCP\DB\QueryBuilder\IQueryBuilder; -use OCP\DB\QueryBuilder\IQueryFunction; use OCP\IDBConnection; use OCP\Server; use Psr\Container\ContainerExceptionInterface; use Psr\Container\NotFoundExceptionInterface; use Psr\Log\LoggerInterface; -/** @template-extends QBMapper */ -class RowMapper extends QBMapper { - protected string $table = 'tables_rows'; - protected TextColumnQB $textColumnQB; - protected SelectionColumnQB $selectionColumnQB; - protected NumberColumnQB $numberColumnQB; - protected DatetimeColumnQB $datetimeColumnQB; - protected SuperColumnQB $genericColumnQB; - protected ColumnMapper $columnMapper; - protected LoggerInterface $logger; +class RowMapper { + private RowSleeveMapper $rowSleeveMapper; + private ?string $userId = null; + private IDBConnection $db; + private LoggerInterface $logger; protected UserHelper $userHelper; - protected int $platform; + /* @var Column[] $columns */ + private array $columns = []; - public function __construct(IDBConnection $db, LoggerInterface $logger, TextColumnQB $textColumnQB, SelectionColumnQB $selectionColumnQB, NumberColumnQB $numberColumnQB, DatetimeColumnQB $datetimeColumnQB, SuperColumnQB $columnQB, ColumnMapper $columnMapper, UserHelper $userHelper) { - parent::__construct($db, $this->table, Row::class); + private ColumnsHelper $columnsHelper; + + public function __construct(?string $userId, IDBConnection $db, LoggerInterface $logger, UserHelper $userHelper, RowSleeveMapper $rowSleeveMapper, ColumnsHelper $columnsHelper) { + $this->rowSleeveMapper = $rowSleeveMapper; + $this->userId = $userId; + $this->db = $db; $this->logger = $logger; - $this->textColumnQB = $textColumnQB; - $this->numberColumnQB = $numberColumnQB; - $this->selectionColumnQB = $selectionColumnQB; - $this->datetimeColumnQB = $datetimeColumnQB; - $this->genericColumnQB = $columnQB; - $this->columnMapper = $columnMapper; $this->userHelper = $userHelper; - $this->setPlatform(); + $this->columnsHelper = $columnsHelper; } - private function setPlatform() { - if (str_contains(strtolower(get_class($this->db->getDatabasePlatform())), 'postgres')) { - $this->platform = IColumnTypeQB::DB_PLATFORM_PGSQL; - } elseif (str_contains(strtolower(get_class($this->db->getDatabasePlatform())), 'sqlite')) { - $this->platform = IColumnTypeQB::DB_PLATFORM_SQLITE; - } else { - $this->platform = IColumnTypeQB::DB_PLATFORM_MYSQL; + /** + * @throws InternalError + */ + public function delete(Row $row): Row { + foreach ($this->columnsHelper->get(['name']) as $columnType) { + $cellMapperClassName = 'OCA\Tables\Db\RowCell' . ucfirst($columnType) . 'Mapper'; + /** @var RowCellMapperSuper $cellMapper */ + try { + $cellMapper = Server::get($cellMapperClassName); + } catch (NotFoundExceptionInterface|ContainerExceptionInterface $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage()); + } + try { + $cellMapper->deleteAllForRow($row->getId()); + } catch (Exception $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + } } - $this->genericColumnQB->setPlatform($this->platform); - $this->textColumnQB->setPlatform($this->platform); - $this->numberColumnQB->setPlatform($this->platform); - $this->selectionColumnQB->setPlatform($this->platform); - $this->datetimeColumnQB->setPlatform($this->platform); + try { + $this->rowSleeveMapper->deleteById($row->getId()); + } catch (Exception $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + } + + return $row; } /** * @param int $id - * + * @param Column[] $columns * @return Row + * @throws InternalError + * @throws NotFoundError + */ + public function find(int $id, array $columns): Row { + $this->setColumns($columns); + $columnIdsArray = array_map(fn (Column $column) => $column->getId(), $columns); + $rows = $this->getRows([$id], $columnIdsArray); + if (count($rows) === 1) { + return $rows[0]; + } elseif (count($rows) === 0) { + $e = new Exception('Wanted row not found.'); + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new NotFoundError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + } else { + $e = new Exception('Too many results for one wanted row.'); + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + } + } + + /** * @throws DoesNotExistException - * @throws Exception * @throws MultipleObjectsReturnedException + * @throws Exception + */ + public function getTableIdForRow(int $rowId): ?int { + $rowSleeve = $this->rowSleeveMapper->find($rowId); + return $rowSleeve->getTableId(); + } + + /** + * @param string $userId + * @param int $tableId + * @param array|null $filter + * @param int|null $limit + * @param int|null $offset + * @return int[] + * @throws InternalError */ - public function find(int $id): Row { + private function getWantedRowIds(string $userId, int $tableId, ?array $filter = null, ?int $limit = null, ?int $offset = null): array { $qb = $this->db->getQueryBuilder(); - $qb->select('t1.*') - ->from($this->table, 't1') - ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))); - return $this->findEntity($qb); + + $qb->select('id') + ->from('tables_row_sleeves', 'sleeves') + ->where($qb->expr()->eq('table_id', $qb->createNamedParameter($tableId, IQueryBuilder::PARAM_INT))); + if($filter) { + $this->addFilterToQuery($qb, $filter, $userId); + } + if ($limit !== null) { + $qb->setMaxResults($limit); + } + if ($offset !== null) { + $qb->setFirstResult($offset); + } + + try { + $result = $this->db->executeQuery($qb->getSQL(), $qb->getParameters(), $qb->getParameterTypes()); + } catch (Exception $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage(), ); + } + + return array_map(fn (array $item) => $item['id'], $result->fetchAll()); + } + + /** + * @param Column[] $columns + * @param int $tableId + * @param int|null $limit + * @param int|null $offset + * @param array|null $filter + * @param array|null $sort + * @param string|null $userId + * @return Row[] + * @throws InternalError + */ + public function findAll(array $columns, int $tableId, int $limit = null, int $offset = null, array $filter = null, array $sort = null, string $userId = null): array { + $this->setColumns($columns); + $columnIdsArray = array_map(fn (Column $column) => $column->getId(), $columns); + + $wantedRowIdsArray = $this->getWantedRowIds($userId, $tableId, $filter, $limit, $offset); + + // TODO add sorting + + return $this->getRows($wantedRowIdsArray, $columnIdsArray); } - private function buildFilterByColumnType($qb, array $filter, string $filterId): ?IQueryFunction { + /** + * @param array $rowIds + * @param array $columnIds + * @return Row[] + * @throws InternalError + */ + private function getRows(array $rowIds, array $columnIds): array { + $qb = $this->db->getQueryBuilder(); + + $qbSqlForColumnTypes = null; + foreach ($this->columnsHelper->get(['name']) as $columnType) { + $qbTmp = $this->db->getQueryBuilder(); + $qbTmp->select('*') + ->from('tables_row_cells_'.$columnType) + ->where($qb->expr()->in('column_id', $qb->createNamedParameter($columnIds, IQueryBuilder::PARAM_INT_ARRAY, ':columnIds'))) + ->andWhere($qb->expr()->in('row_id', $qb->createNamedParameter($rowIds, IQueryBuilder::PARAM_INT_ARRAY, ':rowsIds'))); + + if ($qbSqlForColumnTypes) { + $qbSqlForColumnTypes .= ' UNION ALL ' . $qbTmp->getSQL() . ' '; + } else { + $qbSqlForColumnTypes = '(' . $qbTmp->getSQL(); + } + } + $qbSqlForColumnTypes .= ')'; + + $qb->select('row_id', 'column_id', 'created_by', 'created_at', 't1.last_edit_by', 't1.last_edit_at', 'value', 'table_id') + ->from($qb->createFunction($qbSqlForColumnTypes), 't1') + ->innerJoin('t1', 'tables_row_sleeves', 'rowSleeve', 'rowSleeve.id = t1.row_id'); + try { - $columnQbClassName = 'OCA\Tables\Db\ColumnTypes\\'; - $type = explode("-", $filter['columnType'])[0]; + $result = $this->db->executeQuery($qb->getSQL(), $qb->getParameters(), $qb->getParameterTypes()); + } catch (Exception $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage(), ); + } - $columnQbClassName .= ucfirst($type).'ColumnQB'; + try { + $sleeves = $this->rowSleeveMapper->findMultiple($rowIds); + } catch (Exception $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + } - /** @var IColumnTypeQB $columnQb */ - $columnQb = Server::get($columnQbClassName); - return $columnQb->addWhereFilterExpression($qb, $filter, $filterId); - } catch (NotFoundExceptionInterface|ContainerExceptionInterface $e) { - $this->logger->debug('Column type query builder class not found'); + return $this->parseEntities($result, $sleeves); + } + + /** + * @throws InternalError + */ + private function addFilterToQuery(IQueryBuilder &$qb, array $filters, string $userId): void { + // TODO move this into service + $this->replaceMagicValues($filters, $userId); + + if (count($filters) > 0) { + $qb->andWhere( + $qb->expr()->orX( + ...$this->getFilterGroups($qb, $filters) + ) + ); } - return null; } - private function getInnerFilterExpressions($qb, $filterGroup, int $groupIndex): array { - $innerFilterExpressions = []; - foreach ($filterGroup as $index => $filter) { - $innerFilterExpressions[] = $this->buildFilterByColumnType($qb, $filter, $groupIndex.$index); + private function replaceMagicValues(array &$filters, string $userId): void { + foreach ($filters as &$filterGroup) { + foreach ($filterGroup as &$filter) { + if(substr($filter['value'], 0, 1) === '@') { + $filter['value'] = $this->resolveSearchValue($filter['value'], $userId); + } + } } - return $innerFilterExpressions; } - private function getFilterGroups($qb, $filters): array { + /** + * @throws InternalError + */ + private function getFilterGroups(IQueryBuilder &$qb, array $filters): array { $filterGroups = []; - foreach ($filters as $groupIndex => $filterGroup) { - $filterGroups[] = $qb->expr()->andX(...$this->getInnerFilterExpressions($qb, $filterGroup, $groupIndex)); + foreach ($filters as $filterGroup) { + $tmp = $this->getFilter($qb, $filterGroup); + $filterGroups[] = $qb->expr()->andX(...$tmp); } return $filterGroups; } - private function resolveSearchValue(string $unresolvedSearchValue, string $userId): string { - switch (ltrim($unresolvedSearchValue, '@')) { + /** + * @throws InternalError + */ + private function getFilter(IQueryBuilder &$qb, array $filterGroup): array { + $filterExpressions = []; + foreach ($filterGroup as $filter) { + $sql = $qb->expr()->in( + 'id', + $qb->createFunction($this->getFilterExpression($qb, $this->columns[$filter['columnId']], $filter['operator'], $filter['value'])->getSQL()) + ); + $filterExpressions[] = $sql; + } + return $filterExpressions; + } + + /** + * @throws InternalError + */ + private function getFilterExpression(IQueryBuilder $qb, Column $column, string $operator, string $value): IQueryBuilder { + /*if($column->getType() === 'number' && $column->getNumberDecimals() === 0) { + $paramType = IQueryBuilder::PARAM_INT; + $value = (int)$value; + } elseif ($column->getType() === 'datetime') { + $paramType = IQueryBuilder::PARAM_DATE; + } else { + $paramType = IQueryBuilder::PARAM_STR; + }*/ + + $paramType = $this->getColumnDbParamType($column); + $value = $this->formatValue($column, $value, 'in'); + + $qb2 = $this->db->getQueryBuilder(); + $qb2->select('row_id'); + $qb2->where($qb->expr()->eq('column_id', $qb->createNamedParameter($column->getId()), IQueryBuilder::PARAM_INT)); + $qb2->from('tables_row_cells_' . $column->getType()); + + switch ($operator) { + case 'begins-with': + return $qb2->andWhere($qb->expr()->like('value', $qb->createNamedParameter('%'.$this->db->escapeLikeParameter($value), $paramType))); + case 'ends-with': + return $qb2->andWhere($qb->expr()->like('value', $qb->createNamedParameter($this->db->escapeLikeParameter($value).'%', $paramType))); + case 'contains': + return $qb2->andWhere($qb->expr()->like('value', $qb->createNamedParameter('%'.$this->db->escapeLikeParameter($value).'%', $paramType))); + case 'is-equal': + return $qb2->andWhere($qb->expr()->eq('value', $qb->createNamedParameter($value, $paramType))); + case 'is-greater-than': + return $qb2->andWhere($qb->expr()->gt('value', $qb->createNamedParameter($value, $paramType))); + case 'is-greater-than-or-equal': + return $qb2->andWhere($qb->expr()->gte('value', $qb->createNamedParameter($value, $paramType))); + case 'is-lower-than': + return $qb2->andWhere($qb->expr()->lt('value', $qb->createNamedParameter($value, $paramType))); + case 'is-lower-than-or-equal': + return $qb2->andWhere($qb->expr()->lte('value', $qb->createNamedParameter($value, $paramType))); + case 'is-empty': + return $qb2->andWhere($qb->expr()->isNull('value')); + default: + throw new InternalError('Operator '.$operator.' is not supported.'); + } + } + + /** @noinspection DuplicatedCode */ + private function resolveSearchValue(string $magicValue, string $userId): string { + switch (ltrim($magicValue, '@')) { case 'me': return $userId; case 'my-name': return $this->userHelper->getUserDisplayName($userId); case 'checked': return 'true'; @@ -133,295 +327,319 @@ private function resolveSearchValue(string $unresolvedSearchValue, string $userI return $result ?: ''; case 'datetime-time-now': return date('H:i'); case 'datetime-now': return date('Y-m-d H:i') ? date('Y-m-d H:i') : ''; - default: return $unresolvedSearchValue; + default: return $magicValue; } } - private function addOrderByRules(IQueryBuilder $qb, $sortArray) { - foreach ($sortArray as $index => $sortRule) { - $sortMode = $sortRule['mode']; - if (!in_array($sortMode, ['ASC', 'DESC'])) { - continue; - } - $sortColumnPlaceholder = 'sortColumn'.$index; - if ($sortRule['columnId'] < 0) { - try { - $orderString = SuperColumnQB::getMetaColumnName($sortRule['columnId']); - } catch (InternalError $e) { - return; - } - } else { - if ($this->platform === IColumnTypeQB::DB_PLATFORM_PGSQL) { - $orderString = 'c'.$sortRule['columnId'].'->>\'value\''; - } elseif ($this->platform === IColumnTypeQB::DB_PLATFORM_SQLITE) { - // here is an error for (multiple) sorting, works only for the first column at the moment - $orderString = 'json_extract(t2.value, "$.value")'; - } else { // mariadb / mysql - $orderString = 'JSON_EXTRACT(data, CONCAT( JSON_UNQUOTE(JSON_SEARCH(JSON_EXTRACT(data, \'$[*].columnId\'), \'one\', :'.$sortColumnPlaceholder.')), \'.value\'))'; - } - if (str_starts_with($sortRule['columnType'], 'number')) { - $orderString = 'CAST('.$orderString.' as decimal)'; - } + /** + * @param IResult $result + * @param RowSleeve[] $sleeves + * @return Row[] + * @throws InternalError + */ + private function parseEntities(IResult $result, array $sleeves): array { + $data = $result->fetchAll(); + + $rows = []; + foreach ($sleeves as $sleeve) { + $rows[$sleeve->getId()] = new Row(); + $rows[$sleeve->getId()]->setId($sleeve->getId()); + $rows[$sleeve->getId()]->setCreatedBy($sleeve->getCreatedBy()); + $rows[$sleeve->getId()]->setCreatedAt($sleeve->getCreatedAt()); + $rows[$sleeve->getId()]->setLastEditBy($sleeve->getLastEditBy()); + $rows[$sleeve->getId()]->setLastEditAt($sleeve->getLastEditAt()); + $rows[$sleeve->getId()]->setTableId($sleeve->getTableId()); + } + + foreach ($data as $rowData) { + if (!isset($rowData['row_id']) || !isset($rows[$rowData['row_id']])) { + break; } - $qb->addOrderBy($qb->createFunction($orderString), $sortMode); - $qb->setParameter($sortColumnPlaceholder, $sortRule['columnId'], IQueryBuilder::PARAM_INT); + /* @var array $rowData */ + $rows[$rowData['row_id']]->addCell($rowData['column_id'], $this->formatValue($this->columns[$rowData['column_id']], $rowData['value'])); + } + + // format an array without keys + $return = []; + foreach ($rows as $row) { + $return[] = $row; } + return $return; } /** - * @param View $view - * @param $userId - * @return int * @throws InternalError */ - public function countRowsForView(View $view, $userId): int { - $qb = $this->db->getQueryBuilder(); - $qb->select($qb->func()->count('*', 'counter')) - ->from($this->table, 't1') - ->where($qb->expr()->eq('table_id', $qb->createNamedParameter($view->getTableId(), IQueryBuilder::PARAM_INT))); + public function isRowInViewPresent(int $rowId, View $view, string $userId): bool { + return in_array($rowId, $this->getWantedRowIds($userId, $view->getTableId(), $view->getFilterArray())); + } - $neededColumnIds = $this->getAllColumnIdsFromView($view, $qb); - try { - $neededColumns = $this->columnMapper->getColumnTypes($neededColumnIds); - } catch (Exception $e) { - throw new InternalError('Could not get column types to count rows'); + /** + * @param Row $row + * @param Column[] $columns + * @return Row + * @throws InternalError + * @throws Exception + */ + public function insert(Row $row, array $columns): Row { + if(!$columns) { + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': columns are missing'); } + $this->setColumns($columns); - // Filter + // create a new row sleeve to get a new rowId + $rowSleeve = $this->createNewRowSleeve($row->getTableId()); + $row->setId($rowSleeve->getId()); - $this->addFilterToQuery($qb, $view, $neededColumns, $userId); - - try { - $result = $this->findOneQuery($qb); - return (int)$result['counter']; - } catch (DoesNotExistException|MultipleObjectsReturnedException|Exception $e) { - $this->logger->warning('Exception occurred: '.$e->getMessage().' Returning 0.'); - return 0; + // write all cells to its db-table + foreach ($row->getData() as $cell) { + $this->insertCell($rowSleeve->getId(), $cell['columnId'], $cell['value']); } - } + return $row; + } - public function getRowIdsOfView(View $view, $userId): array { - $qb = $this->db->getQueryBuilder(); - $qb->select('t1.id') - ->from($this->table, 't1') - ->where($qb->expr()->eq('table_id', $qb->createNamedParameter($view->getTableId(), IQueryBuilder::PARAM_INT))); - - $neededColumnIds = $this->getAllColumnIdsFromView($view, $qb); - $neededColumns = $this->columnMapper->getColumnTypes($neededColumnIds); + /** + * @throws InternalError + */ + public function update(Row $row, array $columns): Row { + if(!$columns) { + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': columns are missing'); + } + $this->setColumns($columns); - // Filter + // if nothing has changed + if (count($row->getChangedCells()) === 0) { + return $row; + } - $this->addFilterToQuery($qb, $view, $neededColumns, $userId); - $result = $qb->executeQuery(); + // update meta data for sleeve try { - $ids = []; - while ($row = $result->fetch()) { - $ids[] = $row['id']; - } - return $ids; - } finally { - $result->closeCursor(); + $sleeve = $this->rowSleeveMapper->find($row->getId()); + $this->updateMetaData($sleeve); + $this->rowSleeveMapper->update($sleeve); + } catch (DoesNotExistException|MultipleObjectsReturnedException|Exception $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); } - } - - private function addFilterToQuery(IQueryBuilder $qb, View $view, array $neededColumnTypes, string $userId): void { - $enrichedFilters = $view->getFilterArray(); - if (count($enrichedFilters) > 0) { - foreach ($enrichedFilters as &$filterGroup) { - foreach ($filterGroup as &$filter) { - $filter['columnType'] = $neededColumnTypes[$filter['columnId']]; - // TODO move resolution for magic fields to service layer - if(str_starts_with((string) $filter['value'], '@')) { - $filter['value'] = $this->resolveSearchValue((string) $filter['value'], $userId); - } - } - } - $qb->andWhere( - $qb->expr()->orX( - ...$this->getFilterGroups($qb, $enrichedFilters) - ) - ); + // write all changed cells to its db-table + foreach ($row->getChangedCells() as $cell) { + $this->insertOrUpdateCell($sleeve->getId(), $cell['columnId'], $cell['value']); } + + return $row; } /** - * @param int $tableId - * @param int|null $limit - * @param int|null $offset - * @return array * @throws Exception */ - public function findAllByTable(int $tableId, ?int $limit = null, ?int $offset = null): array { - $qb = $this->db->getQueryBuilder(); - $qb->select('t1.*') - ->from($this->table, 't1') - ->where($qb->expr()->eq('table_id', $qb->createNamedParameter($tableId))); + private function createNewRowSleeve(int $tableId): RowSleeve { + $rowSleeve = new RowSleeve(); + $rowSleeve->setTableId($tableId); + $this->updateMetaData($rowSleeve, true); + return $this->rowSleeveMapper->insert($rowSleeve); + } - if ($limit !== null) { - $qb->setMaxResults($limit); - } - if ($offset !== null) { - $qb->setFirstResult($offset); + /** + * Updates the last_edit_by and last_edit_at data + * optional adds the created_by and created_at data + * + * @param RowSleeve|RowCellSuper $entity + * @param bool $setCreate + * @return void + */ + private function updateMetaData($entity, bool $setCreate = false): void { + $time = new DateTime(); + if ($setCreate) { + $entity->setCreatedBy($this->userId); + $entity->setCreatedAt($time->format('Y-m-d H:i:s')); } - - return $this->findEntities($qb); + $entity->setLastEditBy($this->userId); + $entity->setLastEditAt($time->format('Y-m-d H:i:s')); } /** - * @param View $view - * @param string $userId - * @param int|null $limit - * @param int|null $offset - * @return array - * @throws Exception + * Insert a cell to its specific db-table + * + * @throws InternalError */ - public function findAllByView(View $view, string $userId, ?int $limit = null, ?int $offset = null): array { - $qb = $this->db->getQueryBuilder(); - $qb->select('t1.*') - ->from($this->table, 't1') - ->where($qb->expr()->eq('table_id', $qb->createNamedParameter($view->getTableId(), IQueryBuilder::PARAM_INT))); - - - $neededColumnIds = $this->getAllColumnIdsFromView($view, $qb); - $neededColumnsTypes = $this->columnMapper->getColumnTypes($neededColumnIds); - - // Filter - - $this->addFilterToQuery($qb, $view, $neededColumnsTypes, $userId); + private function insertCell(int $rowId, int $columnId, $value): void { + $cellClassName = 'OCA\Tables\Db\RowCell'.ucfirst($this->columns[$columnId]->getType()); + /** @var RowCellSuper $cell */ + $cell = new $cellClassName(); + + $cell->setRowIdWrapper($rowId); + $cell->setColumnIdWrapper($columnId); + $this->updateMetaData($cell); + + // insert new cell + $cellMapperClassName = 'OCA\Tables\Db\RowCell'.ucfirst($this->columns[$columnId]->getType()).'Mapper'; + /** @var QBMapper $cellMapper */ + try { + $cellMapper = Server::get($cellMapperClassName); + } catch (NotFoundExceptionInterface|ContainerExceptionInterface $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + } - // Sorting + $v = $this->formatValue($this->columns[$columnId], $value, 'in'); + $cell->setValueWrapper($v); - $enrichedSort = $view->getSortArray(); - foreach ($enrichedSort as &$sort) { - $sort['columnType'] = $neededColumnsTypes[$sort['columnId']]; + try { + $cellMapper->insert($cell); + } catch (Exception $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); } - $this->addOrderByRules($qb, $enrichedSort); + } - if ($limit !== null) { - $qb->setMaxResults($limit); - } - if ($offset !== null) { - $qb->setFirstResult($offset); - } - $rows = $this->findEntities($qb); - foreach ($rows as &$row) { - $row->setDataArray(array_filter($row->getDataArray(), function ($item) use ($view) { - return in_array($item['columnId'], $view->getColumnsArray()); - })); - } - return $rows; + /** + * @param RowCellSuper $cell + * @param RowCellMapperSuper $mapper + * @param mixed $value the value should be parsed to the correct format within the row service + * @param Column $column + * @throws InternalError + */ + private function updateCell(RowCellSuper $cell, RowCellMapperSuper $mapper, $value, Column $column): void { + $v = $this->formatValue($column, $value, 'in'); + $cell->setValueWrapper($v); + $this->updateMetaData($cell); + $mapper->updateWrapper($cell); } - private function getAllColumnIdsFromView(View $view, IQueryBuilder $qb): array { - $neededColumnIds = []; - $filters = $view->getFilterArray(); - $sorts = $view->getSortArray(); - foreach ($filters as $filterGroup) { - foreach ($filterGroup as $filter) { - $neededColumnIds[] = $filter['columnId']; - } - } - foreach ($sorts as $sortRule) { - $neededColumnIds[] = $sortRule['columnId']; + /** + * @throws InternalError + */ + private function insertOrUpdateCell(int $rowId, int $columnId, $value): void { + $cellMapperClassName = 'OCA\Tables\Db\RowCell'.ucfirst($this->columns[$columnId]->getType()).'Mapper'; + /** @var RowCellMapperSuper $cellMapper */ + try { + $cellMapper = Server::get($cellMapperClassName); + } catch (NotFoundExceptionInterface|ContainerExceptionInterface $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); } - $neededColumnIds = array_unique($neededColumnIds); - if ($this->platform === IColumnTypeQB::DB_PLATFORM_PGSQL) { - foreach ($neededColumnIds as $columnId) { - if ($columnId >= 0) { - /** @psalm-suppress ImplicitToStringCast */ - $qb->leftJoin("t1", $qb->createFunction('json_array_elements(t1.data)'), 'c' . intval($columnId), $qb->createFunction("CAST(c".intval($columnId).".value->>'columnId' AS int) = ".$columnId)); - // TODO Security - } - } + try { + $cell = $cellMapper->findByRowAndColumn($rowId, $columnId); + $this->updateCell($cell, $cellMapper, $value, $this->columns[$columnId]); + } catch (DoesNotExistException $e) { + $this->insertCell($rowId, $columnId, $value); + } catch (MultipleObjectsReturnedException|Exception $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); } - return $neededColumnIds; } /** - * @throws MultipleObjectsReturnedException - * @throws DoesNotExistException - * @throws Exception + * @param Column[] $columns */ - public function findNext(int $offsetId = -1): Row { - $qb = $this->db->getQueryBuilder(); - $qb->select('t1.*') - ->from($this->table, 't1') - ->where($qb->expr()->gt('id', $qb->createNamedParameter($offsetId))) - ->setMaxResults(1) - ->orderBy('id', 'ASC'); - - return $this->findEntity($qb); + private function setColumns(array $columns): void { + foreach ($columns as $column) { + $this->columns[$column->getId()] = $column; + } } /** - * @return int affected rows - * @throws Exception + * @param Column $column + * @param mixed $value + * @param 'out'|'in' $mode Parse the value for incoming requests that get send to the db or outgoing, from the db to the services + * @return mixed + * @throws InternalError */ - public function deleteAllByTable(int $tableId): int { - $qb = $this->db->getQueryBuilder(); - $qb->delete($this->tableName) - ->where( - $qb->expr()->eq('table_id', $qb->createNamedParameter($tableId)) - ); - return $qb->executeStatement(); + private function formatValue(Column $column, $value, string $mode = 'out') { + $cellMapperClassName = 'OCA\Tables\Db\RowCell'.ucfirst($column->getType()).'Mapper'; + /** @var RowCellMapperSuper $cellMapper */ + try { + $cellMapper = Server::get($cellMapperClassName); + } catch (NotFoundExceptionInterface|ContainerExceptionInterface $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + } + if ($mode === 'out') { + return $cellMapper->parseValueOutgoing($column, $value); + } else { + return $cellMapper->parseValueIncoming($column, $value); + } } /** - * @throws Exception + * @throws InternalError */ - public function findAllWithColumn(int $columnId): array { - $qb = $this->db->getQueryBuilder(); - $qb->select('*') - ->from($this->table, 't1'); - - $this->genericColumnQB->addWhereForFindAllWithColumn($qb, $columnId); + private function getColumnDbParamType(Column $column) { + $cellMapperClassName = 'OCA\Tables\Db\RowCell'.ucfirst($column->getType()).'Mapper'; + /** @var RowCellMapperSuper $cellMapper */ + try { + $cellMapper = Server::get($cellMapperClassName); + } catch (NotFoundExceptionInterface|ContainerExceptionInterface $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + } + return $cellMapper->getDbParamType(); + } - return $this->findEntities($qb); + /** + * @throws InternalError + */ + public function deleteDataForColumn(Column $column): void { + $cellMapperClassName = 'OCA\Tables\Db\RowCell' . ucfirst($column->getType()) . 'Mapper'; + /** @var RowCellMapperSuper $cellMapper */ + try { + $cellMapper = Server::get($cellMapperClassName); + } catch (NotFoundExceptionInterface|ContainerExceptionInterface $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage()); + } + try { + $cellMapper->deleteAllForColumn($column->getId()); + } catch (Exception $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + } } /** * @param int $tableId - * @return int + * @param Column[] $columns + * @return void */ - public function countRows(int $tableId): int { - $qb = $this->db->getQueryBuilder(); - $qb->select($qb->func()->count('*', 'counter')); - $qb->from($this->table, 't1'); - $qb->where( - $qb->expr()->eq('table_id', $qb->createNamedParameter($tableId)) - ); - + public function deleteAllForTable(int $tableId, array $columns): void { + foreach ($columns as $column) { + try { + $this->deleteDataForColumn($column); + } catch (InternalError $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + } + } try { - $result = $this->findOneQuery($qb); - return (int)$result['counter']; - } catch (DoesNotExistException|MultipleObjectsReturnedException|Exception $e) { - $this->logger->warning('Exception occurred: '.$e->getMessage().' Returning 0.'); - return 0; + $this->rowSleeveMapper->deleteAllForTable($tableId); + } catch (Exception $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); } } + public function countRowsForTable(int $tableId): int { + return $this->rowSleeveMapper->countRows($tableId); + } + /** - * @param int $id * @param View $view - * @return Row - * @throws DoesNotExistException - * @throws Exception - * @throws MultipleObjectsReturnedException + * @param string $userId + * @param Column[] $columns + * @return int */ - public function findByView(int $id, View $view): Row { - $qb = $this->db->getQueryBuilder(); - $qb->select('t1.*') - ->from($this->table, 't1') - ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))); - $row = $this->findEntity($qb); - - $row->setDataArray(array_filter($row->getDataArray(), function ($item) use ($view) { - return in_array($item['columnId'], $view->getColumnsArray()); - })); + public function countRowsForView(View $view, string $userId, array $columns): int { + $this->setColumns($columns); - return $row; + $filter = $view->getFilterArray(); + try { + $rowIds = $this->getWantedRowIds($userId, $view->getTableId(), $filter); + } catch (InternalError $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + $rowIds = []; + } + return count($rowIds); } + } diff --git a/lib/Service/RowService.php b/lib/Service/RowService.php index c23ca09a3..cbc34f594 100644 --- a/lib/Service/RowService.php +++ b/lib/Service/RowService.php @@ -4,10 +4,10 @@ use OCA\Tables\Db\Column; use OCA\Tables\Db\ColumnMapper; +use OCA\Tables\Db\LegacyRow; use OCA\Tables\Db\Row; -use OCA\Tables\Db\Row2; -use OCA\Tables\Db\Row2Mapper; use OCA\Tables\Db\RowMapper; +use OCA\Tables\Db\LegacyRowMapper; use OCA\Tables\Db\Table; use OCA\Tables\Db\TableMapper; use OCA\Tables\Db\View; @@ -29,15 +29,15 @@ * @psalm-import-type TablesRow from ResponseDefinitions */ class RowService extends SuperService { - private RowMapper $mapper; + private LegacyRowMapper $mapper; private ColumnMapper $columnMapper; private ViewMapper $viewMapper; private TableMapper $tableMapper; - private Row2Mapper $row2Mapper; + private RowMapper $row2Mapper; private array $tmpRows = []; // holds already loaded rows as a small cache public function __construct(PermissionsService $permissionsService, LoggerInterface $logger, ?string $userId, - RowMapper $mapper, ColumnMapper $columnMapper, ViewMapper $viewMapper, TableMapper $tableMapper, Row2Mapper $row2Mapper) { + LegacyRowMapper $mapper, ColumnMapper $columnMapper, ViewMapper $viewMapper, TableMapper $tableMapper, RowMapper $row2Mapper) { parent::__construct($logger, $userId, $permissionsService); $this->mapper = $mapper; $this->columnMapper = $columnMapper; @@ -47,11 +47,11 @@ public function __construct(PermissionsService $permissionsService, LoggerInterf } /** - * @param Row2[] $rows + * @param Row[] $rows * @psalm-return TablesRow[] */ public function formatRows(array $rows): array { - return array_map(fn (Row2 $row) => $row->jsonSerialize(), $rows); + return array_map(fn (Row $row) => $row->jsonSerialize(), $rows); } /** @@ -59,7 +59,7 @@ public function formatRows(array $rows): array { * @param string $userId * @param ?int $limit * @param ?int $offset - * @return Row2[] + * @return Row[] * @throws InternalError * @throws PermissionError */ @@ -83,7 +83,7 @@ public function findAllByTable(int $tableId, string $userId, ?int $limit = null, * @param string $userId * @param int|null $limit * @param int|null $offset - * @return Row2[] + * @return Row[] * @throws DoesNotExistException * @throws InternalError * @throws MultipleObjectsReturnedException @@ -110,12 +110,12 @@ public function findAllByView(int $viewId, string $userId, ?int $limit = null, ? /** * @param int $id - * @return Row2 + * @return Row * @throws InternalError * @throws NotFoundError * @throws PermissionError */ - public function find(int $id): Row2 { + public function find(int $id): Row { try { $columns = $this->columnMapper->findAllByTable($id); } catch (Exception $e) { @@ -145,14 +145,14 @@ public function find(int $id): Row2 { * @param int|null $tableId * @param int|null $viewId * @param list $data - * @return Row2 + * @return Row * * @throws NotFoundError * @throws PermissionError * @throws Exception * @throws InternalError */ - public function create(?int $tableId, ?int $viewId, array $data): Row2 { + public function create(?int $tableId, ?int $viewId, array $data): Row { if ($this->userId === null || $this->userId === '') { $e = new \Exception('No user id in context, but needed.'); $this->logger->error($e->getMessage(), ['exception' => $e]); @@ -200,7 +200,7 @@ public function create(?int $tableId, ?int $viewId, array $data): Row2 { $data = $this->cleanupData($data, $columns, $tableId, $viewId); // perf - $row2 = new Row2(); + $row2 = new Row(); $row2->setTableId($tableId); $row2->setData($data); try { @@ -296,7 +296,7 @@ private function getColumnFromColumnsArray(int $columnId, array $columns): ?Colu * @throws NotFoundError * @throws InternalError */ - private function getRowById(int $rowId): Row2 { + private function getRowById(int $rowId): Row { if (isset($this->tmpRows[$rowId])) { return $this->tmpRows[$rowId]; } @@ -327,7 +327,7 @@ private function getRowById(int $rowId): Row2 { * @param int|null $viewId * @param list $data * @param string $userId - * @return Row2 + * @return Row * * @throws InternalError * @throws NotFoundError @@ -338,7 +338,7 @@ public function updateSet( ?int $viewId, array $data, string $userId - ): Row2 { + ): Row { try { $item = $this->getRowById($id); } catch (InternalError $e) { @@ -418,14 +418,14 @@ public function updateSet( * @param int $id * @param int|null $viewId * @param string $userId - * @return Row2 + * @return Row * * @throws InternalError * @throws NotFoundError * @throws PermissionError * @noinspection DuplicatedCode */ - public function delete(int $id, ?int $viewId, string $userId): Row2 { + public function delete(int $id, ?int $viewId, string $userId): Row { try { $item = $this->getRowById($id); } catch (InternalError $e) { From 99548ef1834aebf44d19ae79153899da0ce14943 Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Wed, 3 Jan 2024 15:22:32 +0100 Subject: [PATCH 29/73] Code cleanup Signed-off-by: Florian Steffens --- lib/Service/RowService.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/Service/RowService.php b/lib/Service/RowService.php index cbc34f594..c73a41170 100644 --- a/lib/Service/RowService.php +++ b/lib/Service/RowService.php @@ -4,10 +4,9 @@ use OCA\Tables\Db\Column; use OCA\Tables\Db\ColumnMapper; -use OCA\Tables\Db\LegacyRow; +use OCA\Tables\Db\LegacyRowMapper; use OCA\Tables\Db\Row; use OCA\Tables\Db\RowMapper; -use OCA\Tables\Db\LegacyRowMapper; use OCA\Tables\Db\Table; use OCA\Tables\Db\TableMapper; use OCA\Tables\Db\View; @@ -37,7 +36,7 @@ class RowService extends SuperService { private array $tmpRows = []; // holds already loaded rows as a small cache public function __construct(PermissionsService $permissionsService, LoggerInterface $logger, ?string $userId, - LegacyRowMapper $mapper, ColumnMapper $columnMapper, ViewMapper $viewMapper, TableMapper $tableMapper, RowMapper $row2Mapper) { + LegacyRowMapper $mapper, ColumnMapper $columnMapper, ViewMapper $viewMapper, TableMapper $tableMapper, RowMapper $row2Mapper) { parent::__construct($logger, $userId, $permissionsService); $this->mapper = $mapper; $this->columnMapper = $columnMapper; From ba7a9db8ddd66b5539d347646950b905da6ff557 Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Thu, 4 Jan 2024 08:11:34 +0100 Subject: [PATCH 30/73] Repair step to migrate rows data into the new db structure Signed-off-by: Florian Steffens --- appinfo/info.xml | 7 +- lib/Db/LegacyRowMapper.php | 2 +- lib/Db/RowMapper.php | 39 ++++++-- lib/Migration/NewDbStructureRepairStep.php | 100 +++++++++++++++++++++ 4 files changed, 137 insertions(+), 11 deletions(-) create mode 100644 lib/Migration/NewDbStructureRepairStep.php diff --git a/appinfo/info.xml b/appinfo/info.xml index abd009d7a..74c7d3a54 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -22,7 +22,7 @@ Share your tables and views with users and groups within your cloud. Have a good time and manage whatever you want. ]]> - 0.6.5 + 0.7.0 agpl Florian Steffens Tables @@ -46,6 +46,11 @@ Have a good time and manage whatever you want. sqlite + + + OCA\Tables\Migration\NewDbStructureRepairStep + + OCA\Tables\Command\ListTables OCA\Tables\Command\AddTable diff --git a/lib/Db/LegacyRowMapper.php b/lib/Db/LegacyRowMapper.php index 0df2da5d2..311b91afb 100644 --- a/lib/Db/LegacyRowMapper.php +++ b/lib/Db/LegacyRowMapper.php @@ -251,7 +251,7 @@ private function addFilterToQuery(IQueryBuilder $qb, View $view, array $neededCo * @param int $tableId * @param int|null $limit * @param int|null $offset - * @return array + * @return LegacyRow[] * @throws Exception */ public function findAllByTable(int $tableId, ?int $limit = null, ?int $offset = null): array { diff --git a/lib/Db/RowMapper.php b/lib/Db/RowMapper.php index 2905c351b..f1ade800b 100644 --- a/lib/Db/RowMapper.php +++ b/lib/Db/RowMapper.php @@ -388,13 +388,18 @@ public function insert(Row $row, array $columns): Row { } $this->setColumns($columns); - // create a new row sleeve to get a new rowId - $rowSleeve = $this->createNewRowSleeve($row->getTableId()); - $row->setId($rowSleeve->getId()); + if($row->getId()) { + // if row has an id from migration or import etc. + $rowSleeve = $this->createRowSleeveFromExistingData($row->getId(), $row->getTableId(), $row->getCreatedAt(), $row->getCreatedBy(), $row->getLastEditBy(), $row->getLastEditAt()); + } else { + // create a new row sleeve to get a new rowId + $rowSleeve = $this->createNewRowSleeve($row->getTableId()); + $row->setId($rowSleeve->getId()); + } // write all cells to its db-table foreach ($row->getData() as $cell) { - $this->insertCell($rowSleeve->getId(), $cell['columnId'], $cell['value']); + $this->insertCell($rowSleeve->getId(), $cell['columnId'], $cell['value'], $rowSleeve->getLastEditAt(), $rowSleeve->getLastEditBy()); } return $row; @@ -442,22 +447,38 @@ private function createNewRowSleeve(int $tableId): RowSleeve { return $this->rowSleeveMapper->insert($rowSleeve); } + /** + * @throws Exception + */ + private function createRowSleeveFromExistingData(int $id, int $tableId, string $createdAt, string $createdBy, string $lastEditBy, string $lastEditAt): RowSleeve { + $rowSleeve = new RowSleeve(); + $rowSleeve->setId($id); + $rowSleeve->setTableId($tableId); + $rowSleeve->setCreatedBy($createdBy); + $rowSleeve->setCreatedAt($createdAt); + $rowSleeve->setLastEditBy($lastEditBy); + $rowSleeve->setLastEditAt($lastEditAt); + return $this->rowSleeveMapper->insert($rowSleeve); + } + /** * Updates the last_edit_by and last_edit_at data * optional adds the created_by and created_at data * * @param RowSleeve|RowCellSuper $entity * @param bool $setCreate + * @param string|null $lastEditAt + * @param string|null $lastEditBy * @return void */ - private function updateMetaData($entity, bool $setCreate = false): void { + private function updateMetaData($entity, bool $setCreate = false, ?string $lastEditAt = null, ?string $lastEditBy = null): void { $time = new DateTime(); if ($setCreate) { $entity->setCreatedBy($this->userId); $entity->setCreatedAt($time->format('Y-m-d H:i:s')); } - $entity->setLastEditBy($this->userId); - $entity->setLastEditAt($time->format('Y-m-d H:i:s')); + $entity->setLastEditBy($lastEditBy ?: $this->userId); + $entity->setLastEditAt($lastEditAt ?: $time->format('Y-m-d H:i:s')); } /** @@ -465,14 +486,14 @@ private function updateMetaData($entity, bool $setCreate = false): void { * * @throws InternalError */ - private function insertCell(int $rowId, int $columnId, $value): void { + private function insertCell(int $rowId, int $columnId, $value, ?string $lastEditAt = null, ?string $lastEditBy = null): void { $cellClassName = 'OCA\Tables\Db\RowCell'.ucfirst($this->columns[$columnId]->getType()); /** @var RowCellSuper $cell */ $cell = new $cellClassName(); $cell->setRowIdWrapper($rowId); $cell->setColumnIdWrapper($columnId); - $this->updateMetaData($cell); + $this->updateMetaData($cell, false, $lastEditAt, $lastEditBy); // insert new cell $cellMapperClassName = 'OCA\Tables\Db\RowCell'.ucfirst($this->columns[$columnId]->getType()).'Mapper'; diff --git a/lib/Migration/NewDbStructureRepairStep.php b/lib/Migration/NewDbStructureRepairStep.php new file mode 100644 index 000000000..d89eb3ca4 --- /dev/null +++ b/lib/Migration/NewDbStructureRepairStep.php @@ -0,0 +1,100 @@ +logger = $logger; + $this->tableService = $tableService; + $this->columnService = $columnService; + $this->legacyRowMapper = $legacyRowMapper; + $this->rowMapper = $rowMapper; + } + + /** + * Returns the step's name + */ + public function getName() { + return 'Copy the data into the new db structure'; + } + + /** + * @param IOutput $output + */ + public function run(IOutput $output) { + $output->info("Look for tables"); + try { + $tables = $this->tableService->findAll('', true, true, false); + $output->info("Found ". count($tables) . " table(s)"); + } catch (InternalError $e) { + $output->warning("Error while fetching tables. Will aboard."); + return; + } + $this->transferDataForTables($tables, $output); + } + + /** + * @param Table[] $tables + * @return void + */ + private function transferDataForTables(array $tables, IOutput $output) { + foreach ($tables as $table) { + $output->info("-- Start transfer for table " . $table->getId()) . " (" . $table->getTitle() . ")"; + try { + $this->transferTable($table, $output); + } catch (InternalError|PermissionError|Exception $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + $output->warning("Could not transfer data. Continue with next table. The logs will have more information about the error."); + } + } + } + + /** + * @throws PermissionError + * @throws InternalError + * @throws Exception + */ + private function transferTable(Table $table, IOutput $output) { + $columns = $this->columnService->findAllByTable($table->getId(), null, ''); + $output->info("---- Found " . count($columns) . " columns"); + + $legacyRows = $this->legacyRowMapper->findAllByTable($table->getId()); + $output->info("---- Found " . count($legacyRows) . " rows"); + + $output->startProgress(count($legacyRows)); + foreach ($legacyRows as $legacyRow) { + $row = new Row(); + $row->setId($legacyRow->getId()); + $row->setTableId($legacyRow->getTableId()); + $row->setCreatedBy($legacyRow->getCreatedBy()); + $row->setCreatedAt($legacyRow->getCreatedAt()); + $row->setLastEditBy($legacyRow->getLastEditBy()); + $row->setLastEditAt($legacyRow->getLastEditAt()); + $row->setData($legacyRow->getDataArray()); + $this->rowMapper->insert($row, $columns); + + $output->advance(1); + } + $output->finishProgress(); + } +} From 3fb06a6a5a14377cbb777ee5e393e8fbdd005e86 Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Thu, 4 Jan 2024 13:35:39 +0100 Subject: [PATCH 31/73] fix info output Signed-off-by: Florian Steffens --- lib/Migration/NewDbStructureRepairStep.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Migration/NewDbStructureRepairStep.php b/lib/Migration/NewDbStructureRepairStep.php index d89eb3ca4..24ba2973b 100644 --- a/lib/Migration/NewDbStructureRepairStep.php +++ b/lib/Migration/NewDbStructureRepairStep.php @@ -59,7 +59,7 @@ public function run(IOutput $output) { */ private function transferDataForTables(array $tables, IOutput $output) { foreach ($tables as $table) { - $output->info("-- Start transfer for table " . $table->getId()) . " (" . $table->getTitle() . ")"; + $output->info("-- Start transfer for table " . $table->getId() . " (" . $table->getTitle() . ")"); try { $this->transferTable($table, $output); } catch (InternalError|PermissionError|Exception $e) { From dbd4e537608a0b772b3a2b2bf86bd8b3c530ab79 Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Fri, 5 Jan 2024 11:33:07 +0100 Subject: [PATCH 32/73] add command to migrate data to new db structure Signed-off-by: Florian Steffens --- appinfo/info.xml | 1 + lib/Command/TransferLegacyRows.php | 178 +++++++++++++++++++++ lib/Db/LegacyRowMapper.php | 23 ++- lib/Migration/NewDbStructureRepairStep.php | 27 ++-- 4 files changed, 215 insertions(+), 14 deletions(-) create mode 100644 lib/Command/TransferLegacyRows.php diff --git a/appinfo/info.xml b/appinfo/info.xml index 74c7d3a54..92bbfa23d 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -58,6 +58,7 @@ Have a good time and manage whatever you want. OCA\Tables\Command\RenameTable OCA\Tables\Command\ChangeOwnershipTable OCA\Tables\Command\Clean + OCA\Tables\Command\TransferLegacyRows diff --git a/lib/Command/TransferLegacyRows.php b/lib/Command/TransferLegacyRows.php new file mode 100644 index 000000000..2781071a9 --- /dev/null +++ b/lib/Command/TransferLegacyRows.php @@ -0,0 +1,178 @@ + + * + * @author Florian Steffens + * + * @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 . + * + */ + +namespace OCA\Tables\Command; + +use OCA\Tables\Db\LegacyRowMapper; +use OCA\Tables\Db\RowMapper; +use OCA\Tables\Db\Table; +use OCA\Tables\Errors\InternalError; +use OCA\Tables\Errors\NotFoundError; +use OCA\Tables\Errors\PermissionError; +use OCA\Tables\Service\ColumnService; +use OCA\Tables\Service\TableService; +use OCP\DB\Exception; +use Psr\Log\LoggerInterface; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +class TransferLegacyRows extends Command { + protected TableService $tableService; + protected LoggerInterface $logger; + protected LegacyRowMapper $legacyRowMapper; + protected RowMapper $rowMapper; + protected ColumnService $columnService; + + public function __construct(TableService $tableService, LoggerInterface $logger, LegacyRowMapper $legacyRowMapper, RowMapper $rowMapper, ColumnService $columnService) { + parent::__construct(); + $this->tableService = $tableService; + $this->logger = $logger; + $this->legacyRowMapper = $legacyRowMapper; + $this->rowMapper = $rowMapper; + $this->columnService = $columnService; + } + + protected function configure(): void { + $this + ->setName('tables:legacy:transfer:rows') + ->setDescription('Transfer table legacy rows to new schema.') + ->addArgument( + 'table-ids', + InputArgument::OPTIONAL, + 'IDs of tables for the which data is to be transferred. (Multiple comma seperated possible)' + ) + ->addOption( + 'all', + null, + InputOption::VALUE_NONE, + 'Transfer all table data.' + ) + ->addOption( + 'no-delete', + null, + InputOption::VALUE_OPTIONAL, + 'Set to not delete data from new db structure if any.' + ) + ; + } + + /** + * @param InputInterface $input + * @param OutputInterface $output + * @return int + */ + protected function execute(InputInterface $input, OutputInterface $output): int { + $tableIds = $input->getArgument('table-ids'); + $optionAll = !!$input->getOption('all'); + $optionNoDelete = $input->getOption('no-delete') ?: null; + + if ($optionAll) { + $output->writeln("Look for tables"); + try { + $tables = $this->tableService->findAll('', true, true, false); + $output->writeln("Found ". count($tables) . " table(s)"); + } catch (InternalError $e) { + $output->writeln("Error while fetching tables. Will aboard."); + return 1; + } + } elseif ($tableIds) { + $output->writeln("Look for given table(s)"); + $tableIds = explode(',', $tableIds); + $tables = []; + foreach ($tableIds as $tableId) { + try { + $tables[] = $this->tableService->find((int)ltrim($tableId), true, ''); + } catch (InternalError|NotFoundError|PermissionError $e) { + $output->writeln("Could not load table id " . $tableId . ". Will continue."); + } + } + } else { + $output->writeln("🤷🏻‍ Add at least one table id or add the option --all to transfer all tables."); + return 1; + } + if (!$optionNoDelete) { + $this->deleteDataForTables($tables, $output); + } + $this->transferDataForTables($tables, $output); + + return 0; + } + + /** + * @param Table[] $tables + * @return void + */ + private function transferDataForTables(array $tables, OutputInterface $output) { + $i = 1; + foreach ($tables as $table) { + $output->writeln("-- Start transfer for table " . $table->getId() . " (" . $table->getTitle() . ") [" . $i . "/" . count($tables) . "]"); + try { + $this->transferTable($table, $output); + } catch (InternalError|PermissionError|Exception $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + $output->writeln("⚠️ Could not transfer data. Continue with next table. The logs will have more information about the error."); + } + $i++; + } + } + + /** + * @throws PermissionError + * @throws InternalError + * @throws Exception + */ + private function transferTable(Table $table, OutputInterface $output) { + $columns = $this->columnService->findAllByTable($table->getId(), null, ''); + $output->writeln("---- Found " . count($columns) . " columns"); + + $legacyRows = $this->legacyRowMapper->findAllByTable($table->getId()); + $output->writeln("---- Found " . count($legacyRows) . " rows"); + + foreach ($legacyRows as $legacyRow) { + $this->legacyRowMapper->transferLegacyRow($legacyRow, $columns); + } + $output->writeln("---- ✅ All rows transferred."); + } + + /** + * @param Table[] $tables + * @param OutputInterface $output + * @return void + */ + private function deleteDataForTables(array $tables, OutputInterface $output) { + $output->writeln("Start deleting data for tables that should be transferred."); + foreach ($tables as $table) { + try { + $columns = $this->columnService->findAllByTable($table->getId(), null, ''); + } catch (InternalError|PermissionError $e) { + $output->writeln("Could not delete data for table " . $table->getId()); + break; + } + $this->rowMapper->deleteAllForTable($table->getId(), $columns); + $output->writeln("🗑️ Data for table " . $table->getId() . " (" . $table->getTitle() . ")" . " removed."); + } + } +} diff --git a/lib/Db/LegacyRowMapper.php b/lib/Db/LegacyRowMapper.php index 311b91afb..1dd22b4e5 100644 --- a/lib/Db/LegacyRowMapper.php +++ b/lib/Db/LegacyRowMapper.php @@ -33,10 +33,11 @@ class LegacyRowMapper extends QBMapper { protected ColumnMapper $columnMapper; protected LoggerInterface $logger; protected UserHelper $userHelper; + protected RowMapper $rowMapper; protected int $platform; - public function __construct(IDBConnection $db, LoggerInterface $logger, TextColumnQB $textColumnQB, SelectionColumnQB $selectionColumnQB, NumberColumnQB $numberColumnQB, DatetimeColumnQB $datetimeColumnQB, SuperColumnQB $columnQB, ColumnMapper $columnMapper, UserHelper $userHelper) { + public function __construct(IDBConnection $db, LoggerInterface $logger, TextColumnQB $textColumnQB, SelectionColumnQB $selectionColumnQB, NumberColumnQB $numberColumnQB, DatetimeColumnQB $datetimeColumnQB, SuperColumnQB $columnQB, ColumnMapper $columnMapper, UserHelper $userHelper, RowMapper $rowMapper) { parent::__construct($db, $this->table, LegacyRow::class); $this->logger = $logger; $this->textColumnQB = $textColumnQB; @@ -46,6 +47,7 @@ public function __construct(IDBConnection $db, LoggerInterface $logger, TextColu $this->genericColumnQB = $columnQB; $this->columnMapper = $columnMapper; $this->userHelper = $userHelper; + $this->rowMapper = $rowMapper; $this->setPlatform(); } @@ -424,4 +426,23 @@ public function findByView(int $id, View $view): LegacyRow { return $row; } + + /** + * @param Column[] $columns + * @param LegacyRow $legacyRow + * + * @throws Exception + * @throws InternalError + */ + public function transferLegacyRow(LegacyRow $legacyRow, array $columns) { + $row = new Row(); + $row->setId($legacyRow->getId()); + $row->setTableId($legacyRow->getTableId()); + $row->setCreatedBy($legacyRow->getCreatedBy()); + $row->setCreatedAt($legacyRow->getCreatedAt()); + $row->setLastEditBy($legacyRow->getLastEditBy()); + $row->setLastEditAt($legacyRow->getLastEditAt()); + $row->setData($legacyRow->getDataArray()); + $this->rowMapper->insert($row, $columns); + } } diff --git a/lib/Migration/NewDbStructureRepairStep.php b/lib/Migration/NewDbStructureRepairStep.php index 24ba2973b..361fc51eb 100644 --- a/lib/Migration/NewDbStructureRepairStep.php +++ b/lib/Migration/NewDbStructureRepairStep.php @@ -3,7 +3,6 @@ namespace OCA\Tables\Migration; use OCA\Tables\Db\LegacyRowMapper; -use OCA\Tables\Db\Row; use OCA\Tables\Db\RowMapper; use OCA\Tables\Db\Table; use OCA\Tables\Errors\InternalError; @@ -11,6 +10,7 @@ use OCA\Tables\Service\ColumnService; use OCA\Tables\Service\TableService; use OCP\DB\Exception; +use OCP\IConfig; use OCP\Migration\IOutput; use OCP\Migration\IRepairStep; use Psr\Log\LoggerInterface; @@ -22,13 +22,15 @@ class NewDbStructureRepairStep implements IRepairStep { protected LegacyRowMapper $legacyRowMapper; protected RowMapper $rowMapper; protected ColumnService $columnService; + protected IConfig $config; - public function __construct(LoggerInterface $logger, TableService $tableService, ColumnService $columnService, LegacyRowMapper $legacyRowMapper, RowMapper $rowMapper) { + public function __construct(LoggerInterface $logger, TableService $tableService, ColumnService $columnService, LegacyRowMapper $legacyRowMapper, RowMapper $rowMapper, IConfig $config) { $this->logger = $logger; $this->tableService = $tableService; $this->columnService = $columnService; $this->legacyRowMapper = $legacyRowMapper; $this->rowMapper = $rowMapper; + $this->config = $config; } /** @@ -42,6 +44,12 @@ public function getName() { * @param IOutput $output */ public function run(IOutput $output) { + $appVersion = $this->config->getAppValue('tables', 'installed_version'); + + if (!$appVersion || version_compare($appVersion, '0.7.0', '<')) { + return; + } + $output->info("Look for tables"); try { $tables = $this->tableService->findAll('', true, true, false); @@ -58,14 +66,16 @@ public function run(IOutput $output) { * @return void */ private function transferDataForTables(array $tables, IOutput $output) { + $i = 1; foreach ($tables as $table) { - $output->info("-- Start transfer for table " . $table->getId() . " (" . $table->getTitle() . ")"); + $output->info("-- Start transfer for table " . $table->getId() . " (" . $table->getTitle() . ") [" . $i . "/" . count($tables) . "]"); try { $this->transferTable($table, $output); } catch (InternalError|PermissionError|Exception $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); $output->warning("Could not transfer data. Continue with next table. The logs will have more information about the error."); } + $i++; } } @@ -83,16 +93,7 @@ private function transferTable(Table $table, IOutput $output) { $output->startProgress(count($legacyRows)); foreach ($legacyRows as $legacyRow) { - $row = new Row(); - $row->setId($legacyRow->getId()); - $row->setTableId($legacyRow->getTableId()); - $row->setCreatedBy($legacyRow->getCreatedBy()); - $row->setCreatedAt($legacyRow->getCreatedAt()); - $row->setLastEditBy($legacyRow->getLastEditBy()); - $row->setLastEditAt($legacyRow->getLastEditAt()); - $row->setData($legacyRow->getDataArray()); - $this->rowMapper->insert($row, $columns); - + $this->legacyRowMapper->transferLegacyRow($legacyRow, $columns); $output->advance(1); } $output->finishProgress(); From 5a20018d461c25c51d09b3e3285b632815d8b751 Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Fri, 5 Jan 2024 11:38:43 +0100 Subject: [PATCH 33/73] make the column definition not dynamic to avoid runtime problems during web update paths Signed-off-by: Florian Steffens --- .../Version000700Date20230916000000.php | 37 +++++++++++++------ 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/lib/Migration/Version000700Date20230916000000.php b/lib/Migration/Version000700Date20230916000000.php index 909214e3d..beba28a48 100644 --- a/lib/Migration/Version000700Date20230916000000.php +++ b/lib/Migration/Version000700Date20230916000000.php @@ -7,18 +7,39 @@ namespace OCA\Tables\Migration; use Closure; -use OCA\Tables\Helper\ColumnsHelper; use OCP\DB\Exception; use OCP\DB\ISchemaWrapper; use OCP\DB\Types; use OCP\Migration\IOutput; use OCP\Migration\SimpleMigrationStep; -use OCP\Server; -use Psr\Container\ContainerExceptionInterface; -use Psr\Container\NotFoundExceptionInterface; class Version000700Date20230916000000 extends SimpleMigrationStep { + /** + * this is a copy from the definition set in OCA\Tables\Helper\ColumnsHelper + * this has to be in sync! but the definition can not be used directly + * because it might cause problems on auto web updates + * (class might not be loaded if it gets replaced during the runtime) + */ + private array $columns = [ + [ + 'name' => 'text', + 'db_type' => Types::TEXT, + ], + [ + 'name' => 'number', + 'db_type' => Types::FLOAT, + ], + [ + 'name' => 'datetime', + 'db_type' => Types::TEXT, + ], + [ + 'name' => 'selection', + 'db_type' => Types::TEXT, + ], + ]; + /** * @param IOutput $output * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` @@ -32,13 +53,7 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt $this->createRowSleevesTable($schema); - try { - $columnsHelper = Server::get(ColumnsHelper::class); - } catch (NotFoundExceptionInterface|ContainerExceptionInterface $e) { - throw new Exception('Could not fetch columns helper which is needed to setup all the tables.'); - } - - $rowTypeSchema = $columnsHelper->get(['name', 'db_type']); + $rowTypeSchema = $this->columns; foreach ($rowTypeSchema as $colType) { $this->createRowValueTable($schema, $colType['name'], $colType['db_type']); From 05b1a6a8e0d8e32ff4733f2659feb507f8fd1353 Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Fri, 5 Jan 2024 13:21:46 +0100 Subject: [PATCH 34/73] Set legacy transfer to app config to run it only once Signed-off-by: Florian Steffens --- lib/Migration/NewDbStructureRepairStep.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/Migration/NewDbStructureRepairStep.php b/lib/Migration/NewDbStructureRepairStep.php index 361fc51eb..605c89628 100644 --- a/lib/Migration/NewDbStructureRepairStep.php +++ b/lib/Migration/NewDbStructureRepairStep.php @@ -44,9 +44,9 @@ public function getName() { * @param IOutput $output */ public function run(IOutput $output) { - $appVersion = $this->config->getAppValue('tables', 'installed_version'); + $legacyRowTransferRunComplete = $this->config->getAppValue('tables', 'legacyRowTransferRunComplete', "false"); - if (!$appVersion || version_compare($appVersion, '0.7.0', '<')) { + if ($legacyRowTransferRunComplete === "true") { return; } @@ -59,6 +59,7 @@ public function run(IOutput $output) { return; } $this->transferDataForTables($tables, $output); + $this->config->setAppValue('tables', 'legacyRowTransferRunComplete', "true"); } /** From 3ef403cb721de72fb31ff105947a9731eec6d705 Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Thu, 11 Jan 2024 10:56:44 +0100 Subject: [PATCH 35/73] split columns mapper find method into find() and findAll() Signed-off-by: Florian Steffens --- lib/Db/ColumnMapper.php | 30 +++++++++++++++++++----------- lib/Service/RowService.php | 2 +- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/lib/Db/ColumnMapper.php b/lib/Db/ColumnMapper.php index 0f471a2f6..ea76c0d95 100644 --- a/lib/Db/ColumnMapper.php +++ b/lib/Db/ColumnMapper.php @@ -21,25 +21,33 @@ public function __construct(IDBConnection $db, LoggerInterface $logger) { } /** - * @param int|array $id + * @param int $id Column ID * - * @return Column|Column[] + * @return Column * @throws DoesNotExistException * @throws Exception * @throws MultipleObjectsReturnedException */ - public function find($id) { + public function find(int $id): Column { $qb = $this->db->getQueryBuilder(); $qb->select('*') - ->from($this->table); + ->from($this->table) + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))); + return $this->findEntity($qb); + } - if(is_array($id)) { - $qb->where($qb->expr()->in('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT_ARRAY))); - return $this->findEntities($qb); - } else { - $qb->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))); - return $this->findEntity($qb); - } + /** + * @param array $id Column IDs + * + * @return Column[] + * @throws Exception + */ + public function findAll(array $id): array { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->table) + ->where($qb->expr()->in('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT_ARRAY))); + return $this->findEntities($qb); } /** diff --git a/lib/Service/RowService.php b/lib/Service/RowService.php index c73a41170..8ea9edcd2 100644 --- a/lib/Service/RowService.php +++ b/lib/Service/RowService.php @@ -93,7 +93,7 @@ public function findAllByView(int $viewId, string $userId, ?int $limit = null, ? if ($this->permissionsService->canReadRowsByElementId($viewId, 'view', $userId)) { $view = $this->viewMapper->find($viewId); $columnsArray = $view->getColumnsArray(); - $columns = $this->columnMapper->find($columnsArray); + $columns = $this->columnMapper->findAll($columnsArray); return $this->row2Mapper->findAll($columns, $view->getTableId(), $limit, $offset, $view->getFilterArray(), $view->getSortArray(), $userId); // return $this->mapper->findAllByView($this->viewMapper->find($viewId), $userId, $limit, $offset); From 02d0b7ab4317995f18183b1907e5ac362b2ef1f8 Mon Sep 17 00:00:00 2001 From: Florian Date: Thu, 11 Jan 2024 14:47:31 +0100 Subject: [PATCH 36/73] Update lib/Db/RowCellNumberMapper.php Co-authored-by: Arthur Schiwon Signed-off-by: Florian --- lib/Db/RowCellNumberMapper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Db/RowCellNumberMapper.php b/lib/Db/RowCellNumberMapper.php index 94b9d9f2e..3a8867722 100644 --- a/lib/Db/RowCellNumberMapper.php +++ b/lib/Db/RowCellNumberMapper.php @@ -32,7 +32,7 @@ public function parseValueOutgoing(Column $column, $value) { * @inheritDoc */ public function parseValueIncoming(Column $column, $value): ?float { - if($value === '') { + if(!is_numeric($value)) { return null; } return (float) $value; From c8862c74ae1bb4bcdac9d6bf18b5e3dab1b8b141 Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Thu, 11 Jan 2024 14:57:12 +0100 Subject: [PATCH 37/73] wrap row deletion in a transaction Signed-off-by: Florian Steffens --- lib/Db/RowMapper.php | 43 ++++++++++++++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/lib/Db/RowMapper.php b/lib/Db/RowMapper.php index f1ade800b..0943bcda7 100644 --- a/lib/Db/RowMapper.php +++ b/lib/Db/RowMapper.php @@ -18,6 +18,7 @@ use Psr\Container\ContainerExceptionInterface; use Psr\Container\NotFoundExceptionInterface; use Psr\Log\LoggerInterface; +use Throwable; class RowMapper { private RowSleeveMapper $rowSleeveMapper; @@ -41,28 +42,48 @@ public function __construct(?string $userId, IDBConnection $db, LoggerInterface } /** + * @param Row $row + * @return Row * @throws InternalError */ public function delete(Row $row): Row { - foreach ($this->columnsHelper->get(['name']) as $columnType) { - $cellMapperClassName = 'OCA\Tables\Db\RowCell' . ucfirst($columnType) . 'Mapper'; - /** @var RowCellMapperSuper $cellMapper */ + try { + $this->db->beginTransaction(); + } catch (Exception $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + } + try { + foreach ($this->columnsHelper->get(['name']) as $columnType) { + $cellMapperClassName = 'OCA\Tables\Db\RowCell' . ucfirst($columnType) . 'Mapper'; + /** @var RowCellMapperSuper $cellMapper */ + try { + $cellMapper = Server::get($cellMapperClassName); + } catch (NotFoundExceptionInterface|ContainerExceptionInterface $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage()); + } + try { + $cellMapper->deleteAllForRow($row->getId()); + } catch (Exception $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + } + } try { - $cellMapper = Server::get($cellMapperClassName); - } catch (NotFoundExceptionInterface|ContainerExceptionInterface $e) { + $this->rowSleeveMapper->deleteById($row->getId()); + } catch (Exception $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage()); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); } + $this->db->commit(); + } catch (Throwable $e) { try { - $cellMapper->deleteAllForRow($row->getId()); + $this->db->rollBack(); } catch (Exception $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); } - } - try { - $this->rowSleeveMapper->deleteById($row->getId()); - } catch (Exception $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); } From 98f013a786634db51f7dd283a83bdc17c3204801 Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Thu, 11 Jan 2024 15:03:03 +0100 Subject: [PATCH 38/73] code cleanup Signed-off-by: Florian Steffens --- lib/Db/RowMapper.php | 34 +++++++--------------------------- lib/Service/RowService.php | 7 ++++++- 2 files changed, 13 insertions(+), 28 deletions(-) diff --git a/lib/Db/RowMapper.php b/lib/Db/RowMapper.php index 0943bcda7..7604928b9 100644 --- a/lib/Db/RowMapper.php +++ b/lib/Db/RowMapper.php @@ -44,15 +44,10 @@ public function __construct(?string $userId, IDBConnection $db, LoggerInterface /** * @param Row $row * @return Row - * @throws InternalError + * @throws Exception */ public function delete(Row $row): Row { - try { - $this->db->beginTransaction(); - } catch (Exception $e) { - $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); - } + $this->db->beginTransaction(); try { foreach ($this->columnsHelper->get(['name']) as $columnType) { $cellMapperClassName = 'OCA\Tables\Db\RowCell' . ucfirst($columnType) . 'Mapper'; @@ -61,31 +56,16 @@ public function delete(Row $row): Row { $cellMapper = Server::get($cellMapperClassName); } catch (NotFoundExceptionInterface|ContainerExceptionInterface $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage()); + throw new Exception(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage()); } - try { - $cellMapper->deleteAllForRow($row->getId()); - } catch (Exception $e) { - $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); - } - } - try { - $this->rowSleeveMapper->deleteById($row->getId()); - } catch (Exception $e) { - $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + $cellMapper->deleteAllForRow($row->getId()); } + $this->rowSleeveMapper->deleteById($row->getId()); $this->db->commit(); } catch (Throwable $e) { - try { - $this->db->rollBack(); - } catch (Exception $e) { - $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); - } + $this->db->rollBack(); $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + throw new Exception(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); } return $row; diff --git a/lib/Service/RowService.php b/lib/Service/RowService.php index 8ea9edcd2..0812ddf6f 100644 --- a/lib/Service/RowService.php +++ b/lib/Service/RowService.php @@ -463,7 +463,12 @@ public function delete(int $id, ?int $viewId, string $userId): Row { } } - return $this->row2Mapper->delete($item); + try { + return $this->row2Mapper->delete($item); + } catch (Exception $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + } } /** From 14da9ffd50efd3bf840439b007cf95d012a6528d Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Thu, 11 Jan 2024 15:04:21 +0100 Subject: [PATCH 39/73] update names to avoid misunderstandings with the word "magic" Signed-off-by: Florian Steffens --- lib/Db/RowMapper.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/Db/RowMapper.php b/lib/Db/RowMapper.php index 7604928b9..c96e30f4d 100644 --- a/lib/Db/RowMapper.php +++ b/lib/Db/RowMapper.php @@ -213,7 +213,7 @@ private function getRows(array $rowIds, array $columnIds): array { */ private function addFilterToQuery(IQueryBuilder &$qb, array $filters, string $userId): void { // TODO move this into service - $this->replaceMagicValues($filters, $userId); + $this->replacePlaceholderValues($filters, $userId); if (count($filters) > 0) { $qb->andWhere( @@ -224,7 +224,7 @@ private function addFilterToQuery(IQueryBuilder &$qb, array $filters, string $us } } - private function replaceMagicValues(array &$filters, string $userId): void { + private function replacePlaceholderValues(array &$filters, string $userId): void { foreach ($filters as &$filterGroup) { foreach ($filterGroup as &$filter) { if(substr($filter['value'], 0, 1) === '@') { @@ -307,8 +307,8 @@ private function getFilterExpression(IQueryBuilder $qb, Column $column, string $ } /** @noinspection DuplicatedCode */ - private function resolveSearchValue(string $magicValue, string $userId): string { - switch (ltrim($magicValue, '@')) { + private function resolveSearchValue(string $placeholder, string $userId): string { + switch (ltrim($placeholder, '@')) { case 'me': return $userId; case 'my-name': return $this->userHelper->getUserDisplayName($userId); case 'checked': return 'true'; @@ -328,7 +328,7 @@ private function resolveSearchValue(string $magicValue, string $userId): string return $result ?: ''; case 'datetime-time-now': return date('H:i'); case 'datetime-now': return date('Y-m-d H:i') ? date('Y-m-d H:i') : ''; - default: return $magicValue; + default: return $placeholder; } } From 980664146ccd431ff6952fc0183827809279f7ae Mon Sep 17 00:00:00 2001 From: Florian Date: Thu, 11 Jan 2024 15:07:15 +0100 Subject: [PATCH 40/73] Update lib/Db/RowMapper.php Co-authored-by: Arthur Schiwon Signed-off-by: Florian --- lib/Db/RowMapper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Db/RowMapper.php b/lib/Db/RowMapper.php index c96e30f4d..fb91de51a 100644 --- a/lib/Db/RowMapper.php +++ b/lib/Db/RowMapper.php @@ -472,7 +472,7 @@ private function createRowSleeveFromExistingData(int $id, int $tableId, string $ * @param string|null $lastEditBy * @return void */ - private function updateMetaData($entity, bool $setCreate = false, ?string $lastEditAt = null, ?string $lastEditBy = null): void { + private function updateMetaData(Entity $entity, bool $setCreate = false, ?string $lastEditAt = null, ?string $lastEditBy = null): void { $time = new DateTime(); if ($setCreate) { $entity->setCreatedBy($this->userId); From 8cb2b38c223e0a32885b8e0f5ec9f04adb8a8705 Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Thu, 11 Jan 2024 15:08:27 +0100 Subject: [PATCH 41/73] Revert "Update lib/Db/RowMapper.php" This reverts commit f6676591db58f21261c295384688594e0df090cf. --- lib/Db/RowMapper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Db/RowMapper.php b/lib/Db/RowMapper.php index fb91de51a..c96e30f4d 100644 --- a/lib/Db/RowMapper.php +++ b/lib/Db/RowMapper.php @@ -472,7 +472,7 @@ private function createRowSleeveFromExistingData(int $id, int $tableId, string $ * @param string|null $lastEditBy * @return void */ - private function updateMetaData(Entity $entity, bool $setCreate = false, ?string $lastEditAt = null, ?string $lastEditBy = null): void { + private function updateMetaData($entity, bool $setCreate = false, ?string $lastEditAt = null, ?string $lastEditBy = null): void { $time = new DateTime(); if ($setCreate) { $entity->setCreatedBy($this->userId); From d41e7173a5cef952ed83b30f38699ee841364c4d Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Thu, 11 Jan 2024 15:20:40 +0100 Subject: [PATCH 42/73] rename row to row2 to avoid overlapping namespaces specially within the update process Signed-off-by: Florian Steffens --- lib/Command/TransferLegacyRows.php | 6 ++-- lib/Db/LegacyRowMapper.php | 6 ++-- lib/Db/{Row.php => Row2.php} | 2 +- lib/Db/{RowMapper.php => Row2Mapper.php} | 28 ++++++++--------- lib/Migration/NewDbStructureRepairStep.php | 6 ++-- lib/Service/RowService.php | 36 +++++++++++----------- 6 files changed, 42 insertions(+), 42 deletions(-) rename lib/Db/{Row.php => Row2.php} (98%) rename lib/Db/{RowMapper.php => Row2Mapper.php} (98%) diff --git a/lib/Command/TransferLegacyRows.php b/lib/Command/TransferLegacyRows.php index 2781071a9..35fa7f4d7 100644 --- a/lib/Command/TransferLegacyRows.php +++ b/lib/Command/TransferLegacyRows.php @@ -24,7 +24,7 @@ namespace OCA\Tables\Command; use OCA\Tables\Db\LegacyRowMapper; -use OCA\Tables\Db\RowMapper; +use OCA\Tables\Db\Row2Mapper; use OCA\Tables\Db\Table; use OCA\Tables\Errors\InternalError; use OCA\Tables\Errors\NotFoundError; @@ -43,10 +43,10 @@ class TransferLegacyRows extends Command { protected TableService $tableService; protected LoggerInterface $logger; protected LegacyRowMapper $legacyRowMapper; - protected RowMapper $rowMapper; + protected Row2Mapper $rowMapper; protected ColumnService $columnService; - public function __construct(TableService $tableService, LoggerInterface $logger, LegacyRowMapper $legacyRowMapper, RowMapper $rowMapper, ColumnService $columnService) { + public function __construct(TableService $tableService, LoggerInterface $logger, LegacyRowMapper $legacyRowMapper, Row2Mapper $rowMapper, ColumnService $columnService) { parent::__construct(); $this->tableService = $tableService; $this->logger = $logger; diff --git a/lib/Db/LegacyRowMapper.php b/lib/Db/LegacyRowMapper.php index 1dd22b4e5..0e5ac383d 100644 --- a/lib/Db/LegacyRowMapper.php +++ b/lib/Db/LegacyRowMapper.php @@ -33,11 +33,11 @@ class LegacyRowMapper extends QBMapper { protected ColumnMapper $columnMapper; protected LoggerInterface $logger; protected UserHelper $userHelper; - protected RowMapper $rowMapper; + protected Row2Mapper $rowMapper; protected int $platform; - public function __construct(IDBConnection $db, LoggerInterface $logger, TextColumnQB $textColumnQB, SelectionColumnQB $selectionColumnQB, NumberColumnQB $numberColumnQB, DatetimeColumnQB $datetimeColumnQB, SuperColumnQB $columnQB, ColumnMapper $columnMapper, UserHelper $userHelper, RowMapper $rowMapper) { + public function __construct(IDBConnection $db, LoggerInterface $logger, TextColumnQB $textColumnQB, SelectionColumnQB $selectionColumnQB, NumberColumnQB $numberColumnQB, DatetimeColumnQB $datetimeColumnQB, SuperColumnQB $columnQB, ColumnMapper $columnMapper, UserHelper $userHelper, Row2Mapper $rowMapper) { parent::__construct($db, $this->table, LegacyRow::class); $this->logger = $logger; $this->textColumnQB = $textColumnQB; @@ -435,7 +435,7 @@ public function findByView(int $id, View $view): LegacyRow { * @throws InternalError */ public function transferLegacyRow(LegacyRow $legacyRow, array $columns) { - $row = new Row(); + $row = new Row2(); $row->setId($legacyRow->getId()); $row->setTableId($legacyRow->getTableId()); $row->setCreatedBy($legacyRow->getCreatedBy()); diff --git a/lib/Db/Row.php b/lib/Db/Row2.php similarity index 98% rename from lib/Db/Row.php rename to lib/Db/Row2.php index 03ec606bc..e95f08046 100644 --- a/lib/Db/Row.php +++ b/lib/Db/Row2.php @@ -8,7 +8,7 @@ /** * @psalm-import-type TablesRow from ResponseDefinitions */ -class Row implements JsonSerializable { +class Row2 implements JsonSerializable { private ?int $id = null; private ?int $tableId = null; private ?string $createdBy = null; diff --git a/lib/Db/RowMapper.php b/lib/Db/Row2Mapper.php similarity index 98% rename from lib/Db/RowMapper.php rename to lib/Db/Row2Mapper.php index c96e30f4d..004147339 100644 --- a/lib/Db/RowMapper.php +++ b/lib/Db/Row2Mapper.php @@ -20,7 +20,7 @@ use Psr\Log\LoggerInterface; use Throwable; -class RowMapper { +class Row2Mapper { private RowSleeveMapper $rowSleeveMapper; private ?string $userId = null; private IDBConnection $db; @@ -42,11 +42,11 @@ public function __construct(?string $userId, IDBConnection $db, LoggerInterface } /** - * @param Row $row - * @return Row + * @param Row2 $row + * @return Row2 * @throws Exception */ - public function delete(Row $row): Row { + public function delete(Row2 $row): Row2 { $this->db->beginTransaction(); try { foreach ($this->columnsHelper->get(['name']) as $columnType) { @@ -74,11 +74,11 @@ public function delete(Row $row): Row { /** * @param int $id * @param Column[] $columns - * @return Row + * @return Row2 * @throws InternalError * @throws NotFoundError */ - public function find(int $id, array $columns): Row { + public function find(int $id, array $columns): Row2 { $this->setColumns($columns); $columnIdsArray = array_map(fn (Column $column) => $column->getId(), $columns); $rows = $this->getRows([$id], $columnIdsArray); @@ -148,7 +148,7 @@ private function getWantedRowIds(string $userId, int $tableId, ?array $filter = * @param array|null $filter * @param array|null $sort * @param string|null $userId - * @return Row[] + * @return Row2[] * @throws InternalError */ public function findAll(array $columns, int $tableId, int $limit = null, int $offset = null, array $filter = null, array $sort = null, string $userId = null): array { @@ -165,7 +165,7 @@ public function findAll(array $columns, int $tableId, int $limit = null, int $of /** * @param array $rowIds * @param array $columnIds - * @return Row[] + * @return Row2[] * @throws InternalError */ private function getRows(array $rowIds, array $columnIds): array { @@ -335,7 +335,7 @@ private function resolveSearchValue(string $placeholder, string $userId): string /** * @param IResult $result * @param RowSleeve[] $sleeves - * @return Row[] + * @return Row2[] * @throws InternalError */ private function parseEntities(IResult $result, array $sleeves): array { @@ -343,7 +343,7 @@ private function parseEntities(IResult $result, array $sleeves): array { $rows = []; foreach ($sleeves as $sleeve) { - $rows[$sleeve->getId()] = new Row(); + $rows[$sleeve->getId()] = new Row2(); $rows[$sleeve->getId()]->setId($sleeve->getId()); $rows[$sleeve->getId()]->setCreatedBy($sleeve->getCreatedBy()); $rows[$sleeve->getId()]->setCreatedAt($sleeve->getCreatedAt()); @@ -377,13 +377,13 @@ public function isRowInViewPresent(int $rowId, View $view, string $userId): bool } /** - * @param Row $row + * @param Row2 $row * @param Column[] $columns - * @return Row + * @return Row2 * @throws InternalError * @throws Exception */ - public function insert(Row $row, array $columns): Row { + public function insert(Row2 $row, array $columns): Row2 { if(!$columns) { throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': columns are missing'); } @@ -409,7 +409,7 @@ public function insert(Row $row, array $columns): Row { /** * @throws InternalError */ - public function update(Row $row, array $columns): Row { + public function update(Row2 $row, array $columns): Row2 { if(!$columns) { throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': columns are missing'); } diff --git a/lib/Migration/NewDbStructureRepairStep.php b/lib/Migration/NewDbStructureRepairStep.php index 605c89628..a7543dcb6 100644 --- a/lib/Migration/NewDbStructureRepairStep.php +++ b/lib/Migration/NewDbStructureRepairStep.php @@ -3,7 +3,7 @@ namespace OCA\Tables\Migration; use OCA\Tables\Db\LegacyRowMapper; -use OCA\Tables\Db\RowMapper; +use OCA\Tables\Db\Row2Mapper; use OCA\Tables\Db\Table; use OCA\Tables\Errors\InternalError; use OCA\Tables\Errors\PermissionError; @@ -20,11 +20,11 @@ class NewDbStructureRepairStep implements IRepairStep { protected LoggerInterface $logger; protected TableService $tableService; protected LegacyRowMapper $legacyRowMapper; - protected RowMapper $rowMapper; + protected Row2Mapper $rowMapper; protected ColumnService $columnService; protected IConfig $config; - public function __construct(LoggerInterface $logger, TableService $tableService, ColumnService $columnService, LegacyRowMapper $legacyRowMapper, RowMapper $rowMapper, IConfig $config) { + public function __construct(LoggerInterface $logger, TableService $tableService, ColumnService $columnService, LegacyRowMapper $legacyRowMapper, Row2Mapper $rowMapper, IConfig $config) { $this->logger = $logger; $this->tableService = $tableService; $this->columnService = $columnService; diff --git a/lib/Service/RowService.php b/lib/Service/RowService.php index 0812ddf6f..fc8ca4aa6 100644 --- a/lib/Service/RowService.php +++ b/lib/Service/RowService.php @@ -5,8 +5,8 @@ use OCA\Tables\Db\Column; use OCA\Tables\Db\ColumnMapper; use OCA\Tables\Db\LegacyRowMapper; -use OCA\Tables\Db\Row; -use OCA\Tables\Db\RowMapper; +use OCA\Tables\Db\Row2; +use OCA\Tables\Db\Row2Mapper; use OCA\Tables\Db\Table; use OCA\Tables\Db\TableMapper; use OCA\Tables\Db\View; @@ -32,11 +32,11 @@ class RowService extends SuperService { private ColumnMapper $columnMapper; private ViewMapper $viewMapper; private TableMapper $tableMapper; - private RowMapper $row2Mapper; + private Row2Mapper $row2Mapper; private array $tmpRows = []; // holds already loaded rows as a small cache public function __construct(PermissionsService $permissionsService, LoggerInterface $logger, ?string $userId, - LegacyRowMapper $mapper, ColumnMapper $columnMapper, ViewMapper $viewMapper, TableMapper $tableMapper, RowMapper $row2Mapper) { + LegacyRowMapper $mapper, ColumnMapper $columnMapper, ViewMapper $viewMapper, TableMapper $tableMapper, Row2Mapper $row2Mapper) { parent::__construct($logger, $userId, $permissionsService); $this->mapper = $mapper; $this->columnMapper = $columnMapper; @@ -46,11 +46,11 @@ public function __construct(PermissionsService $permissionsService, LoggerInterf } /** - * @param Row[] $rows + * @param Row2[] $rows * @psalm-return TablesRow[] */ public function formatRows(array $rows): array { - return array_map(fn (Row $row) => $row->jsonSerialize(), $rows); + return array_map(fn (Row2 $row) => $row->jsonSerialize(), $rows); } /** @@ -58,7 +58,7 @@ public function formatRows(array $rows): array { * @param string $userId * @param ?int $limit * @param ?int $offset - * @return Row[] + * @return Row2[] * @throws InternalError * @throws PermissionError */ @@ -82,7 +82,7 @@ public function findAllByTable(int $tableId, string $userId, ?int $limit = null, * @param string $userId * @param int|null $limit * @param int|null $offset - * @return Row[] + * @return Row2[] * @throws DoesNotExistException * @throws InternalError * @throws MultipleObjectsReturnedException @@ -109,12 +109,12 @@ public function findAllByView(int $viewId, string $userId, ?int $limit = null, ? /** * @param int $id - * @return Row + * @return Row2 * @throws InternalError * @throws NotFoundError * @throws PermissionError */ - public function find(int $id): Row { + public function find(int $id): Row2 { try { $columns = $this->columnMapper->findAllByTable($id); } catch (Exception $e) { @@ -144,14 +144,14 @@ public function find(int $id): Row { * @param int|null $tableId * @param int|null $viewId * @param list $data - * @return Row + * @return Row2 * * @throws NotFoundError * @throws PermissionError * @throws Exception * @throws InternalError */ - public function create(?int $tableId, ?int $viewId, array $data): Row { + public function create(?int $tableId, ?int $viewId, array $data): Row2 { if ($this->userId === null || $this->userId === '') { $e = new \Exception('No user id in context, but needed.'); $this->logger->error($e->getMessage(), ['exception' => $e]); @@ -199,7 +199,7 @@ public function create(?int $tableId, ?int $viewId, array $data): Row { $data = $this->cleanupData($data, $columns, $tableId, $viewId); // perf - $row2 = new Row(); + $row2 = new Row2(); $row2->setTableId($tableId); $row2->setData($data); try { @@ -295,7 +295,7 @@ private function getColumnFromColumnsArray(int $columnId, array $columns): ?Colu * @throws NotFoundError * @throws InternalError */ - private function getRowById(int $rowId): Row { + private function getRowById(int $rowId): Row2 { if (isset($this->tmpRows[$rowId])) { return $this->tmpRows[$rowId]; } @@ -326,7 +326,7 @@ private function getRowById(int $rowId): Row { * @param int|null $viewId * @param list $data * @param string $userId - * @return Row + * @return Row2 * * @throws InternalError * @throws NotFoundError @@ -337,7 +337,7 @@ public function updateSet( ?int $viewId, array $data, string $userId - ): Row { + ): Row2 { try { $item = $this->getRowById($id); } catch (InternalError $e) { @@ -417,14 +417,14 @@ public function updateSet( * @param int $id * @param int|null $viewId * @param string $userId - * @return Row + * @return Row2 * * @throws InternalError * @throws NotFoundError * @throws PermissionError * @noinspection DuplicatedCode */ - public function delete(int $id, ?int $viewId, string $userId): Row { + public function delete(int $id, ?int $viewId, string $userId): Row2 { try { $item = $this->getRowById($id); } catch (InternalError $e) { From 4f756947ab1cab4281e7903eacd8fe5883d3761c Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Thu, 11 Jan 2024 15:25:28 +0100 Subject: [PATCH 43/73] code cleanup Signed-off-by: Florian Steffens --- lib/Service/RowService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Service/RowService.php b/lib/Service/RowService.php index fc8ca4aa6..ef792b623 100644 --- a/lib/Service/RowService.php +++ b/lib/Service/RowService.php @@ -36,7 +36,7 @@ class RowService extends SuperService { private array $tmpRows = []; // holds already loaded rows as a small cache public function __construct(PermissionsService $permissionsService, LoggerInterface $logger, ?string $userId, - LegacyRowMapper $mapper, ColumnMapper $columnMapper, ViewMapper $viewMapper, TableMapper $tableMapper, Row2Mapper $row2Mapper) { + LegacyRowMapper $mapper, ColumnMapper $columnMapper, ViewMapper $viewMapper, TableMapper $tableMapper, Row2Mapper $row2Mapper) { parent::__construct($logger, $userId, $permissionsService); $this->mapper = $mapper; $this->columnMapper = $columnMapper; From 2455d515eccb31a6da7ea849e7a6c94e9c015b2d Mon Sep 17 00:00:00 2001 From: Florian Date: Thu, 11 Jan 2024 15:28:12 +0100 Subject: [PATCH 44/73] Update lib/Command/TransferLegacyRows.php Co-authored-by: Arthur Schiwon Signed-off-by: Florian --- lib/Command/TransferLegacyRows.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Command/TransferLegacyRows.php b/lib/Command/TransferLegacyRows.php index 35fa7f4d7..49b255e4c 100644 --- a/lib/Command/TransferLegacyRows.php +++ b/lib/Command/TransferLegacyRows.php @@ -144,7 +144,7 @@ private function transferDataForTables(array $tables, OutputInterface $output) { * @throws InternalError * @throws Exception */ - private function transferTable(Table $table, OutputInterface $output) { + private function transferTable(Table $table, OutputInterface $output): void { $columns = $this->columnService->findAllByTable($table->getId(), null, ''); $output->writeln("---- Found " . count($columns) . " columns"); From 35e8ad534c25c53665c87ab3cff12e76c168bb24 Mon Sep 17 00:00:00 2001 From: Florian Date: Thu, 11 Jan 2024 15:28:36 +0100 Subject: [PATCH 45/73] Update lib/Command/TransferLegacyRows.php Co-authored-by: Arthur Schiwon Signed-off-by: Florian --- lib/Command/TransferLegacyRows.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Command/TransferLegacyRows.php b/lib/Command/TransferLegacyRows.php index 49b255e4c..cd42cec1d 100644 --- a/lib/Command/TransferLegacyRows.php +++ b/lib/Command/TransferLegacyRows.php @@ -162,7 +162,7 @@ private function transferTable(Table $table, OutputInterface $output): void { * @param OutputInterface $output * @return void */ - private function deleteDataForTables(array $tables, OutputInterface $output) { + private function deleteDataForTables(array $tables, OutputInterface $output): void { $output->writeln("Start deleting data for tables that should be transferred."); foreach ($tables as $table) { try { From 8b6699e7ad9707359af51e4b51e0c253d51df242 Mon Sep 17 00:00:00 2001 From: Florian Date: Thu, 11 Jan 2024 15:28:48 +0100 Subject: [PATCH 46/73] Update lib/Command/TransferLegacyRows.php Co-authored-by: Arthur Schiwon Signed-off-by: Florian --- lib/Command/TransferLegacyRows.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Command/TransferLegacyRows.php b/lib/Command/TransferLegacyRows.php index cd42cec1d..d5f556c73 100644 --- a/lib/Command/TransferLegacyRows.php +++ b/lib/Command/TransferLegacyRows.php @@ -111,7 +111,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } } else { $output->writeln("🤷🏻‍ Add at least one table id or add the option --all to transfer all tables."); - return 1; + return 2; } if (!$optionNoDelete) { $this->deleteDataForTables($tables, $output); From 46a045a66b12276e277f4d11fb25c52d1bcb7484 Mon Sep 17 00:00:00 2001 From: Florian Date: Thu, 11 Jan 2024 15:29:01 +0100 Subject: [PATCH 47/73] Update lib/Command/TransferLegacyRows.php Co-authored-by: Arthur Schiwon Signed-off-by: Florian --- lib/Command/TransferLegacyRows.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Command/TransferLegacyRows.php b/lib/Command/TransferLegacyRows.php index d5f556c73..ca6b21171 100644 --- a/lib/Command/TransferLegacyRows.php +++ b/lib/Command/TransferLegacyRows.php @@ -125,7 +125,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int * @param Table[] $tables * @return void */ - private function transferDataForTables(array $tables, OutputInterface $output) { + private function transferDataForTables(array $tables, OutputInterface $output): void { $i = 1; foreach ($tables as $table) { $output->writeln("-- Start transfer for table " . $table->getId() . " (" . $table->getTitle() . ") [" . $i . "/" . count($tables) . "]"); From 4e3e84868f3f95db7f085e6dc8132b3f2ad69279 Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Thu, 11 Jan 2024 15:30:59 +0100 Subject: [PATCH 48/73] use new row mapper in clean command Signed-off-by: Florian Steffens --- lib/Command/Clean.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/Command/Clean.php b/lib/Command/Clean.php index c08e6dacd..91a366e78 100644 --- a/lib/Command/Clean.php +++ b/lib/Command/Clean.php @@ -25,6 +25,7 @@ use OCA\Tables\Db\LegacyRow; use OCA\Tables\Db\LegacyRowMapper; +use OCA\Tables\Db\Row2Mapper; use OCA\Tables\Errors\InternalError; use OCA\Tables\Errors\NotFoundError; use OCA\Tables\Errors\PermissionError; @@ -50,7 +51,7 @@ class Clean extends Command { protected RowService $rowService; protected TableService $tableService; protected LoggerInterface $logger; - protected LegacyRowMapper $rowMapper; + protected Row2Mapper $rowMapper; private bool $dry = false; private int $truncateLength = 20; @@ -60,7 +61,7 @@ class Clean extends Command { private OutputInterface $output; - public function __construct(LoggerInterface $logger, ColumnService $columnService, RowService $rowService, TableService $tableService, LegacyRowMapper $rowMapper) { + public function __construct(LoggerInterface $logger, ColumnService $columnService, RowService $rowService, TableService $tableService, Row2Mapper $rowMapper) { parent::__construct(); $this->logger = $logger; $this->columnService = $columnService; From 9c4a182897cbad3ba5d771bb3ca1bd8428448c52 Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Fri, 12 Jan 2024 10:24:10 +0100 Subject: [PATCH 49/73] adjust the clean command to use the new db structure and code base Signed-off-by: Florian Steffens --- lib/Command/Clean.php | 55 ++++++++++++++++++++------------------ lib/Db/Row2Mapper.php | 15 +++++++++++ lib/Db/RowSleeveMapper.php | 16 +++++++++++ 3 files changed, 60 insertions(+), 26 deletions(-) diff --git a/lib/Command/Clean.php b/lib/Command/Clean.php index 91a366e78..40a286e53 100644 --- a/lib/Command/Clean.php +++ b/lib/Command/Clean.php @@ -23,8 +23,8 @@ namespace OCA\Tables\Command; -use OCA\Tables\Db\LegacyRow; -use OCA\Tables\Db\LegacyRowMapper; +use Exception; +use OCA\Tables\Db\Row2; use OCA\Tables\Db\Row2Mapper; use OCA\Tables\Errors\InternalError; use OCA\Tables\Errors\NotFoundError; @@ -32,9 +32,6 @@ use OCA\Tables\Service\ColumnService; use OCA\Tables\Service\RowService; use OCA\Tables\Service\TableService; -use OCP\AppFramework\Db\DoesNotExistException; -use OCP\AppFramework\Db\MultipleObjectsReturnedException; -use OCP\DB\Exception; use Psr\Log\LoggerInterface; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; @@ -56,7 +53,7 @@ class Clean extends Command { private bool $dry = false; private int $truncateLength = 20; - private ?LegacyRow $row = null; + private ?Row2 $row = null; private int $offset = -1; private OutputInterface $output; @@ -107,16 +104,21 @@ protected function execute(InputInterface $input, OutputInterface $output): int private function getNextRow():void { try { - $this->row = $this->rowMapper->findNext($this->offset); + $nextRowId = $this->rowMapper->findNextId($this->offset); + if ($nextRowId === null) { + $this->print(""); + $this->print("No more rows found.", self::PRINT_LEVEL_INFO); + $this->print(""); + $this->row = null; + return; + } + $tableId = $this->rowMapper->getTableIdForRow($nextRowId); + $columns = $this->columnService->findAllByTable($tableId, null, ''); + $this->row = $this->rowMapper->find($nextRowId, $columns); $this->offset = $this->row->getId(); - } catch (MultipleObjectsReturnedException|Exception $e) { + } catch (Exception $e) { $this->print('Error while fetching row', self::PRINT_LEVEL_ERROR); $this->logger->error('Following error occurred during executing occ command "'.self::class.'"', ['exception' => $e]); - } catch (DoesNotExistException $e) { - $this->print(""); - $this->print("No more rows found.", self::PRINT_LEVEL_INFO); - $this->print(""); - $this->row = null; } } @@ -142,14 +144,14 @@ private function checkIfColumnsForRowsExists(): void { } private function checkColumns(): void { - $data = json_decode($this->row->getData()); - foreach ($data as $date) { - $suffix = strlen($date->value) > $this->truncateLength ? "...": ""; + foreach ($this->row->getData() as $date) { + $valueAsString = is_string($date['value']) ? $date['value'] : json_encode($date['value']); + $suffix = strlen($valueAsString) > $this->truncateLength ? "...": ""; $this->print(""); - $this->print("columnId: " . $date->columnId . " -> " . substr($date->value, 0, $this->truncateLength) . $suffix, self::PRINT_LEVEL_INFO); + $this->print("columnId: " . $date['columnId'] . " -> " . substr($valueAsString, 0, $this->truncateLength) . $suffix, self::PRINT_LEVEL_INFO); try { - $this->columnService->find($date->columnId, ''); + $this->columnService->find($date['columnId'], ''); if($this->output->isVerbose()) { $this->print("column found", self::PRINT_LEVEL_SUCCESS); } @@ -160,10 +162,10 @@ private function checkColumns(): void { if($this->output->isVerbose()) { $this->print("corresponding column not found.", self::PRINT_LEVEL_ERROR); } else { - $this->print("columnId: " . $date->columnId . " not found, but needed by row ".$this->row->getId(), self::PRINT_LEVEL_WARNING); + $this->print("columnId: " . $date['columnId'] . " not found, but needed by row ".$this->row->getId(), self::PRINT_LEVEL_WARNING); } // if the corresponding column is not found, lets delete the data from the row. - $this->deleteDataFromRow($date->columnId); + $this->deleteDataFromRow($date['columnId']); } catch (PermissionError $e) { $this->print("😱️ permission error while looking for column", self::PRINT_LEVEL_ERROR); $this->logger->error('Following error occurred during executing occ command "'.self::class.'"', ['exception' => $e]); @@ -183,17 +185,18 @@ private function deleteDataFromRow(int $columnId): void { } $this->print("DANGER, start deleting", self::PRINT_LEVEL_WARNING); - $data = json_decode($this->row->getData(), true); + $data = $this->row->getData(); - // $this->print("Data before: \t".json_encode(array_values($data), 0), self::PRINT_LEVEL_INFO); + $this->print("Data before: \t".json_encode(array_values($data)), self::PRINT_LEVEL_INFO); $key = array_search($columnId, array_column($data, 'columnId')); unset($data[$key]); - // $this->print("Data after: \t".json_encode(array_values($data), 0), self::PRINT_LEVEL_INFO); - $this->row->setDataArray(array_values($data)); + $this->print("Data after: \t".json_encode(array_values($data)), self::PRINT_LEVEL_INFO); + $this->row->setData(array_values($data)); + try { - $this->rowMapper->update($this->row); + $this->rowMapper->update($this->row, $this->columnService->findAllByTable($this->row->getTableId())); $this->print("Row successfully updated", self::PRINT_LEVEL_SUCCESS); - } catch (Exception $e) { + } catch (InternalError|PermissionError $e) { $this->print("Error while updating row to db.", self::PRINT_LEVEL_ERROR); $this->logger->error('Following error occurred during executing occ command "'.self::class.'"', ['exception' => $e]); } diff --git a/lib/Db/Row2Mapper.php b/lib/Db/Row2Mapper.php index 004147339..8d22e1c2f 100644 --- a/lib/Db/Row2Mapper.php +++ b/lib/Db/Row2Mapper.php @@ -95,6 +95,21 @@ public function find(int $id, array $columns): Row2 { } } + /** + * @throws InternalError + */ + public function findNextId(int $offsetId = -1): ?int { + try { + $rowSleeve = $this->rowSleeveMapper->findNext($offsetId); + } catch (MultipleObjectsReturnedException|Exception $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + } catch (DoesNotExistException $e) { + return null; + } + return $rowSleeve->getId(); + } + /** * @throws DoesNotExistException * @throws MultipleObjectsReturnedException diff --git a/lib/Db/RowSleeveMapper.php b/lib/Db/RowSleeveMapper.php index c86f7e387..6932a5960 100644 --- a/lib/Db/RowSleeveMapper.php +++ b/lib/Db/RowSleeveMapper.php @@ -46,6 +46,22 @@ public function findMultiple(array $ids): array { return $this->findEntities($qb); } + /** + * @throws MultipleObjectsReturnedException + * @throws DoesNotExistException + * @throws Exception + */ + public function findNext(int $offsetId = -1): RowSleeve { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->table) + ->where($qb->expr()->gt('id', $qb->createNamedParameter($offsetId))) + ->setMaxResults(1) + ->orderBy('id', 'ASC'); + + return $this->findEntity($qb); + } + /** * @param int $sleeveId * @throws Exception From 2951d0737e838367b973e1edba000575a0c62169 Mon Sep 17 00:00:00 2001 From: Florian Date: Fri, 12 Jan 2024 10:35:00 +0100 Subject: [PATCH 50/73] Update lib/Service/RowService.php Co-authored-by: Arthur Schiwon Signed-off-by: Florian --- lib/Service/RowService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Service/RowService.php b/lib/Service/RowService.php index ef792b623..47d7a656f 100644 --- a/lib/Service/RowService.php +++ b/lib/Service/RowService.php @@ -126,7 +126,7 @@ public function find(int $id): Row2 { $row = $this->row2Mapper->find($id, $columns); } catch (InternalError $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage(), $e->getCode(), $e); } catch (NotFoundError $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); throw new NotFoundError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); From e785a9a2edf0037c70aca3af1ced10e4efd7fe4f Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Fri, 12 Jan 2024 10:30:28 +0100 Subject: [PATCH 51/73] only add the where clause if needed Signed-off-by: Florian Steffens --- lib/Db/TableMapper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Db/TableMapper.php b/lib/Db/TableMapper.php index c26fe2a26..5b62a167f 100644 --- a/lib/Db/TableMapper.php +++ b/lib/Db/TableMapper.php @@ -58,7 +58,7 @@ public function findAll(?string $userId = null): array { $qb = $this->db->getQueryBuilder(); $qb->select('*') ->from($this->table); - if ($userId != null) { + if ($userId !== null && $userId !== '') { $qb->where($qb->expr()->eq('ownership', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR))); } return $this->findEntities($qb); From 46be5b47b51f5953e093173bd6326b8a35807c01 Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Fri, 12 Jan 2024 10:32:47 +0100 Subject: [PATCH 52/73] code cleanup Signed-off-by: Florian Steffens --- lib/Service/RowService.php | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/lib/Service/RowService.php b/lib/Service/RowService.php index 47d7a656f..8ea7242fc 100644 --- a/lib/Service/RowService.php +++ b/lib/Service/RowService.php @@ -66,8 +66,6 @@ public function findAllByTable(int $tableId, string $userId, ?int $limit = null, try { if ($this->permissionsService->canReadRowsByElementId($tableId, 'table', $userId)) { return $this->row2Mapper->findAll($this->columnMapper->findAllByTable($tableId), $tableId, $limit, $offset, null, null, $userId); - - // return $this->mapper->findAllByTable($tableId, $limit, $offset); } else { throw new PermissionError('no read access to table id = '.$tableId); } @@ -95,8 +93,6 @@ public function findAllByView(int $viewId, string $userId, ?int $limit = null, ? $columnsArray = $view->getColumnsArray(); $columns = $this->columnMapper->findAll($columnsArray); return $this->row2Mapper->findAll($columns, $view->getTableId(), $limit, $offset, $view->getFilterArray(), $view->getSortArray(), $userId); - - // return $this->mapper->findAllByView($this->viewMapper->find($viewId), $userId, $limit, $offset); } else { throw new PermissionError('no read access to view id = '.$viewId); } @@ -208,16 +204,6 @@ public function create(?int $tableId, ?int $viewId, array $data): Row2 { $this->logger->error($e->getMessage(), ['exception' => $e]); throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); } - - /*$time = new DateTime(); - $item = new Row(); - $item->setDataArray($data); - $item->setTableId($viewId ? $view->getTableId() : $tableId); - $item->setCreatedBy($this->userId); - $item->setCreatedAt($time->format('Y-m-d H:i:s')); - $item->setLastEditBy($this->userId); - $item->setLastEditAt($time->format('Y-m-d H:i:s')); - return $this->mapper->insert($item);*/ } /** From 2b6945ef8ec6ed40472eaf945aeedfc4f0f33ef0 Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Fri, 12 Jan 2024 10:40:25 +0100 Subject: [PATCH 53/73] added some error details Signed-off-by: Florian Steffens --- lib/Service/RowService.php | 62 +++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/lib/Service/RowService.php b/lib/Service/RowService.php index 8ea7242fc..4b433a635 100644 --- a/lib/Service/RowService.php +++ b/lib/Service/RowService.php @@ -71,7 +71,7 @@ public function findAllByTable(int $tableId, string $userId, ?int $limit = null, } } catch (Exception $e) { $this->logger->error($e->getMessage()); - throw new InternalError($e->getMessage()); + throw new InternalError($e->getMessage(), $e->getCode(), $e); } } @@ -98,7 +98,7 @@ public function findAllByView(int $viewId, string $userId, ?int $limit = null, ? } } catch (Exception $e) { $this->logger->error($e->getMessage()); - throw new InternalError($e->getMessage()); + throw new InternalError($e->getMessage(), $e->getCode(), $e); } } @@ -115,7 +115,7 @@ public function find(int $id): Row2 { $columns = $this->columnMapper->findAllByTable($id); } catch (Exception $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage(), $e->getCode(), $e); } try { @@ -125,7 +125,7 @@ public function find(int $id): Row2 { throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage(), $e->getCode(), $e); } catch (NotFoundError $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new NotFoundError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + throw new NotFoundError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage(), $e->getCode(), $e); } // security @@ -151,7 +151,7 @@ public function create(?int $tableId, ?int $viewId, array $data): Row2 { if ($this->userId === null || $this->userId === '') { $e = new \Exception('No user id in context, but needed.'); $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage(), $e->getCode(), $e); } if ($viewId) { @@ -162,7 +162,7 @@ public function create(?int $tableId, ?int $viewId, array $data): Row2 { throw new NotFoundError("Given view could not be found. More details can be found in the log."); } catch (InternalError|Exception|MultipleObjectsReturnedException $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage(), $e->getCode(), $e); } // security @@ -176,20 +176,20 @@ public function create(?int $tableId, ?int $viewId, array $data): Row2 { $table = $this->tableMapper->find($tableId); } catch (DoesNotExistException $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new NotFoundError("Given table could not be found. More details can be found in the log."); + throw new NotFoundError("Given table could not be found. More details can be found in the log.", $e->getCode(), $e); } catch (MultipleObjectsReturnedException|Exception $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage(), $e->getCode(), $e); } // security if (!$this->permissionsService->canCreateRows($table, 'table')) { - throw new PermissionError('create row at the table id = '.$tableId.' is not allowed.'); + throw new PermissionError('create row at the table id = '.$tableId.' is not allowed.', $e->getCode(), $e); } $columns = $this->columnMapper->findAllByTable($tableId); } else { - throw new InternalError('Cannot create row without table or view in context'); + throw new InternalError('Cannot create row without table or view in context', $e->getCode(), $e); } $data = $this->cleanupData($data, $columns, $tableId, $viewId); @@ -202,7 +202,7 @@ public function create(?int $tableId, ?int $viewId, array $data): Row2 { return $this->row2Mapper->insert($row2, $this->columnMapper->findAllByTable($tableId)); } catch (InternalError|Exception $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage(), $e->getCode(), $e); } } @@ -230,7 +230,7 @@ private function cleanupData(array $data, array $columns, ?int $tableId, ?int $v if (!$column) { $e = new \Exception('No column found, can not parse value.'); $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage(), $e->getCode(), $e); } // parse given value to respect the column type value format @@ -290,16 +290,16 @@ private function getRowById(int $rowId): Row2 { if ($this->row2Mapper->getTableIdForRow($rowId) === null) { $e = new \Exception('No table id in row, but needed.'); $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage(), $e->getCode(), $e); } $row = $this->row2Mapper->find($rowId, $this->columnMapper->findAllByTable($this->row2Mapper->getTableIdForRow($rowId))); $row->markAsLoaded(); } catch (InternalError|DoesNotExistException|MultipleObjectsReturnedException|Exception $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage(), $e->getCode(), $e); } catch (NotFoundError $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new NotFoundError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + throw new NotFoundError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage(), $e->getCode(), $e); } $this->tmpRows[$rowId] = $row; return $row; @@ -328,10 +328,10 @@ public function updateSet( $item = $this->getRowById($id); } catch (InternalError $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage(), $e->getCode(), $e); } catch (NotFoundError $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new NotFoundError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + throw new NotFoundError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage(), $e->getCode(), $e); } if ($viewId) { @@ -339,24 +339,24 @@ public function updateSet( if (!$this->permissionsService->canUpdateRowsByViewId($viewId)) { $e = new \Exception('Update row is not allowed.'); $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage(), $e->getCode(), $e); } try { $view = $this->viewMapper->find($viewId); } catch (InternalError|MultipleObjectsReturnedException|Exception $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage(), $e->getCode(), $e); } catch (DoesNotExistException $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new NotFoundError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + throw new NotFoundError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage(), $e->getCode(), $e); } // is row in view? if($this->row2Mapper->isRowInViewPresent($id, $view, $userId)) { $e = new \Exception('Update row is not allowed.'); $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage(), $e->getCode(), $e); } // fetch all needed columns @@ -364,7 +364,7 @@ public function updateSet( $columns = $this->columnMapper->findMultiple($view->getColumnsArray()); } catch (Exception $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage(), $e->getCode(), $e); } } else { // if no view id is set, we assume a table and take the tableId from the row @@ -374,13 +374,13 @@ public function updateSet( if (!$this->permissionsService->canUpdateRowsByTableId($tableId)) { $e = new \Exception('Update row is not allowed.'); $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage(), $e->getCode(), $e); } try { $columns = $this->columnMapper->findAllByTable($tableId); } catch (Exception $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage(), $e->getCode(), $e); } } @@ -415,10 +415,10 @@ public function delete(int $id, ?int $viewId, string $userId): Row2 { $item = $this->getRowById($id); } catch (InternalError $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage(), $e->getCode(), $e); } catch (NotFoundError $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new NotFoundError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + throw new NotFoundError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage(), $e->getCode(), $e); } if ($viewId) { @@ -426,26 +426,26 @@ public function delete(int $id, ?int $viewId, string $userId): Row2 { if (!$this->permissionsService->canDeleteRowsByViewId($viewId)) { $e = new \Exception('Update row is not allowed.'); $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage(), $e->getCode(), $e); } try { $view = $this->viewMapper->find($viewId); } catch (InternalError|DoesNotExistException|MultipleObjectsReturnedException|Exception $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage(), $e->getCode(), $e); } $rowIds = $this->mapper->getRowIdsOfView($view, $userId); if(!in_array($id, $rowIds)) { $e = new \Exception('Update row is not allowed.'); $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage(), $e->getCode(), $e); } } else { // security if (!$this->permissionsService->canDeleteRowsByTableId($item->getTableId())) { $e = new \Exception('Update row is not allowed.'); $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new PermissionError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + throw new PermissionError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage(), $e->getCode(), $e); } } @@ -453,7 +453,7 @@ public function delete(int $id, ?int $viewId, string $userId): Row2 { return $this->row2Mapper->delete($item); } catch (Exception $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage(), $e->getCode(), $e); } } From 32fb5a2ab9f21ac821b0adedf7d494ce1300fed4 Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Fri, 12 Jan 2024 10:46:31 +0100 Subject: [PATCH 54/73] Revert "added some error details" This reverts commit b87f19c1665b6b7700d5ee1cdd9e4cf1c548769b. --- lib/Service/RowService.php | 62 +++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/lib/Service/RowService.php b/lib/Service/RowService.php index 4b433a635..8ea7242fc 100644 --- a/lib/Service/RowService.php +++ b/lib/Service/RowService.php @@ -71,7 +71,7 @@ public function findAllByTable(int $tableId, string $userId, ?int $limit = null, } } catch (Exception $e) { $this->logger->error($e->getMessage()); - throw new InternalError($e->getMessage(), $e->getCode(), $e); + throw new InternalError($e->getMessage()); } } @@ -98,7 +98,7 @@ public function findAllByView(int $viewId, string $userId, ?int $limit = null, ? } } catch (Exception $e) { $this->logger->error($e->getMessage()); - throw new InternalError($e->getMessage(), $e->getCode(), $e); + throw new InternalError($e->getMessage()); } } @@ -115,7 +115,7 @@ public function find(int $id): Row2 { $columns = $this->columnMapper->findAllByTable($id); } catch (Exception $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage(), $e->getCode(), $e); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); } try { @@ -125,7 +125,7 @@ public function find(int $id): Row2 { throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage(), $e->getCode(), $e); } catch (NotFoundError $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new NotFoundError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage(), $e->getCode(), $e); + throw new NotFoundError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); } // security @@ -151,7 +151,7 @@ public function create(?int $tableId, ?int $viewId, array $data): Row2 { if ($this->userId === null || $this->userId === '') { $e = new \Exception('No user id in context, but needed.'); $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage(), $e->getCode(), $e); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); } if ($viewId) { @@ -162,7 +162,7 @@ public function create(?int $tableId, ?int $viewId, array $data): Row2 { throw new NotFoundError("Given view could not be found. More details can be found in the log."); } catch (InternalError|Exception|MultipleObjectsReturnedException $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage(), $e->getCode(), $e); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); } // security @@ -176,20 +176,20 @@ public function create(?int $tableId, ?int $viewId, array $data): Row2 { $table = $this->tableMapper->find($tableId); } catch (DoesNotExistException $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new NotFoundError("Given table could not be found. More details can be found in the log.", $e->getCode(), $e); + throw new NotFoundError("Given table could not be found. More details can be found in the log."); } catch (MultipleObjectsReturnedException|Exception $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage(), $e->getCode(), $e); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); } // security if (!$this->permissionsService->canCreateRows($table, 'table')) { - throw new PermissionError('create row at the table id = '.$tableId.' is not allowed.', $e->getCode(), $e); + throw new PermissionError('create row at the table id = '.$tableId.' is not allowed.'); } $columns = $this->columnMapper->findAllByTable($tableId); } else { - throw new InternalError('Cannot create row without table or view in context', $e->getCode(), $e); + throw new InternalError('Cannot create row without table or view in context'); } $data = $this->cleanupData($data, $columns, $tableId, $viewId); @@ -202,7 +202,7 @@ public function create(?int $tableId, ?int $viewId, array $data): Row2 { return $this->row2Mapper->insert($row2, $this->columnMapper->findAllByTable($tableId)); } catch (InternalError|Exception $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage(), $e->getCode(), $e); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); } } @@ -230,7 +230,7 @@ private function cleanupData(array $data, array $columns, ?int $tableId, ?int $v if (!$column) { $e = new \Exception('No column found, can not parse value.'); $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage(), $e->getCode(), $e); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); } // parse given value to respect the column type value format @@ -290,16 +290,16 @@ private function getRowById(int $rowId): Row2 { if ($this->row2Mapper->getTableIdForRow($rowId) === null) { $e = new \Exception('No table id in row, but needed.'); $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage(), $e->getCode(), $e); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); } $row = $this->row2Mapper->find($rowId, $this->columnMapper->findAllByTable($this->row2Mapper->getTableIdForRow($rowId))); $row->markAsLoaded(); } catch (InternalError|DoesNotExistException|MultipleObjectsReturnedException|Exception $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage(), $e->getCode(), $e); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); } catch (NotFoundError $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new NotFoundError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage(), $e->getCode(), $e); + throw new NotFoundError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); } $this->tmpRows[$rowId] = $row; return $row; @@ -328,10 +328,10 @@ public function updateSet( $item = $this->getRowById($id); } catch (InternalError $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage(), $e->getCode(), $e); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); } catch (NotFoundError $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new NotFoundError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage(), $e->getCode(), $e); + throw new NotFoundError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); } if ($viewId) { @@ -339,24 +339,24 @@ public function updateSet( if (!$this->permissionsService->canUpdateRowsByViewId($viewId)) { $e = new \Exception('Update row is not allowed.'); $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage(), $e->getCode(), $e); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); } try { $view = $this->viewMapper->find($viewId); } catch (InternalError|MultipleObjectsReturnedException|Exception $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage(), $e->getCode(), $e); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); } catch (DoesNotExistException $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new NotFoundError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage(), $e->getCode(), $e); + throw new NotFoundError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); } // is row in view? if($this->row2Mapper->isRowInViewPresent($id, $view, $userId)) { $e = new \Exception('Update row is not allowed.'); $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage(), $e->getCode(), $e); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); } // fetch all needed columns @@ -364,7 +364,7 @@ public function updateSet( $columns = $this->columnMapper->findMultiple($view->getColumnsArray()); } catch (Exception $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage(), $e->getCode(), $e); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); } } else { // if no view id is set, we assume a table and take the tableId from the row @@ -374,13 +374,13 @@ public function updateSet( if (!$this->permissionsService->canUpdateRowsByTableId($tableId)) { $e = new \Exception('Update row is not allowed.'); $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage(), $e->getCode(), $e); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); } try { $columns = $this->columnMapper->findAllByTable($tableId); } catch (Exception $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage(), $e->getCode(), $e); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); } } @@ -415,10 +415,10 @@ public function delete(int $id, ?int $viewId, string $userId): Row2 { $item = $this->getRowById($id); } catch (InternalError $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage(), $e->getCode(), $e); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); } catch (NotFoundError $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new NotFoundError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage(), $e->getCode(), $e); + throw new NotFoundError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); } if ($viewId) { @@ -426,26 +426,26 @@ public function delete(int $id, ?int $viewId, string $userId): Row2 { if (!$this->permissionsService->canDeleteRowsByViewId($viewId)) { $e = new \Exception('Update row is not allowed.'); $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage(), $e->getCode(), $e); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); } try { $view = $this->viewMapper->find($viewId); } catch (InternalError|DoesNotExistException|MultipleObjectsReturnedException|Exception $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage(), $e->getCode(), $e); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); } $rowIds = $this->mapper->getRowIdsOfView($view, $userId); if(!in_array($id, $rowIds)) { $e = new \Exception('Update row is not allowed.'); $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage(), $e->getCode(), $e); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); } } else { // security if (!$this->permissionsService->canDeleteRowsByTableId($item->getTableId())) { $e = new \Exception('Update row is not allowed.'); $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new PermissionError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage(), $e->getCode(), $e); + throw new PermissionError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); } } @@ -453,7 +453,7 @@ public function delete(int $id, ?int $viewId, string $userId): Row2 { return $this->row2Mapper->delete($item); } catch (Exception $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage(), $e->getCode(), $e); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); } } From b08ed0bb0be307f94df8ead04d230c0c74708a9e Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Fri, 12 Jan 2024 10:51:45 +0100 Subject: [PATCH 55/73] adjust psalm return types Signed-off-by: Florian Steffens --- lib/ResponseDefinitions.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/ResponseDefinitions.php b/lib/ResponseDefinitions.php index 1f36de842..42668bce5 100644 --- a/lib/ResponseDefinitions.php +++ b/lib/ResponseDefinitions.php @@ -20,8 +20,8 @@ * lastEditAt: string, * description: string|null, * columns: int[], - * sort: ?array{int, array{columnId: int, mode: 'ASC'|'DESC'}}, - * filter: ?array{int, array{columnId: int, operator: '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: string|int|float}}, + * sort: list, + * filter: list>, * isShared: bool, * onSharePermissions: ?array{ * read: bool, From 6dd64f6dcf14a1b7bdde745f9ecb0d35b03ae06c Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Fri, 12 Jan 2024 11:15:21 +0100 Subject: [PATCH 56/73] add CI upgrade test Signed-off-by: Florian Steffens --- .github/workflows/app-upgrade-mysql.yml | 148 ++++++++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 .github/workflows/app-upgrade-mysql.yml diff --git a/.github/workflows/app-upgrade-mysql.yml b/.github/workflows/app-upgrade-mysql.yml new file mode 100644 index 000000000..9a56bb710 --- /dev/null +++ b/.github/workflows/app-upgrade-mysql.yml @@ -0,0 +1,148 @@ +name: app upgrade mysql + +on: pull_request + +permissions: + contents: read + +concurrency: + group: app-upgrade-mysql-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + changes: + runs-on: ubuntu-latest + + outputs: + src: ${{ steps.changes.outputs.src}} + + steps: + - uses: dorny/paths-filter@4512585405083f25c027a35db413c2b3b9006d50 # v2.11.1 + id: changes + continue-on-error: true + with: + filters: | + src: + - '.github/workflows/**' + - 'appinfo/**' + - 'lib/**' + - 'templates/**' + - 'tests/**' + - 'vendor/**' + - 'vendor-bin/**' + - '.php-cs-fixer.dist.php' + - 'composer.json' + - 'composer.lock' + + app-upgrade-mysql: + runs-on: ubuntu-latest + + needs: changes + if: needs.changes.outputs.src != 'false' + + strategy: + matrix: + php-versions: ['8.2'] + server-versions: ['master'] + + services: + mysql: + image: ghcr.io/nextcloud/continuous-integration-mysql-8.0:latest + ports: + - 4444:3306/tcp + env: + MYSQL_ROOT_PASSWORD: rootpassword + options: --health-cmd="mysqladmin ping" --health-interval 5s --health-timeout 2s --health-retries 5 + + steps: + - name: Set app env + run: | + # Split and keep last + echo "APP_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV + + - name: Checkout server + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + with: + submodules: true + repository: nextcloud/server + ref: ${{ matrix.server-versions }} + + - name: Register text Git reference + run: | + text_app_ref="$(if [ "${{ matrix.server-versions }}" = "master" ]; then echo -n "main"; else echo -n "${{ matrix.server-versions }}"; fi)" + echo "text_app_ref=$text_app_ref" >> $GITHUB_ENV + + - name: Checkout text app + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + with: + repository: nextcloud/text + path: apps/text + ref: ${{ env.text_app_ref }} + + - name: Checkout viewer app + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + with: + repository: nextcloud/viewer + path: apps/viewer + ref: ${{ matrix.server-versions }} + + - name: Set up php ${{ matrix.php-versions }} + uses: shivammathur/setup-php@e6f75134d35752277f093989e72e140eaa222f35 # v2 + with: + php-version: ${{ matrix.php-versions }} + # https://docs.nextcloud.com/server/stable/admin_manual/installation/source_installation.html#prerequisites-for-manual-installation + extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib, mysql, pdo_mysql, sqlite, pdo_sqlite + coverage: none + ini-file: development + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Enable ONLY_FULL_GROUP_BY MySQL option + run: | + echo "SET GLOBAL sql_mode=(SELECT CONCAT(@@sql_mode,',ONLY_FULL_GROUP_BY'));" | mysql -h 127.0.0.1 -P 4444 -u root -prootpassword + echo "SELECT @@sql_mode;" | mysql -h 127.0.0.1 -P 4444 -u root -prootpassword + + - name: Set up Nextcloud, enable tables from app store + env: + DB_PORT: 4444 + run: | + mkdir data + ./occ maintenance:install --verbose --database=mysql --database-name=nextcloud --database-host=127.0.0.1 --database-port=$DB_PORT --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass admin + ./occ app:enable --force ${{ env.APP_NAME }} + + - name: Checkout app + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + with: + path: apps/${{ env.APP_NAME }} + + - name: Set up dependencies + working-directory: apps/${{ env.APP_NAME }} + run: composer i --no-dev + + - name: Upgrade Nextcloud and see whether the app still works + run: | + ./occ upgrade + ./occ app:list + + - name: Upload nextcloud logs + uses: actions/upload-artifact@v4 + if: always() + with: + name: nextcloud.log + path: data/nextcloud.log + retention-days: 5 + + + summary: + permissions: + contents: none + runs-on: ubuntu-latest + needs: [changes, app-upgrade-mysql] + + if: always() + + name: app-upgrade-mysql-summary + + steps: + - name: Summary status + run: if ${{ needs.changes.outputs.src != 'false' && needs.app-upgrade-mysql.result != 'success' }}; then exit 1; fi From baf54ca122a5d952612f23e8747a8339e0f86048 Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Fri, 12 Jan 2024 12:34:50 +0100 Subject: [PATCH 57/73] add unit test for data migration to new db modal - prepare LegacyRowMapper.php and Row2.php - add LegacyRowMapperTest.php Signed-off-by: Florian Steffens --- lib/Db/LegacyRowMapper.php | 22 ++++- lib/Db/Row2.php | 2 +- tests/unit/Service/LegacyRowMapperTest.php | 107 +++++++++++++++++++++ 3 files changed, 128 insertions(+), 3 deletions(-) create mode 100644 tests/unit/Service/LegacyRowMapperTest.php diff --git a/lib/Db/LegacyRowMapper.php b/lib/Db/LegacyRowMapper.php index 0e5ac383d..a70b47e2e 100644 --- a/lib/Db/LegacyRowMapper.php +++ b/lib/Db/LegacyRowMapper.php @@ -37,7 +37,17 @@ class LegacyRowMapper extends QBMapper { protected int $platform; - public function __construct(IDBConnection $db, LoggerInterface $logger, TextColumnQB $textColumnQB, SelectionColumnQB $selectionColumnQB, NumberColumnQB $numberColumnQB, DatetimeColumnQB $datetimeColumnQB, SuperColumnQB $columnQB, ColumnMapper $columnMapper, UserHelper $userHelper, Row2Mapper $rowMapper) { + public function __construct( + IDBConnection $db, + LoggerInterface $logger, + TextColumnQB $textColumnQB, + SelectionColumnQB $selectionColumnQB, + NumberColumnQB $numberColumnQB, + DatetimeColumnQB $datetimeColumnQB, + SuperColumnQB $columnQB, + ColumnMapper $columnMapper, + UserHelper $userHelper, + Row2Mapper $rowMapper) { parent::__construct($db, $this->table, LegacyRow::class); $this->logger = $logger; $this->textColumnQB = $textColumnQB; @@ -435,6 +445,14 @@ public function findByView(int $id, View $view): LegacyRow { * @throws InternalError */ public function transferLegacyRow(LegacyRow $legacyRow, array $columns) { + $this->rowMapper->insert($this->migrateLegacyRow($legacyRow), $columns); + } + + /** + * @param LegacyRow $legacyRow + * @return Row2 + */ + public function migrateLegacyRow(LegacyRow $legacyRow): Row2 { $row = new Row2(); $row->setId($legacyRow->getId()); $row->setTableId($legacyRow->getTableId()); @@ -443,6 +461,6 @@ public function transferLegacyRow(LegacyRow $legacyRow, array $columns) { $row->setLastEditBy($legacyRow->getLastEditBy()); $row->setLastEditAt($legacyRow->getLastEditAt()); $row->setData($legacyRow->getDataArray()); - $this->rowMapper->insert($row, $columns); + return $row; } } diff --git a/lib/Db/Row2.php b/lib/Db/Row2.php index e95f08046..7acabf859 100644 --- a/lib/Db/Row2.php +++ b/lib/Db/Row2.php @@ -118,11 +118,11 @@ public function jsonSerialize(): array { return [ 'id' => $this->id, 'tableId' => $this->tableId, - 'data' => $this->data, 'createdBy' => $this->createdBy, 'createdAt' => $this->createdAt, 'lastEditBy' => $this->lastEditBy, 'lastEditAt' => $this->lastEditAt, + 'data' => $this->data, ]; } diff --git a/tests/unit/Service/LegacyRowMapperTest.php b/tests/unit/Service/LegacyRowMapperTest.php new file mode 100644 index 000000000..62811f25b --- /dev/null +++ b/tests/unit/Service/LegacyRowMapperTest.php @@ -0,0 +1,107 @@ + 1, + 'value' => 'one' + ], + // assume column is a number column + [ + 'columnId' => 2, + 'value' => 22.2 + ], + // assume column is a selection column + [ + 'columnId' => 3, + 'value' => 1 + ], + // assume columns are selection-check columns + [ + 'columnId' => 4, + 'value' => '"true"' + ], + [ + 'columnId' => 5, + 'value' => '"false"' + ], + // assume columns are selection-multi columns + [ + 'columnId' => 6, + 'value' => '[1]' + ], + [ + 'columnId' => 7, + 'value' => '[2,3]' + ], + [ + 'columnId' => 8, + 'value' => 'null' + ], + // assume columns are datetime columns + [ + 'columnId' => 9, + 'value' => '2023-12-24 10:00' + ], + [ + 'columnId' => 10, + 'value' => '2023-12-25' + ], + [ + 'columnId' => 11, + 'value' => '11:11' + ], + ]; + + $dbConnection = Server::get(IDBConnection::class); + $textColumnQb = $this->createMock(TextColumnQB::class); + $selectionColumnQb = $this->createMock(SelectionColumnQB::class); + $numberColumnQb = $this->createMock(NumberColumnQB::class); + $datetimeColumnQb = $this->createMock(DatetimeColumnQB::class); + $superColumnQb = $this->createMock(SuperColumnQB::class); + $columnMapper = $this->createMock(ColumnMapper::class); + $row2Mapper = $this->createMock(Row2Mapper::class); + $logger = $this->createMock(LoggerInterface::class); + $userHelper = $this->createMock(UserHelper::class); + $legacyRowMapper = new LegacyRowMapper($dbConnection, $logger, $textColumnQb, $selectionColumnQb, $numberColumnQb, $datetimeColumnQb, $superColumnQb, $columnMapper, $userHelper, $row2Mapper); + + $legacyRow = new LegacyRow(); + $legacyRow->setId(5); + $legacyRow->setTableId(10); + $legacyRow->setCreatedBy('user1'); + $legacyRow->setCreatedAt('2023-12-24 09:00:00'); + $legacyRow->setLastEditAt('2023-12-24 09:30:00'); + $legacyRow->setLastEditBy('user1'); + $legacyRow->setDataArray($data); + + $row2 = $legacyRowMapper->migrateLegacyRow($legacyRow); + $data2 = $row2->getData(); + + self::assertTrue($data === $data2); + self::assertTrue($legacyRow->jsonSerialize() === $row2->jsonSerialize()); + } +} From e84c1029f71b1af3ac1f7638595b4185fd282b99 Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Tue, 16 Jan 2024 09:46:12 +0100 Subject: [PATCH 58/73] simplify the columns definition helper Signed-off-by: Florian Steffens --- lib/Db/Row2Mapper.php | 4 +- lib/Helper/ColumnsHelper.php | 52 ++----------------- .../Version000700Date20230916000000.php | 4 +- 3 files changed, 9 insertions(+), 51 deletions(-) diff --git a/lib/Db/Row2Mapper.php b/lib/Db/Row2Mapper.php index 8d22e1c2f..6d86e4f76 100644 --- a/lib/Db/Row2Mapper.php +++ b/lib/Db/Row2Mapper.php @@ -49,7 +49,7 @@ public function __construct(?string $userId, IDBConnection $db, LoggerInterface public function delete(Row2 $row): Row2 { $this->db->beginTransaction(); try { - foreach ($this->columnsHelper->get(['name']) as $columnType) { + foreach ($this->columnsHelper->columns as $columnType) { $cellMapperClassName = 'OCA\Tables\Db\RowCell' . ucfirst($columnType) . 'Mapper'; /** @var RowCellMapperSuper $cellMapper */ try { @@ -187,7 +187,7 @@ private function getRows(array $rowIds, array $columnIds): array { $qb = $this->db->getQueryBuilder(); $qbSqlForColumnTypes = null; - foreach ($this->columnsHelper->get(['name']) as $columnType) { + foreach ($this->columnsHelper->columns as $columnType) { $qbTmp = $this->db->getQueryBuilder(); $qbTmp->select('*') ->from('tables_row_cells_'.$columnType) diff --git a/lib/Helper/ColumnsHelper.php b/lib/Helper/ColumnsHelper.php index 720e82260..338ad07cd 100644 --- a/lib/Helper/ColumnsHelper.php +++ b/lib/Helper/ColumnsHelper.php @@ -2,54 +2,12 @@ namespace OCA\Tables\Helper; -use OCP\DB\Types; - class ColumnsHelper { - private array $columns = [ - [ - 'name' => 'text', - 'db_type' => Types::TEXT, - ], - [ - 'name' => 'number', - 'db_type' => Types::FLOAT, - ], - [ - 'name' => 'datetime', - 'db_type' => Types::TEXT, - ], - [ - 'name' => 'selection', - 'db_type' => Types::TEXT, - ], + public array $columns = [ + 'text', + 'number', + 'datetime', + 'selection' ]; - - /** - * @param string[] $keys Keys that should be returned - * @return array - */ - public function get(array $keys): array { - $arr = []; - foreach ($this->columns as $column) { - $c = []; - foreach ($keys as $key) { - if (isset($column[$key])) { - $c[$key] = $column[$key]; - } else { - $c[$key] = null; - } - } - $arr[] = $c; - } - - if (count($keys) <= 1) { - $out = []; - foreach ($arr as $item) { - $out[] = $item[$keys[0]]; - } - return $out; - } - return $arr; - } } diff --git a/lib/Migration/Version000700Date20230916000000.php b/lib/Migration/Version000700Date20230916000000.php index beba28a48..b9e138c0f 100644 --- a/lib/Migration/Version000700Date20230916000000.php +++ b/lib/Migration/Version000700Date20230916000000.php @@ -16,8 +16,8 @@ class Version000700Date20230916000000 extends SimpleMigrationStep { /** - * this is a copy from the definition set in OCA\Tables\Helper\ColumnsHelper - * this has to be in sync! but the definition can not be used directly + * this is a copy from the definition set in OCA\Tables\Helper\ColumnsHelper with added types + * the names have to be in sync! but the definition can not be used directly * because it might cause problems on auto web updates * (class might not be loaded if it gets replaced during the runtime) */ From 152ed103a7fd36c9916f1a74ba37ca3efaa03a15 Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Tue, 16 Jan 2024 09:54:52 +0100 Subject: [PATCH 59/73] only trigger CI upgrade test on PR with destination branch main Signed-off-by: Florian Steffens --- .github/workflows/app-upgrade-mysql.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/app-upgrade-mysql.yml b/.github/workflows/app-upgrade-mysql.yml index 9a56bb710..39001b40f 100644 --- a/.github/workflows/app-upgrade-mysql.yml +++ b/.github/workflows/app-upgrade-mysql.yml @@ -1,6 +1,8 @@ name: app upgrade mysql -on: pull_request +on: + pull_request: + branches: main permissions: contents: read From 1bdc6da6625c23f5c3b75f692a2010feebcf5f1f Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Tue, 16 Jan 2024 09:57:15 +0100 Subject: [PATCH 60/73] remove unnecessary method Signed-off-by: Florian Steffens --- lib/Db/Row2.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/Db/Row2.php b/lib/Db/Row2.php index 7acabf859..6b1b6adb0 100644 --- a/lib/Db/Row2.php +++ b/lib/Db/Row2.php @@ -107,10 +107,6 @@ public function insertOrUpdateCell(array $entry): string { return 'inserted'; } - public function removeCell(int $columnId): void { - // TODO - } - /** * @psalm-return TablesRow */ From bb74f1976400f6fd6d07692ea421972d8c6f7af3 Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Tue, 16 Jan 2024 10:02:57 +0100 Subject: [PATCH 61/73] fix permission check Signed-off-by: Florian Steffens --- lib/Service/RowService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Service/RowService.php b/lib/Service/RowService.php index 8ea7242fc..a4f3c0a1e 100644 --- a/lib/Service/RowService.php +++ b/lib/Service/RowService.php @@ -353,7 +353,7 @@ public function updateSet( } // is row in view? - if($this->row2Mapper->isRowInViewPresent($id, $view, $userId)) { + if(!$this->row2Mapper->isRowInViewPresent($id, $view, $userId)) { $e = new \Exception('Update row is not allowed.'); $this->logger->error($e->getMessage(), ['exception' => $e]); throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); From 1cdcbac29770d64b1e1ad34d083d75e2a2a5e765 Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Thu, 18 Jan 2024 14:11:17 +0100 Subject: [PATCH 62/73] Add command for clean up legacy rows Signed-off-by: Florian Steffens --- appinfo/info.xml | 1 + lib/Command/CleanLegacy.php | 224 ++++++++++++++++++++++++++++++++++++ 2 files changed, 225 insertions(+) create mode 100644 lib/Command/CleanLegacy.php diff --git a/appinfo/info.xml b/appinfo/info.xml index 92bbfa23d..6c23467c2 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -58,6 +58,7 @@ Have a good time and manage whatever you want. OCA\Tables\Command\RenameTable OCA\Tables\Command\ChangeOwnershipTable OCA\Tables\Command\Clean + OCA\Tables\Command\CleanLegacy OCA\Tables\Command\TransferLegacyRows diff --git a/lib/Command/CleanLegacy.php b/lib/Command/CleanLegacy.php new file mode 100644 index 000000000..be2a7868a --- /dev/null +++ b/lib/Command/CleanLegacy.php @@ -0,0 +1,224 @@ + + * + * @author Florian Steffens + * + * @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 . + * + */ + +namespace OCA\Tables\Command; + +use OCA\Tables\Db\LegacyRow; +use OCA\Tables\Db\LegacyRowMapper; +use OCA\Tables\Errors\InternalError; +use OCA\Tables\Errors\NotFoundError; +use OCA\Tables\Errors\PermissionError; +use OCA\Tables\Service\ColumnService; +use OCA\Tables\Service\RowService; +use OCA\Tables\Service\TableService; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Db\MultipleObjectsReturnedException; +use OCP\DB\Exception; +use Psr\Log\LoggerInterface; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +class CleanLegacy extends Command { + public const PRINT_LEVEL_SUCCESS = 1; + public const PRINT_LEVEL_INFO = 2; + public const PRINT_LEVEL_WARNING = 3; + public const PRINT_LEVEL_ERROR = 4; + + protected ColumnService $columnService; + protected RowService $rowService; + protected TableService $tableService; + protected LoggerInterface $logger; + protected LegacyRowMapper $rowMapper; + + private bool $dry = false; + private int $truncateLength = 20; + + private ?LegacyRow $row = null; + private int $offset = -1; + + private OutputInterface $output; + + public function __construct(LoggerInterface $logger, ColumnService $columnService, RowService $rowService, TableService $tableService, LegacyRowMapper $rowMapper) { + parent::__construct(); + $this->logger = $logger; + $this->columnService = $columnService; + $this->rowService = $rowService; + $this->tableService = $tableService; + $this->rowMapper = $rowMapper; + } + + protected function configure(): void { + $this + ->setName('tables:legacy:clean') + ->setDescription('Clean the tables legacy data.') + ->addOption( + 'dry', + 'd', + InputOption::VALUE_NONE, + 'Prints all wanted changes, but do not write anything to the database.' + ) + ; + } + + /** + * @param InputInterface $input + * @param OutputInterface $output + * @return int + */ + protected function execute(InputInterface $input, OutputInterface $output): int { + $this->output = $output; + $this->dry = !!$input->getOption('dry'); + + if ($this->dry) { + $this->print("Dry run activated."); + } + if ($output->isVerbose()) { + $this->print("Verbose mode activated."); + } + + // check action, starting point for magic + $this->checkIfColumnsForRowsExists(); + + return 0; + } + + private function getNextRow():void { + try { + $this->row = $this->rowMapper->findNext($this->offset); + $this->offset = $this->row->getId(); + } catch (MultipleObjectsReturnedException|Exception $e) { + $this->print('Error while fetching row', self::PRINT_LEVEL_ERROR); + $this->logger->error('Following error occurred during executing occ command "'.self::class.'"', ['exception' => $e]); + } catch (DoesNotExistException $e) { + $this->print(""); + $this->print("No more rows found.", self::PRINT_LEVEL_INFO); + $this->print(""); + $this->row = null; + } + } + + + /** + * Take each data set from all rows and check if the column (mapped by id) exists + * + * @return void + */ + private function checkIfColumnsForRowsExists(): void { + + $this->getNextRow(); + while ($this->row) { + $this->print(""); + $this->print(""); + $this->print("Lets check row with id = " . $this->row->getId()); + $this->print("=========================================="); + + $this->checkColumns(); + + $this->getNextRow(); + } + } + + private function checkColumns(): void { + $data = json_decode($this->row->getData()); + foreach ($data as $date) { + // this is a fix and possible because we don't really need the row data + if (is_array($date->value)) { + $date->value = json_encode($date->value); + } + $suffix = strlen($date->value) > $this->truncateLength ? "...": ""; + $this->print(""); + $this->print("columnId: " . $date->columnId . " -> " . substr($date->value, 0, $this->truncateLength) . $suffix, self::PRINT_LEVEL_INFO); + + try { + $this->columnService->find($date->columnId, ''); + if($this->output->isVerbose()) { + $this->print("column found", self::PRINT_LEVEL_SUCCESS); + } + } catch (InternalError $e) { + $this->print("😱️ internal error while looking for column", self::PRINT_LEVEL_ERROR); + $this->logger->error('Following error occurred during executing occ command "'.self::class.'"', ['exception' => $e]); + } catch (NotFoundError $e) { + if($this->output->isVerbose()) { + $this->print("corresponding column not found.", self::PRINT_LEVEL_ERROR); + } else { + $this->print("columnId: " . $date->columnId . " not found, but needed by row ".$this->row->getId(), self::PRINT_LEVEL_WARNING); + } + // if the corresponding column is not found, lets delete the data from the row. + $this->deleteDataFromRow($date->columnId); + } catch (PermissionError $e) { + $this->print("😱️ permission error while looking for column", self::PRINT_LEVEL_ERROR); + $this->logger->error('Following error occurred during executing occ command "'.self::class.'"', ['exception' => $e]); + } + } + } + + /** + * Deletes the data for a given columnID from the dataset within a row + * @param int $columnId + * @return void + */ + private function deleteDataFromRow(int $columnId): void { + if ($this->dry) { + $this->print("Is dry run, will not remove the column data from row dataset.", self::PRINT_LEVEL_INFO); + return; + } + + $this->print("DANGER, start deleting", self::PRINT_LEVEL_WARNING); + $data = json_decode($this->row->getData(), true); + + // $this->print("Data before: \t".json_encode(array_values($data), 0), self::PRINT_LEVEL_INFO); + $key = array_search($columnId, array_column($data, 'columnId')); + unset($data[$key]); + // $this->print("Data after: \t".json_encode(array_values($data), 0), self::PRINT_LEVEL_INFO); + $this->row->setDataArray(array_values($data)); + try { + $this->rowMapper->update($this->row); + $this->print("Row successfully updated", self::PRINT_LEVEL_SUCCESS); + } catch (Exception $e) { + $this->print("Error while updating row to db.", self::PRINT_LEVEL_ERROR); + $this->logger->error('Following error occurred during executing occ command "'.self::class.'"', ['exception' => $e]); + } + } + + private function print(string $message, int $level = null): void { + if($level === self::PRINT_LEVEL_SUCCESS) { + echo "✅ ".$message; + echo "\n"; + } elseif ($level === self::PRINT_LEVEL_INFO && $this->output->isVerbose()) { + echo "ℹ️ ".$message; + echo "\n"; + } elseif ($level === self::PRINT_LEVEL_WARNING) { + echo "⚠️ ".$message; + echo "\n"; + } elseif ($level === self::PRINT_LEVEL_ERROR) { + echo "❌ ".$message; + echo "\n"; + } elseif ($this->output->isVerbose()) { + echo $message; + echo "\n"; + } + } + +} From aeae31a63996de8b22f94650ac089df1f8800fdb Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Thu, 18 Jan 2024 14:12:18 +0100 Subject: [PATCH 63/73] cleanup Signed-off-by: Florian Steffens --- appinfo/info.xml | 2 +- lib/Command/TransferLegacyRows.php | 5 +++++ lib/Db/Row2Mapper.php | 12 +----------- lib/Migration/NewDbStructureRepairStep.php | 2 +- 4 files changed, 8 insertions(+), 13 deletions(-) diff --git a/appinfo/info.xml b/appinfo/info.xml index 6c23467c2..251444002 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -22,7 +22,7 @@ Share your tables and views with users and groups within your cloud. Have a good time and manage whatever you want. ]]> - 0.7.0 + 0.7.0-dev.1 agpl Florian Steffens Tables diff --git a/lib/Command/TransferLegacyRows.php b/lib/Command/TransferLegacyRows.php index ca6b21171..0c12555e6 100644 --- a/lib/Command/TransferLegacyRows.php +++ b/lib/Command/TransferLegacyRows.php @@ -123,16 +123,21 @@ protected function execute(InputInterface $input, OutputInterface $output): int /** * @param Table[] $tables + * @param OutputInterface $output * @return void */ private function transferDataForTables(array $tables, OutputInterface $output): void { $i = 1; foreach ($tables as $table) { + $output->writeln(''); $output->writeln("-- Start transfer for table " . $table->getId() . " (" . $table->getTitle() . ") [" . $i . "/" . count($tables) . "]"); try { $this->transferTable($table, $output); } catch (InternalError|PermissionError|Exception $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); + if ($output->isVerbose()) { + $output->writeln("❌ Error: " . $e->getMessage()); + } $output->writeln("⚠️ Could not transfer data. Continue with next table. The logs will have more information about the error."); } $i++; diff --git a/lib/Db/Row2Mapper.php b/lib/Db/Row2Mapper.php index 6d86e4f76..0782cf760 100644 --- a/lib/Db/Row2Mapper.php +++ b/lib/Db/Row2Mapper.php @@ -255,8 +255,7 @@ private function replacePlaceholderValues(array &$filters, string $userId): void private function getFilterGroups(IQueryBuilder &$qb, array $filters): array { $filterGroups = []; foreach ($filters as $filterGroup) { - $tmp = $this->getFilter($qb, $filterGroup); - $filterGroups[] = $qb->expr()->andX(...$tmp); + $filterGroups[] = $qb->expr()->andX(...$this->getFilter($qb, $filterGroup)); } return $filterGroups; } @@ -280,15 +279,6 @@ private function getFilter(IQueryBuilder &$qb, array $filterGroup): array { * @throws InternalError */ private function getFilterExpression(IQueryBuilder $qb, Column $column, string $operator, string $value): IQueryBuilder { - /*if($column->getType() === 'number' && $column->getNumberDecimals() === 0) { - $paramType = IQueryBuilder::PARAM_INT; - $value = (int)$value; - } elseif ($column->getType() === 'datetime') { - $paramType = IQueryBuilder::PARAM_DATE; - } else { - $paramType = IQueryBuilder::PARAM_STR; - }*/ - $paramType = $this->getColumnDbParamType($column); $value = $this->formatValue($column, $value, 'in'); diff --git a/lib/Migration/NewDbStructureRepairStep.php b/lib/Migration/NewDbStructureRepairStep.php index a7543dcb6..50b3b24fe 100644 --- a/lib/Migration/NewDbStructureRepairStep.php +++ b/lib/Migration/NewDbStructureRepairStep.php @@ -36,7 +36,7 @@ public function __construct(LoggerInterface $logger, TableService $tableService, /** * Returns the step's name */ - public function getName() { + public function getName(): string { return 'Copy the data into the new db structure'; } From 85f2e5bb7d5e9ca89f787f0929c2d768f44d20e7 Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Thu, 18 Jan 2024 14:12:42 +0100 Subject: [PATCH 64/73] add handling for view meta-column filtering Signed-off-by: Florian Steffens --- lib/Db/Row2Mapper.php | 100 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 96 insertions(+), 4 deletions(-) diff --git a/lib/Db/Row2Mapper.php b/lib/Db/Row2Mapper.php index 0782cf760..0179865a2 100644 --- a/lib/Db/Row2Mapper.php +++ b/lib/Db/Row2Mapper.php @@ -266,10 +266,28 @@ private function getFilterGroups(IQueryBuilder &$qb, array $filters): array { private function getFilter(IQueryBuilder &$qb, array $filterGroup): array { $filterExpressions = []; foreach ($filterGroup as $filter) { - $sql = $qb->expr()->in( - 'id', - $qb->createFunction($this->getFilterExpression($qb, $this->columns[$filter['columnId']], $filter['operator'], $filter['value'])->getSQL()) - ); + $columnId = $filter['columnId']; + + // if is normal column + if ($columnId >= 0) { + $sql = $qb->expr()->in( + 'id', + $qb->createFunction($this->getFilterExpression($qb, $this->columns[$filter['columnId']], $filter['operator'], $filter['value'])->getSQL()) + ); + + // if is meta data column + } elseif ($columnId < 0) { + $sql = $qb->expr()->in( + 'id', + $qb->createFunction($this->getMetaFilterExpression($qb, $columnId, $filter['operator'], $filter['value'])->getSQL()) + ); + + // if column id is unknown + } else { + $e = new Exception("Needed column (" . $filter['columnId'] . ") not found."); + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + } $filterExpressions[] = $sql; } return $filterExpressions; @@ -311,6 +329,74 @@ private function getFilterExpression(IQueryBuilder $qb, Column $column, string $ } } + /** + * + * -1 => 'number', ID + * -2 => 'text-line', Created + * -3 => 'datetime', At + * -4 => 'text-line', LastEdit + * -5 => 'datetime', At + * @throws InternalError + */ + private function getMetaFilterExpression(IQueryBuilder $qb, int $columnId, string $operator, string $value): IQueryBuilder { + $qb2 = $this->db->getQueryBuilder(); + $qb2->select('id'); + $qb2->from('tables_row_sleeves'); + + switch ($columnId) { + case -1: // row ID + $qb2->where($this->getSqlOperator($operator, $qb, 'id', $value, IQueryBuilder::PARAM_INT)); + break; + case -2: // created by + $qb2->where($this->getSqlOperator($operator, $qb, 'created_by', $value, IQueryBuilder::PARAM_STR)); + break; + case -3: // created at + $qb2->where($this->getSqlOperator($operator, $qb, 'created_at', $value, IQueryBuilder::PARAM_DATE)); + break; + case -4: // last edit by + $qb2->where($this->getSqlOperator($operator, $qb, 'last_edit_by', $value, IQueryBuilder::PARAM_STR)); + break; + case -5: // last edit at + $qb2->where($this->getSqlOperator($operator, $qb, 'last_edit_at', $value, IQueryBuilder::PARAM_DATE)); + break; + } + return $qb2; + } + + /** + * @param string $operator + * @param IQueryBuilder $qb + * @param string $column + * @param mixed $value + * @param mixed $paramType + * @return string + * @throws InternalError + */ + private function getSqlOperator(string $operator, IQueryBuilder $qb, string $columnName, $value, $paramType): string { + switch ($operator) { + case 'begins-with': + return $qb->expr()->like($columnName, $qb->createNamedParameter('%'.$this->db->escapeLikeParameter($value), $paramType)); + case 'ends-with': + return $qb->expr()->like($columnName, $qb->createNamedParameter($this->db->escapeLikeParameter($value).'%', $paramType)); + case 'contains': + return $qb->expr()->like($columnName, $qb->createNamedParameter('%'.$this->db->escapeLikeParameter($value).'%', $paramType)); + case 'is-equal': + return $qb->expr()->eq($columnName, $qb->createNamedParameter($value, $paramType)); + case 'is-greater-than': + return $qb->expr()->gt($columnName, $qb->createNamedParameter($value, $paramType)); + case 'is-greater-than-or-equal': + return $qb->expr()->gte($columnName, $qb->createNamedParameter($value, $paramType)); + case 'is-lower-than': + return $qb->expr()->lt($columnName, $qb->createNamedParameter($value, $paramType)); + case 'is-lower-than-or-equal': + return $qb->expr()->lte($columnName, $qb->createNamedParameter($value, $paramType)); + case 'is-empty': + return $qb->expr()->isNull($columnName); + default: + throw new InternalError('Operator '.$operator.' is not supported.'); + } + } + /** @noinspection DuplicatedCode */ private function resolveSearchValue(string $placeholder, string $userId): string { switch (ltrim($placeholder, '@')) { @@ -493,6 +579,12 @@ private function updateMetaData($entity, bool $setCreate = false, ?string $lastE * @throws InternalError */ private function insertCell(int $rowId, int $columnId, $value, ?string $lastEditAt = null, ?string $lastEditBy = null): void { + if (!isset($this->columns[$columnId])) { + $e = new Exception("Can not insert cell, because the given column-id is not known"); + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + } + $cellClassName = 'OCA\Tables\Db\RowCell'.ucfirst($this->columns[$columnId]->getType()); /** @var RowCellSuper $cell */ $cell = new $cellClassName(); From bead04e65533c82987e78a672b8d144d05d4932c Mon Sep 17 00:00:00 2001 From: Florian Date: Mon, 22 Jan 2024 10:42:45 +0100 Subject: [PATCH 65/73] Update lib/Migration/NewDbStructureRepairStep.php MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Julius Härtl Signed-off-by: Florian --- lib/Migration/NewDbStructureRepairStep.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Migration/NewDbStructureRepairStep.php b/lib/Migration/NewDbStructureRepairStep.php index 50b3b24fe..97b41bcd8 100644 --- a/lib/Migration/NewDbStructureRepairStep.php +++ b/lib/Migration/NewDbStructureRepairStep.php @@ -74,7 +74,7 @@ private function transferDataForTables(array $tables, IOutput $output) { $this->transferTable($table, $output); } catch (InternalError|PermissionError|Exception $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); - $output->warning("Could not transfer data. Continue with next table. The logs will have more information about the error."); + $output->warning("Could not transfer data. Continue with next table. The logs will have more information about the error: " . $e->getMessage()); } $i++; } From 41f21740237863f78bc70650747ec787a7826706 Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Mon, 22 Jan 2024 13:05:49 +0100 Subject: [PATCH 66/73] Check for column existence Signed-off-by: Florian Steffens --- lib/Db/LegacyRowMapper.php | 33 ++++++++++++++++++++++++++++++--- lib/Db/Row2Mapper.php | 12 ++++++------ 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/lib/Db/LegacyRowMapper.php b/lib/Db/LegacyRowMapper.php index a70b47e2e..c3a4a5db2 100644 --- a/lib/Db/LegacyRowMapper.php +++ b/lib/Db/LegacyRowMapper.php @@ -445,14 +445,15 @@ public function findByView(int $id, View $view): LegacyRow { * @throws InternalError */ public function transferLegacyRow(LegacyRow $legacyRow, array $columns) { - $this->rowMapper->insert($this->migrateLegacyRow($legacyRow), $columns); + $this->rowMapper->insert($this->migrateLegacyRow($legacyRow, $columns), $columns); } /** * @param LegacyRow $legacyRow + * @param Column[] $columns * @return Row2 */ - public function migrateLegacyRow(LegacyRow $legacyRow): Row2 { + public function migrateLegacyRow(LegacyRow $legacyRow, array $columns): Row2 { $row = new Row2(); $row->setId($legacyRow->getId()); $row->setTableId($legacyRow->getTableId()); @@ -460,7 +461,33 @@ public function migrateLegacyRow(LegacyRow $legacyRow): Row2 { $row->setCreatedAt($legacyRow->getCreatedAt()); $row->setLastEditBy($legacyRow->getLastEditBy()); $row->setLastEditAt($legacyRow->getLastEditAt()); - $row->setData($legacyRow->getDataArray()); + + $legacyData = $legacyRow->getDataArray(); + $data = []; + foreach ($legacyData as $legacyDatum) { + $columnId = $legacyDatum['columnId']; + if ($this->getColumnFromColumnsArray($columnId, $columns)) { + $data[] = $legacyDatum; + } else { + $this->logger->warning("The row with id " . $row->getId() . " has a value for the column with id " . $columnId . ". But this column does not exist or is not part of the table " . $row->getTableId() . ". Will ignore this value abd continue."); + } + } + $row->setData($data); + return $row; } + + /** + * @param int $columnId + * @param Column[] $columns + * @return Column|null + */ + private function getColumnFromColumnsArray(int $columnId, array $columns): ?Column { + foreach ($columns as $column) { + if($column->getId() === $columnId) { + return $column; + } + } + return null; + } } diff --git a/lib/Db/Row2Mapper.php b/lib/Db/Row2Mapper.php index 0179865a2..8ff51a29f 100644 --- a/lib/Db/Row2Mapper.php +++ b/lib/Db/Row2Mapper.php @@ -475,9 +475,6 @@ public function isRowInViewPresent(int $rowId, View $view, string $userId): bool * @throws Exception */ public function insert(Row2 $row, array $columns): Row2 { - if(!$columns) { - throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': columns are missing'); - } $this->setColumns($columns); if($row->getId()) { @@ -489,9 +486,12 @@ public function insert(Row2 $row, array $columns): Row2 { $row->setId($rowSleeve->getId()); } - // write all cells to its db-table - foreach ($row->getData() as $cell) { - $this->insertCell($rowSleeve->getId(), $cell['columnId'], $cell['value'], $rowSleeve->getLastEditAt(), $rowSleeve->getLastEditBy()); + // if the table/view has columns + if (count($columns) > 0) { + // write all cells to its db-table + foreach ($row->getData() as $cell) { + $this->insertCell($rowSleeve->getId(), $cell['columnId'], $cell['value'], $rowSleeve->getLastEditAt(), $rowSleeve->getLastEditBy()); + } } return $row; From ce72ade2045b37e6af9cd0a2170ab8a8b6bc5610 Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Mon, 22 Jan 2024 14:23:32 +0100 Subject: [PATCH 67/73] Adjust the unit tests Signed-off-by: Florian Steffens --- tests/unit/Service/LegacyRowMapperTest.php | 136 +++++++++++++-------- 1 file changed, 83 insertions(+), 53 deletions(-) diff --git a/tests/unit/Service/LegacyRowMapperTest.php b/tests/unit/Service/LegacyRowMapperTest.php index 62811f25b..e39d538dc 100644 --- a/tests/unit/Service/LegacyRowMapperTest.php +++ b/tests/unit/Service/LegacyRowMapperTest.php @@ -24,58 +24,88 @@ class LegacyRowMapperTest extends TestCase { * @throws NotFoundExceptionInterface */ public function testMigrateLegacyRow() { - $data = [ - // assume column is a text column - [ - 'columnId' => 1, - 'value' => 'one' - ], - // assume column is a number column - [ - 'columnId' => 2, - 'value' => 22.2 - ], - // assume column is a selection column - [ - 'columnId' => 3, - 'value' => 1 - ], - // assume columns are selection-check columns - [ - 'columnId' => 4, - 'value' => '"true"' - ], - [ - 'columnId' => 5, - 'value' => '"false"' - ], - // assume columns are selection-multi columns - [ - 'columnId' => 6, - 'value' => '[1]' - ], - [ - 'columnId' => 7, - 'value' => '[2,3]' - ], - [ - 'columnId' => 8, - 'value' => 'null' - ], - // assume columns are datetime columns - [ - 'columnId' => 9, - 'value' => '2023-12-24 10:00' - ], - [ - 'columnId' => 10, - 'value' => '2023-12-25' - ], - [ - 'columnId' => 11, - 'value' => '11:11' - ], - ]; + $data = []; + $columns = []; + + $data[] = ['columnId' => 1, 'value' => 'one']; + $col = new Column(); + $col->setId(1); + $col->setType('text'); + $col->setSubtype('line'); + $columns[] = $col; + + $data[] = ['columnId' => 2, 'value' => 22.2]; + $col = new Column(); + $col->setId(2); + $col->setType('number'); + $columns[] = $col; + + $data[] = ['columnId' => 3, 'value' => 1]; + $col = new Column(); + $col->setId(3); + $col->setType('selection'); + $columns[] = $col; + + $data[] = ['columnId' => 12, 'value' => '2']; + $col = new Column(); + $col->setId(12); + $col->setType('selection'); + $columns[] = $col; + + $data[] = ['columnId' => 4, 'value' => '"true"']; + $col = new Column(); + $col->setId(4); + $col->setType('selection'); + $col->setSubtype('check'); + $columns[] = $col; + + $data[] = ['columnId' => 5, 'value' => '"false"']; + $col = new Column(); + $col->setId(5); + $col->setType('selection'); + $col->setSubtype('check'); + $columns[] = $col; + + $data[] = ['columnId' => 6, 'value' => '[1]']; + $col = new Column(); + $col->setId(6); + $col->setType('selection'); + $col->setSubtype('multi'); + $columns[] = $col; + + $data[] = ['columnId' => 7, 'value' => '[2,3]']; + $col = new Column(); + $col->setId(7); + $col->setType('selection'); + $col->setSubtype('multi'); + $columns[] = $col; + + $data[] = ['columnId' => 8, 'value' => 'null']; + $col = new Column(); + $col->setId(8); + $col->setType('selection'); + $col->setSubtype('multi'); + $columns[] = $col; + + $data[] = ['columnId' => 9, 'value' => '2023-12-24 10:00']; + $col = new Column(); + $col->setId(9); + $col->setType('datetime'); + $columns[] = $col; + + $data[] = ['columnId' => 10, 'value' => '2023-12-25']; + $col = new Column(); + $col->setId(10); + $col->setType('datetime'); + $col->setSubtype('date'); + $columns[] = $col; + + $data[] = ['columnId' => 11, 'value' => '11:11']; + $col = new Column(); + $col->setId(11); + $col->setType('datetime'); + $col->setSubtype('time'); + $columns[] = $col; $dbConnection = Server::get(IDBConnection::class); $textColumnQb = $this->createMock(TextColumnQB::class); @@ -98,7 +128,7 @@ public function testMigrateLegacyRow() { $legacyRow->setLastEditBy('user1'); $legacyRow->setDataArray($data); - $row2 = $legacyRowMapper->migrateLegacyRow($legacyRow); + $row2 = $legacyRowMapper->migrateLegacyRow($legacyRow, $columns); $data2 = $row2->getData(); self::assertTrue($data === $data2); From 3c9d453db4b8a7f44405bfb4527b6a0a514834aa Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Tue, 23 Jan 2024 16:44:27 +0100 Subject: [PATCH 68/73] fix: replace placeholder for selection and check columns Signed-off-by: Florian Steffens fixup! fix: replace placeholder for selection and check columns --- lib/Db/Row2Mapper.php | 3 +++ lib/Db/RowCellSelectionMapper.php | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/lib/Db/Row2Mapper.php b/lib/Db/Row2Mapper.php index 8ff51a29f..6dc3ab993 100644 --- a/lib/Db/Row2Mapper.php +++ b/lib/Db/Row2Mapper.php @@ -399,6 +399,9 @@ private function getSqlOperator(string $operator, IQueryBuilder $qb, string $col /** @noinspection DuplicatedCode */ private function resolveSearchValue(string $placeholder, string $userId): string { + if (substr($placeholder, 0, 14) === '@selection-id-') { + return substr($placeholder, 14); + } switch (ltrim($placeholder, '@')) { case 'me': return $userId; case 'my-name': return $this->userHelper->getUserDisplayName($userId); diff --git a/lib/Db/RowCellSelectionMapper.php b/lib/Db/RowCellSelectionMapper.php index b74cd083b..66d71dff0 100644 --- a/lib/Db/RowCellSelectionMapper.php +++ b/lib/Db/RowCellSelectionMapper.php @@ -18,6 +18,14 @@ public function __construct(IDBConnection $db) { * @inheritDoc */ public function parseValueIncoming(Column $column, $value): string { + if ($column->getSubtype() === 'check') { + return json_encode(ltrim($value, '"')); + } + + if ($column->getSubtype() === '' || $column->getSubtype() === null) { + return $value ?? ''; + } + return json_encode($value); } From 89ca33de08e19b6b52d42e12ea68d37b5ad16ce3 Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Tue, 23 Jan 2024 16:49:27 +0100 Subject: [PATCH 69/73] Adjust no-delete option to delete option to avoid data lost Signed-off-by: Florian Steffens --- lib/Command/TransferLegacyRows.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/Command/TransferLegacyRows.php b/lib/Command/TransferLegacyRows.php index 0c12555e6..7c7ae50e0 100644 --- a/lib/Command/TransferLegacyRows.php +++ b/lib/Command/TransferLegacyRows.php @@ -71,10 +71,10 @@ protected function configure(): void { 'Transfer all table data.' ) ->addOption( - 'no-delete', + 'delete', null, InputOption::VALUE_OPTIONAL, - 'Set to not delete data from new db structure if any.' + 'Set to delete data from new db structure if any before transferring data.' ) ; } @@ -87,7 +87,7 @@ protected function configure(): void { protected function execute(InputInterface $input, OutputInterface $output): int { $tableIds = $input->getArgument('table-ids'); $optionAll = !!$input->getOption('all'); - $optionNoDelete = $input->getOption('no-delete') ?: null; + $optionDelete = $input->getOption('delete') ?: null; if ($optionAll) { $output->writeln("Look for tables"); @@ -113,7 +113,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $output->writeln("🤷🏻‍ Add at least one table id or add the option --all to transfer all tables."); return 2; } - if (!$optionNoDelete) { + if ($optionDelete) { $this->deleteDataForTables($tables, $output); } $this->transferDataForTables($tables, $output); From 57e42b70052860556ef5c33b1d310aeaec8c7ca4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Wed, 24 Jan 2024 08:46:46 +0100 Subject: [PATCH 70/73] fix: Print error during commands and catch more general errors during migration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- lib/Command/TransferLegacyRows.php | 2 +- lib/Migration/NewDbStructureRepairStep.php | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/Command/TransferLegacyRows.php b/lib/Command/TransferLegacyRows.php index 7c7ae50e0..6c76c77dd 100644 --- a/lib/Command/TransferLegacyRows.php +++ b/lib/Command/TransferLegacyRows.php @@ -138,7 +138,7 @@ private function transferDataForTables(array $tables, OutputInterface $output): if ($output->isVerbose()) { $output->writeln("❌ Error: " . $e->getMessage()); } - $output->writeln("⚠️ Could not transfer data. Continue with next table. The logs will have more information about the error."); + $output->writeln("⚠️ Could not transfer data. Continue with next table. The logs will have more information about the error: " . $e->getMessage()); } $i++; } diff --git a/lib/Migration/NewDbStructureRepairStep.php b/lib/Migration/NewDbStructureRepairStep.php index 97b41bcd8..ba1cf879f 100644 --- a/lib/Migration/NewDbStructureRepairStep.php +++ b/lib/Migration/NewDbStructureRepairStep.php @@ -14,6 +14,7 @@ use OCP\Migration\IOutput; use OCP\Migration\IRepairStep; use Psr\Log\LoggerInterface; +use Throwable; class NewDbStructureRepairStep implements IRepairStep { @@ -72,7 +73,7 @@ private function transferDataForTables(array $tables, IOutput $output) { $output->info("-- Start transfer for table " . $table->getId() . " (" . $table->getTitle() . ") [" . $i . "/" . count($tables) . "]"); try { $this->transferTable($table, $output); - } catch (InternalError|PermissionError|Exception $e) { + } catch (InternalError|PermissionError|Exception|Throwable $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); $output->warning("Could not transfer data. Continue with next table. The logs will have more information about the error: " . $e->getMessage()); } From 72b9ef39cc3adff1500dc1ef7e42ca20dcc8f572 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Wed, 24 Jan 2024 15:15:48 +0100 Subject: [PATCH 71/73] fix: use all rows for querying row candidates if a filter on a not-accessible row is used MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- lib/Service/RowService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Service/RowService.php b/lib/Service/RowService.php index a4f3c0a1e..a789cc075 100644 --- a/lib/Service/RowService.php +++ b/lib/Service/RowService.php @@ -513,7 +513,7 @@ public function getRowsCount(int $tableId): int { public function getViewRowsCount(View $view, string $userId): int { if ($this->permissionsService->canReadRowsByElementId($view->getId(), 'view', $userId)) { try { - return $this->row2Mapper->countRowsForView($view, $userId, $this->columnMapper->findMultiple($view->getColumnsArray())); + return $this->row2Mapper->countRowsForView($view, $userId, $this->columnMapper->findAllByTable($view->getTableId())); } catch (Exception $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); return 0; From 02a58d0902bca99c17dbc7502218688295842b7c Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Wed, 24 Jan 2024 11:19:05 +0100 Subject: [PATCH 72/73] enh: add filtering in views for multi selection Signed-off-by: Florian Steffens --- lib/Db/Row2Mapper.php | 15 ++++++++++++++- src/shared/components/ncTable/mixins/filter.js | 4 ++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/lib/Db/Row2Mapper.php b/lib/Db/Row2Mapper.php index 6dc3ab993..f8603ddee 100644 --- a/lib/Db/Row2Mapper.php +++ b/lib/Db/Row2Mapper.php @@ -311,8 +311,21 @@ private function getFilterExpression(IQueryBuilder $qb, Column $column, string $ case 'ends-with': return $qb2->andWhere($qb->expr()->like('value', $qb->createNamedParameter($this->db->escapeLikeParameter($value).'%', $paramType))); case 'contains': + if ($column->getType() === 'selection' && $column->getSubtype() === 'multi') { + $value = str_replace(['"', '\''], '', $value); + return $qb2->andWhere($qb2->expr()->orX( + $qb->expr()->like('value', $qb->createNamedParameter('['.$this->db->escapeLikeParameter($value).']')), + $qb->expr()->like('value', $qb->createNamedParameter('['.$this->db->escapeLikeParameter($value).',%')), + $qb->expr()->like('value', $qb->createNamedParameter('%,'.$this->db->escapeLikeParameter($value).']%')), + $qb->expr()->like('value', $qb->createNamedParameter('%,'.$this->db->escapeLikeParameter($value).',%')) + )); + } return $qb2->andWhere($qb->expr()->like('value', $qb->createNamedParameter('%'.$this->db->escapeLikeParameter($value).'%', $paramType))); case 'is-equal': + if ($column->getType() === 'selection' && $column->getSubtype() === 'multi') { + $value = str_replace(['"', '\''], '', $value); + return $qb2->andWhere($qb->expr()->eq('value', $qb->createNamedParameter('['.$this->db->escapeLikeParameter($value).']', $paramType))); + } return $qb2->andWhere($qb->expr()->eq('value', $qb->createNamedParameter($value, $paramType))); case 'is-greater-than': return $qb2->andWhere($qb->expr()->gt('value', $qb->createNamedParameter($value, $paramType))); @@ -366,7 +379,7 @@ private function getMetaFilterExpression(IQueryBuilder $qb, int $columnId, strin /** * @param string $operator * @param IQueryBuilder $qb - * @param string $column + * @param string $columnName * @param mixed $value * @param mixed $paramType * @return string diff --git a/src/shared/components/ncTable/mixins/filter.js b/src/shared/components/ncTable/mixins/filter.js index e65dc3206..34f796482 100644 --- a/src/shared/components/ncTable/mixins/filter.js +++ b/src/shared/components/ncTable/mixins/filter.js @@ -49,7 +49,7 @@ export const Filters = { Contains: new Filter({ id: FilterIds.Contains, label: t('tables', 'Contains'), - goodFor: [ColumnTypes.TextLine, ColumnTypes.TextLong, ColumnTypes.TextLink, ColumnTypes.TextRich], + goodFor: [ColumnTypes.TextLine, ColumnTypes.TextLong, ColumnTypes.TextLink, ColumnTypes.TextRich, ColumnTypes.SelectionMulti], incompatibleWith: [FilterIds.IsEmpty, FilterIds.IsEqual], }), BeginsWith: new Filter({ @@ -68,7 +68,7 @@ export const Filters = { id: FilterIds.IsEqual, label: t('tables', 'Is equal'), shortLabel: '=', - goodFor: [ColumnTypes.TextLine, ColumnTypes.Number, ColumnTypes.SelectionCheck, ColumnTypes.TextLink, ColumnTypes.NumberStars, ColumnTypes.NumberProgress, ColumnTypes.DatetimeDate, ColumnTypes.DatetimeTime, ColumnTypes.Datetime, ColumnTypes.Selection], + goodFor: [ColumnTypes.TextLine, ColumnTypes.Number, ColumnTypes.SelectionCheck, ColumnTypes.TextLink, ColumnTypes.NumberStars, ColumnTypes.NumberProgress, ColumnTypes.DatetimeDate, ColumnTypes.DatetimeTime, ColumnTypes.Datetime, ColumnTypes.Selection, ColumnTypes.SelectionMulti], incompatibleWith: [FilterIds.IsEmpty, FilterIds.IsEqual, FilterIds.BeginsWith, FilterIds.EndsWith, FilterIds.Contains, FilterIds.IsGreaterThan, FilterIds.IsGreaterThanOrEqual, FilterIds.IsLowerThan, FilterIds.IsLowerThanOrEqual], }), IsGreaterThan: new Filter({ From 55e9c29305727e56e1c359b5760ce989c60841da Mon Sep 17 00:00:00 2001 From: Florian Steffens Date: Thu, 25 Jan 2024 10:06:34 +0100 Subject: [PATCH 73/73] test: add end2end tests for views filtering for selection columns - add data-cy to form sections - add general commands to fill in data in forms for selection column - add test cases for all selection types - add basic combination test cases Signed-off-by: Florian Steffens fix: test urls Signed-off-by: Florian Steffens --- cypress/e2e/view-filtering-selection.cy.js | 371 +++++++++++++++++++++ cypress/support/commands.js | 18 + src/modules/modals/CreateRow.vue | 2 +- 3 files changed, 390 insertions(+), 1 deletion(-) create mode 100644 cypress/e2e/view-filtering-selection.cy.js diff --git a/cypress/e2e/view-filtering-selection.cy.js b/cypress/e2e/view-filtering-selection.cy.js new file mode 100644 index 000000000..4d5226ed6 --- /dev/null +++ b/cypress/e2e/view-filtering-selection.cy.js @@ -0,0 +1,371 @@ +let localUser + +describe('Filtering in a view by selection columns', () => { + + before(function() { + cy.createRandomUser().then(user => { + localUser = user + }) + }) + + beforeEach(function() { + cy.login(localUser) + cy.visit('apps/tables') + }) + + it('Setup table', () => { + cy.createTable('View filtering test table') + cy.createTextLineColumn('title', null, null, true) + cy.createSelectionColumn('selection', ['sel1', 'sel2', 'sel3', 'sel4'], null, false) + cy.createSelectionMultiColumn('multi selection', ['A', 'B', 'C', 'D'], null, false) + cy.createSelectionCheckColumn('check', null, false) + + // add row + cy.get('button').contains('Create row').click() + cy.fillInValueTextLine('title', 'first row') + cy.fillInValueSelection('selection', 'sel1') + cy.fillInValueSelectionMulti('multi selection', ['A', 'B']) + cy.fillInValueSelectionCheck('check') + cy.get('button').contains('Save').click() + + // add row + cy.get('button').contains('Create row').click() + cy.fillInValueTextLine('title', 'second row') + cy.fillInValueSelection('selection', 'sel2') + cy.fillInValueSelectionMulti('multi selection', ['B']) + cy.fillInValueSelectionCheck('check') + cy.get('button').contains('Save').click() + + // add row + cy.get('button').contains('Create row').click() + cy.fillInValueTextLine('title', 'third row') + cy.fillInValueSelection('selection', 'sel3') + cy.fillInValueSelectionMulti('multi selection', ['C', 'B', 'D']) + cy.get('button').contains('Save').click() + + // add row + cy.get('button').contains('Create row').click() + cy.fillInValueTextLine('title', 'fourth row') + cy.fillInValueSelectionMulti('multi selection', ['A']) + cy.fillInValueSelectionCheck('check') + cy.get('button').contains('Save').click() + + // add row + cy.get('button').contains('Create row').click() + cy.fillInValueTextLine('title', 'fifths row') + cy.fillInValueSelection('selection', 'sel4') + cy.fillInValueSelectionMulti('multi selection', ['D']) + cy.fillInValueSelectionCheck('check') + cy.get('button').contains('Save').click() + + // add row + cy.get('button').contains('Create row').click() + cy.fillInValueTextLine('title', 'sixths row') + cy.fillInValueSelection('selection', 'sel1') + cy.fillInValueSelectionMulti('multi selection', ['C', 'D']) + cy.fillInValueSelectionCheck('check') + cy.get('button').contains('Save').click() + + // add row + cy.get('button').contains('Create row').click() + cy.fillInValueTextLine('title', 'sevenths row') + cy.fillInValueSelection('selection', 'sel2') + cy.fillInValueSelectionMulti('multi selection', ['A', 'C', 'B', 'D']) + cy.get('button').contains('Save').click() + }) + + it('Filter view for single selection', () => { + cy.loadTable('View filtering test table') + + // # create view with filter + // ## create view and set title + const title = 'Filter for single selection' + cy.get('[data-cy="customTableAction"] button').click() + cy.get('.v-popper__popper li button span').contains('Create view').click({ force: true }) + cy.get('.modal-container #settings-section_title input').type(title) + + // ## add filter + cy.get('button').contains('Add new filter group').click() + cy.get('.modal-container .filter-group .v-select.select').eq(0).click() + cy.get('ul.vs__dropdown-menu li span[title="selection"]').click() + cy.get('.modal-container .filter-group .v-select.select').eq(1).click() + cy.get('ul.vs__dropdown-menu li span[title="Is equal"]').click() + cy.get('.modal-container .filter-group .v-select.select').eq(2).click() + cy.get('ul.vs__dropdown-menu li span[title="sel2"]').click() + + // ## save view + cy.intercept({ method: 'POST', url: '**/apps/tables/view' }).as('createView') + cy.intercept({ method: 'PUT', url: '**/apps/tables/view/*' }).as('updateView') + cy.contains('button', 'Create View').click() + cy.wait('@createView') + cy.wait('@updateView') + cy.contains('.app-navigation-entry-link span', title).should('exist') + + // # check for expected rows + // ## expected + let expected = ['sevenths row', 'second row'] + expected.forEach(item => { + cy.get('.custom-table table tr td div').contains(item).should('be.visible') + }) + + // ## not expected + let unexpected = ['first row', 'third row', 'fourth row', 'fifths row', 'sixths row'] + unexpected.forEach(item => { + cy.get('.custom-table table tr td div').contains(item).should('not.exist') + }) + + // # change filter value + // ## adjust filter + cy.get('[data-cy="customTableAction"] button').click() + cy.get('.v-popper__popper li button span').contains('Edit view').click({ force: true }) + cy.get('.modal-container .filter-group .v-select.select').eq(2).click() + cy.get('ul.vs__dropdown-menu li span[title="sel1"]').click() + + // ## update view + cy.intercept({ method: 'PUT', url: '**/apps/tables/view/*' }).as('updateView') + cy.contains('button', 'Save View').click() + cy.wait('@updateView') + + // # check for expected rows + // ## expected + expected = ['first row', 'sixths row'] + expected.forEach(item => { + cy.get('.custom-table table tr td div').contains(item).should('be.visible') + }) + + // ## not expected + unexpected = ['second row', 'third row', 'fourth row', 'fifths row', 'sevenths row'] + unexpected.forEach(item => { + cy.get('.custom-table table tr td div').contains(item).should('not.exist') + }) + }) + + it('Filter view for multi selection - equals', () => { + cy.loadTable('View filtering test table') + + // # create view with filter + // ## create view and set title + const title = 'Filter for multi selection 1' + cy.get('[data-cy="customTableAction"] button').click() + cy.get('.v-popper__popper li button span').contains('Create view').click({ force: true }) + cy.get('.modal-container #settings-section_title input').type(title) + + // ## add filter + cy.get('button').contains('Add new filter group').click() + cy.get('.modal-container .filter-group .v-select.select').eq(0).click() + cy.get('ul.vs__dropdown-menu li span[title="multi selection"]').click() + cy.get('.modal-container .filter-group .v-select.select').eq(1).click() + cy.get('ul.vs__dropdown-menu li span[title="Is equal"]').click() + cy.get('.modal-container .filter-group .v-select.select').eq(2).click() + cy.get('ul.vs__dropdown-menu li span[title="A"]').click() + + // ## save view + cy.intercept({ method: 'POST', url: '**/apps/tables/view' }).as('createView') + cy.intercept({ method: 'PUT', url: '**/apps/tables/view/*' }).as('updateView') + cy.contains('button', 'Create View').click() + cy.wait('@createView') + cy.wait('@updateView') + cy.contains('.app-navigation-entry-link span', title).should('exist') + + // # check for expected rows + // ## expected + const expected = ['fourth row'] + expected.forEach(item => { + cy.get('.custom-table table tr td div').contains(item).should('be.visible') + }) + + // ## not expected + const unexpected = ['first row', 'second row', 'third row', 'fifths row', 'sixths row', 'sevenths row'] + unexpected.forEach(item => { + cy.get('.custom-table table tr td div').contains(item).should('not.exist') + }) + }) + + it('Filter view for multi selection - contains', () => { + cy.loadTable('View filtering test table') + + // # create view with filter + // ## create view and set title + const title = 'Filter for multi selection 2' + cy.get('[data-cy="customTableAction"] button').click() + cy.get('.v-popper__popper li button span').contains('Create view').click({ force: true }) + cy.get('.modal-container #settings-section_title input').type(title) + + // ## add filter + cy.get('button').contains('Add new filter group').click() + cy.get('.modal-container .filter-group .v-select.select').eq(0).click() + cy.get('ul.vs__dropdown-menu li span[title="multi selection"]').click() + cy.get('.modal-container .filter-group .v-select.select').eq(1).click() + cy.get('ul.vs__dropdown-menu li span[title="Contains"]').click() + cy.get('.modal-container .filter-group .v-select.select').eq(2).click() + cy.get('ul.vs__dropdown-menu li span[title="A"]').click() + + // ## save view + cy.intercept({ method: 'POST', url: '**/apps/tables/view' }).as('createView') + cy.intercept({ method: 'PUT', url: '**/apps/tables/view/*' }).as('updateView') + cy.contains('button', 'Create View').click() + cy.wait('@createView') + cy.wait('@updateView') + cy.contains('.app-navigation-entry-link span', title).should('exist') + + // # check for expected rows + // ## expected + const expected = ['first row', 'fourth row', 'sevenths row'] + expected.forEach(item => { + cy.get('.custom-table table tr td div').contains(item).should('be.visible') + }) + + // ## not expected + const unexpected = ['second row', 'third row', 'fifths row', 'sixths row'] + unexpected.forEach(item => { + cy.get('.custom-table table tr td div').contains(item).should('not.exist') + }) + }) + + it('Filter view for multi selection - multiple contains', () => { + cy.loadTable('View filtering test table') + + // # create view with filter + // ## create view and set title + const title = 'Filter for multi selection 3' + cy.get('[data-cy="customTableAction"] button').click() + cy.get('.v-popper__popper li button span').contains('Create view').click({ force: true }) + cy.get('.modal-container #settings-section_title input').type(title) + + // ## add filter + cy.get('button').contains('Add new filter group').click() + cy.get('.modal-container .filter-group .v-select.select').eq(0).click() + cy.get('ul.vs__dropdown-menu li span[title="multi selection"]').click() + cy.get('.modal-container .filter-group .v-select.select').eq(1).click() + cy.get('ul.vs__dropdown-menu li span[title="Contains"]').click() + cy.get('.modal-container .filter-group .v-select.select').eq(2).click() + cy.get('ul.vs__dropdown-menu li span[title="A"]').click() + + cy.get('button').contains('Add new filter').click() + cy.get('.modal-container .filter-group .v-select.select').eq(3).click() + cy.get('ul.vs__dropdown-menu li span[title="multi selection"]').click() + cy.get('.modal-container .filter-group .v-select.select').eq(4).click() + cy.get('ul.vs__dropdown-menu li span[title="Contains"]').click() + cy.get('.modal-container .filter-group .v-select.select').eq(5).click() + cy.get('ul.vs__dropdown-menu li span[title="B"]').click() + + // ## save view + cy.intercept({ method: 'POST', url: '**/apps/tables/view' }).as('createView') + cy.intercept({ method: 'PUT', url: '**/apps/tables/view/*' }).as('updateView') + cy.contains('button', 'Create View').click() + cy.wait('@createView') + cy.wait('@updateView') + cy.contains('.app-navigation-entry-link span', title).should('exist') + + // # check for expected rows + // ## expected + const expected = ['first row', 'sevenths row'] + expected.forEach(item => { + cy.get('.custom-table table tr td div').contains(item).should('be.visible') + }) + + // ## not expected + const unexpected = ['second row', 'third row', 'fourth row', 'fifths row', 'sixths row'] + unexpected.forEach(item => { + cy.get('.custom-table table tr td div').contains(item).should('not.exist') + }) + }) + + it('Filter view for multi selection - multiple filter groups', () => { + cy.loadTable('View filtering test table') + + // # create view with filter + // ## create view and set title + const title = 'Filter for multi selection 3' + cy.get('[data-cy="customTableAction"] button').click() + cy.get('.v-popper__popper li button span').contains('Create view').click({ force: true }) + cy.get('.modal-container #settings-section_title input').type(title) + + // ## add filter + cy.get('button').contains('Add new filter group').click() + cy.get('.modal-container .filter-group .v-select.select').eq(0).click() + cy.get('ul.vs__dropdown-menu li span[title="multi selection"]').click() + cy.get('.modal-container .filter-group .v-select.select').eq(1).click() + cy.get('ul.vs__dropdown-menu li span[title="Contains"]').click() + cy.get('.modal-container .filter-group .v-select.select').eq(2).click() + cy.get('ul.vs__dropdown-menu li span[title="A"]').click() + + cy.get('button').contains('Add new filter').click() + cy.get('.modal-container .filter-group .v-select.select').eq(3).click() + cy.get('ul.vs__dropdown-menu li span[title="multi selection"]').click() + cy.get('.modal-container .filter-group .v-select.select').eq(4).click() + cy.get('ul.vs__dropdown-menu li span[title="Contains"]').click() + cy.get('.modal-container .filter-group .v-select.select').eq(5).click() + cy.get('ul.vs__dropdown-menu li span[title="B"]').click() + + cy.get('button').contains('Add new filter group').click() + cy.get('.modal-container .filter-group .v-select.select').eq(6).click() + cy.get('ul.vs__dropdown-menu li span[title="multi selection"]').click() + cy.get('.modal-container .filter-group .v-select.select').eq(7).click() + cy.get('ul.vs__dropdown-menu li span[title="Contains"]').click() + cy.get('.modal-container .filter-group .v-select.select').eq(8).click() + cy.get('ul.vs__dropdown-menu li span[title="D"]').click() + + // ## save view + cy.intercept({ method: 'POST', url: '**/apps/tables/view' }).as('createView') + cy.intercept({ method: 'PUT', url: '**/apps/tables/view/*' }).as('updateView') + cy.contains('button', 'Create View').click() + cy.wait('@createView') + cy.wait('@updateView') + cy.contains('.app-navigation-entry-link span', title).should('exist') + + // # check for expected rows + // ## expected + const expected = ['first row', 'third row', 'fifths row', 'sixths row', 'sevenths row'] + expected.forEach(item => { + cy.get('.custom-table table tr td div').contains(item).should('be.visible') + }) + + // ## not expected + const unexpected = ['second row', 'fourths row'] + unexpected.forEach(item => { + cy.get('.custom-table table tr td div').contains(item).should('not.exist') + }) + }) + + it('Filter view for selection check', () => { + cy.loadTable('View filtering test table') + + // # create view with filter + // ## create view and set title + const title = 'Filter for check selection' + cy.get('[data-cy="customTableAction"] button').click() + cy.get('.v-popper__popper li button span').contains('Create view').click({ force: true }) + cy.get('.modal-container #settings-section_title input').type(title) + + // ## add filter + cy.get('button').contains('Add new filter group').click() + cy.get('.modal-container .filter-group .v-select.select').eq(0).click() + cy.get('ul.vs__dropdown-menu li span[title="check"]').click() + cy.get('.modal-container .filter-group .v-select.select').eq(1).click() + cy.get('ul.vs__dropdown-menu li span[title="Is equal"]').click() + cy.get('.modal-container .filter-group .v-select.select').eq(2).click() + cy.get('ul.vs__dropdown-menu li span[title="Checked"]').click() + + // ## save view + cy.intercept({ method: 'POST', url: '**/apps/tables/view' }).as('createView') + cy.intercept({ method: 'PUT', url: '**/apps/tables/view/*' }).as('updateView') + cy.contains('button', 'Create View').click() + cy.wait('@createView') + cy.wait('@updateView') + cy.contains('.app-navigation-entry-link span', title).should('exist') + + // # check for expected rows + // ## expected + const expected = ['first row', 'second row', 'fourth row', 'fifths row', 'sixths row'] + expected.forEach(item => { + cy.get('.custom-table table tr td div').contains(item).should('be.visible') + }) + + // ## not expected + const unexpected = ['third row', 'sevenths row'] + unexpected.forEach(item => { + cy.get('.custom-table table tr td div').contains(item).should('not.exist') + }) + }) +}) diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 3d05189da..94b67ed02 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -422,3 +422,21 @@ Cypress.Commands.add('removeColumn', (title) => { cy.get('.v-popper__popper ul.nc-button-group-content').last().get('button').last().click() cy.get('.modal__content button').contains('Confirm').click() }) + +// fill in a value in the 'create row' or 'edit row' model +Cypress.Commands.add('fillInValueTextLine', (columnTitle, value) => { + cy.get('.modal__content [data-cy="' + columnTitle + '"] .slot input').type(value) +}) +Cypress.Commands.add('fillInValueSelection', (columnTitle, optionLabel) => { + cy.get('.modal__content [data-cy="' + columnTitle + '"] .slot input').click() + cy.get('ul.vs__dropdown-menu li span[title="' + optionLabel + '"]').click() +}) +Cypress.Commands.add('fillInValueSelectionMulti', (columnTitle, optionLabels) => { + optionLabels.forEach(item => { + cy.get('.modal__content [data-cy="' + columnTitle + '"] .slot input').click() + cy.get('ul.vs__dropdown-menu li span[title="' + item + '"]').click() + }) +}) +Cypress.Commands.add('fillInValueSelectionCheck', (columnTitle) => { + cy.get('.modal__content [data-cy="' + columnTitle + '"] .checkbox-radio-switch__label').click() +}) diff --git a/src/modules/modals/CreateRow.vue b/src/modules/modals/CreateRow.vue index c753dcf3c..b934756e4 100644 --- a/src/modules/modals/CreateRow.vue +++ b/src/modules/modals/CreateRow.vue @@ -8,7 +8,7 @@ -
+