From 77783f4552e30e691eccb292c327e715b0067e4b Mon Sep 17 00:00:00 2001 From: jessevz Date: Thu, 11 Dec 2025 17:33:31 +0100 Subject: [PATCH 1/9] Made it possible to sort on 1 to 1 relationship attributes --- .../apiv2/common/AbstractBaseAPI.class.php | 56 +++++++++++++++++-- .../apiv2/common/AbstractModelAPI.class.php | 40 ++++++------- 2 files changed, 69 insertions(+), 27 deletions(-) diff --git a/src/inc/apiv2/common/AbstractBaseAPI.class.php b/src/inc/apiv2/common/AbstractBaseAPI.class.php index 5499615db..f773675bc 100644 --- a/src/inc/apiv2/common/AbstractBaseAPI.class.php +++ b/src/inc/apiv2/common/AbstractBaseAPI.class.php @@ -144,6 +144,16 @@ protected function getFeatures(): array { } return $features; } + + /** + * Get features based on DBA model features + * + * @param string $dbaClass is the dba class to get the features from + */ + //TODO doesnt retrieve features based on form fields, could be done by adding api class in relationship objects + final protected function getFeaturesOther(string $dbaClass): array { + return call_user_func($dbaClass . '::getFeatures'); + } protected function getUpdateHandlers($id, $current_user): array { return []; @@ -174,6 +184,11 @@ public function getAliasedFeatures(): array { $features = $this->getFeatures(); return $this->mapFeatures($features); } + + public function getAliasedFeaturesOther($dbaclass): array { + $features = $this->getFeaturesOther($dbaclass); + return $this->mapFeatures($features); + } final protected function mapFeatures($features): array { $mappedFeatures = []; @@ -183,6 +198,18 @@ final protected function mapFeatures($features): array { } return $mappedFeatures; } + + public static function getToOneRelationships(): array { + return []; + } + + public static function getToManyRelationships(): array { + return []; + } + + public function getAllRelationships(): array { + return array_merge($this->getToOneRelationships(), $this->getToManyRelationships()); + } /** * Retrieve currently logged-in user @@ -1145,19 +1172,38 @@ protected function makeOrderFilterTemplates(Request $request, array $features, s $orderings = $this->getQueryParameterAsList($request, 'sort'); $contains_primary_key = false; foreach ($orderings as $order) { - if (preg_match('/^(?P[-])?(?P[_a-zA-Z]+)$/', $order, $matches)) { + $factory = null; + $joinKey = null; + $features_sort = $features; + if (preg_match('/^(?P[-])?(?P[_a-zA-Z.]+)$/', $order, $matches)) { // Special filtering of _id to use for uniform access to model primary key $cast_key = $matches['key'] == 'id' ? $this->getPrimaryKey() : $matches['key']; if ($cast_key == $this->getPrimaryKey()) { $contains_primary_key = true; } - if (array_key_exists($cast_key, $features)) { - $remappedKey = $features[$cast_key]['dbname']; + if (strpos($cast_key, ".")) { + $parts = explode(".", $cast_key); + if (count($parts) == 2) { // Only relations of 1 deep allowed ex. task.keyspace + $relationString = $parts[0]; + //currently getting all relationships, but its probably only possible to sort on 1 to 1 relations + $relations = $this->getAllRelationships(); + if (array_key_exists($relationString, $relations)) { + $relationClass = $relations[$relationString]['relationType']; + $relationFeatures = $this->getAliasedFeaturesOther($relationClass); + $factory = $this->getModelFactory($relationClass); + $joinKey = $relations[$relationString]['relationKey']; + $features_sort = $relationFeatures; + $cast_key = $parts[1]; + } + } + } + if (array_key_exists($cast_key, $features_sort)) { + $remappedKey = $features_sort[$cast_key]['dbname']; $type = ($matches['operator'] == '-') ? "DESC" : "ASC"; if ($reverseSort) { $type = ($type == "ASC") ? "DESC" : "ASC"; } - $orderTemplates[] = ['by' => $remappedKey, 'type' => $type]; + $orderTemplates[] = ['by' => $remappedKey, 'type' => $type, 'factory' => $factory, 'joinKey' => $joinKey]; } else { throw new HttpForbidden("Ordering parameter '" . $order . "' is not valid"); @@ -1170,7 +1216,7 @@ protected function makeOrderFilterTemplates(Request $request, array $features, s //when no primary key has been added in the sort parameter, add the default case of sorting on primary key as last sort if (!$contains_primary_key) { - $orderTemplates[] = ['by' => $this->getPrimaryKey(), 'type' => $defaultSort]; + $orderTemplates[] = ['by' => $this->getPrimaryKey(), 'type' => $defaultSort, 'factory' => null, 'joinKey' => null]; } return $orderTemplates; diff --git a/src/inc/apiv2/common/AbstractModelAPI.class.php b/src/inc/apiv2/common/AbstractModelAPI.class.php index 1126d03a5..293a10802 100644 --- a/src/inc/apiv2/common/AbstractModelAPI.class.php +++ b/src/inc/apiv2/common/AbstractModelAPI.class.php @@ -23,14 +23,6 @@ abstract protected function createObject(array $data): int; abstract protected function deleteObject(object $object): void; - public static function getToOneRelationships(): array { - return []; - } - - public static function getToManyRelationships(): array { - return []; - } - /** * Available 'expand' parameters on $object */ @@ -126,16 +118,6 @@ public function getFeaturesWithoutFormfields(): array { return $this->mapFeatures($features); } - /** - * Get features based on DBA model features - * - * @param string $dbaClass is the dba class to get the features from - */ - //TODO doesnt retrieve features based on form fields, could be done by adding api class in relationship objects - final protected function getFeaturesOther(string $dbaClass): array { - return call_user_func($dbaClass . '::getFeatures'); - } - /** * Find primary key for another DBA object * A little bit hacky because the getPrimaryKey function in dbaClass is not static @@ -547,16 +529,23 @@ protected static function compare_keys($key1, $key2, $isNegativeSort) { protected static function getMinMaxCursor($apiClass, string $sort, array $filters, $request, $aliasedfeatures) { $filters[Factory::LIMIT] = new LimitFilter(1); - + $primaryKey = $apiClass->getPrimaryKey(); // Descending queries are used to retrieve the last element. For this all sorts have to be reversed, since // if all order quereis are reversed and limit to 1, you will retrieve the last element. $reverseSort = ($sort == "DESC") ? true : false; $orderTemplates = $apiClass->makeOrderFilterTemplates($request, $aliasedfeatures, $sort, $reverseSort); $orderFilters = []; + $joinFilters = []; foreach ($orderTemplates as $orderTemplate) { - $orderFilters[] = new OrderFilter($orderTemplate['by'], $orderTemplate['type']); + $orderFilters[] = new OrderFilter($orderTemplate['by'], $orderTemplate['type'], $orderTemplate['factory']); + if ($orderTemplate['factory'] !== null){ + // if factory of ordertemplate is not null, sort is happenning on joined table + $otherFactory = $orderTemplate['factory']; + $joinFilters[] = new JoinFilter($otherFactory, $orderTemplate['joinKey'], $apiClass->getPrimaryKeyOther($otherFactory->getNullObject()::class)); + } } $filters[Factory::ORDER] = $orderFilters; + $filters[Factory::JOIN] = $joinFilters; $factory = $apiClass->getFactory(); $result = $factory->filter($filters); //handle joined queries @@ -661,23 +650,30 @@ public static function getManyResources(object $apiClass, Request $request, Resp } else { $defaultSort = "ASC"; } + $primaryKey = $apiClass->getPrimaryKey(); $orderTemplates = $apiClass->makeOrderFilterTemplates($request, $aliasedfeatures, $defaultSort); $orderTemplates[0]["type"] = $defaultSort; $primaryFilter = $orderTemplates[0]['by']; $orderFilters = []; + $joinFilters = []; // Build actual order filters foreach ($orderTemplates as $orderTemplate) { // $aFs[Factory::ORDER][] = new OrderFilter($orderTemplate['by'], $orderTemplate['type']); - $orderFilters[] = new OrderFilter($orderTemplate['by'], $orderTemplate['type']); + $orderFilters[] = new OrderFilter($orderTemplate['by'], $orderTemplate['type'], $orderTemplate['factory']); + if ($orderTemplate['factory'] !== null) { + // if factory of ordertemplate is not null, sort is happenning on joined table + $otherFactory = $orderTemplate['factory']; + $joinFilters[] = new JoinFilter($otherFactory, $orderTemplate['joinKey'], $apiClass->getPrimaryKeyOther($otherFactory->getNullObject()::class)); + } } $aFs[Factory::ORDER] = $orderFilters; + $aFs[Factory::JOIN] = $joinFilters; /* Include relation filters */ $finalFs = array_merge($aFs, $relationFs); - $primaryKey = $apiClass->getPrimaryKey(); //TODO it would be even better if its possible to see if the primary filter is unique, instead of primary key. //But this probably needs to be added in getFeatures() then. $primaryKeyIsNotPrimaryFilter = $primaryFilter != $primaryKey; From ed8c2fe50acfb1dfe24e98162657dd3c53d786da Mon Sep 17 00:00:00 2001 From: jessevz Date: Mon, 15 Dec 2025 15:16:59 +0100 Subject: [PATCH 2/9] Added the possibility to do left, right and outer joins to the ORM, and filtering on joins --- src/dba/AbstractModelFactory.class.php | 13 ++++++++--- src/dba/Join.class.php | 10 ++++++++ src/dba/JoinFilter.class.php | 32 +++++++++++++++++++++++++- 3 files changed, 51 insertions(+), 4 deletions(-) diff --git a/src/dba/AbstractModelFactory.class.php b/src/dba/AbstractModelFactory.class.php index c17de0893..044521f5c 100755 --- a/src/dba/AbstractModelFactory.class.php +++ b/src/dba/AbstractModelFactory.class.php @@ -562,6 +562,7 @@ public function getFromDB($pk): ?AbstractModel { * @return AbstractModel[]|AbstractModel Returns a list of matching objects or Null */ private function filterWithJoin(array $options): array|AbstractModel { + $vals = array(); $joins = $this->getJoins($options); $factories = array($this); $query = "SELECT " . Util::createPrefixedString($this->getMappedModelTable(), self::getMappedModelKeys($this->getNullObject())); @@ -580,11 +581,14 @@ private function filterWithJoin(array $options): array|AbstractModel { } $match1 = self::getMappedModelKey($localFactory->getNullObject(), $join->getMatch1()); $match2 = self::getMappedModelKey($joinFactory->getNullObject(), $join->getMatch2()); - $query .= " INNER JOIN " . $joinFactory->getMappedModelTable() . " ON " . $localFactory->getMappedModelTable() . "." . $match1 . "=" . $joinFactory->getMappedModelTable() . "." . $match2 . " "; + $query .= " " . $join->getJoinType() . " JOIN " . $joinFactory->getMappedModelTable() . " ON " . $localFactory->getMappedModelTable() . "." . $match1 . "=" . $joinFactory->getMappedModelTable() . "." . $match2 . " "; + $joinQueryFilters = $join->getQueryFilters(); + if (count($joinQueryFilters) > 0) { + $query .= $this->applyFilters($vals, $joinQueryFilters, true) ; + } } // Apply all normal filter to this query - $vals = array(); if (array_key_exists("filter", $options)) { $query .= $this->applyFilters($vals, $options['filter']); } @@ -709,7 +713,7 @@ public function filter(array $options, bool $single = false) { * @param $filters Filter|Filter[] * @return string */ - private function applyFilters(&$vals, Filter|array $filters): string { + private function applyFilters(&$vals, Filter|array $filters, bool $isJoinFilter = false): string { $parts = array(); if (!is_array($filters)) { $filters = array($filters); @@ -730,6 +734,9 @@ private function applyFilters(&$vals, Filter|array $filters): string { $vals[] = $v; } } + if ($isJoinFilter) { + return " AND " . implode(" AND ", $parts); + } return " WHERE " . implode(" AND ", $parts); } diff --git a/src/dba/Join.class.php b/src/dba/Join.class.php index 917095fc1..006496f8f 100644 --- a/src/dba/Join.class.php +++ b/src/dba/Join.class.php @@ -17,4 +17,14 @@ abstract function getMatch1(); * @return string */ abstract function getMatch2(); + + /** + * @return string + */ + abstract function getJoinType(); + + /** + * @return QueryFilter[] array of queryfilters that have to be perfromed on the join + */ + abstract function getQueryFilters(); } \ No newline at end of file diff --git a/src/dba/JoinFilter.class.php b/src/dba/JoinFilter.class.php index f23d80a78..8e85bbf9b 100755 --- a/src/dba/JoinFilter.class.php +++ b/src/dba/JoinFilter.class.php @@ -27,6 +27,16 @@ class JoinFilter extends Join { * @var AbstractModelFactory */ private $overrideOwnFactory; + + /** + * @var string + */ + private $joinType; + + /** + * @var QueryFilter[] array of queryfilters that have to be perfromed on the join + */ + private $queryFilters; /** * JoinFilter constructor. @@ -34,11 +44,14 @@ class JoinFilter extends Join { * @param $matching1 string * @param $matching2 string * @param $overrideOwnFactory AbstractModelFactory + * @param $joinType string is normally inner, left or right */ - function __construct($otherFactory, $matching1, $matching2, $overrideOwnFactory = null) { + function __construct($otherFactory, $matching1, $matching2, $overrideOwnFactory = null, $joinType = "inner", $queryFilters = []) { $this->otherFactory = $otherFactory; $this->match1 = $matching1; $this->match2 = $matching2; + $this->joinType = $joinType; + $this->queryFilters = $queryFilters; $this->otherTableName = $this->otherFactory->getMappedModelTable(); $this->overrideOwnFactory = $overrideOwnFactory; @@ -62,6 +75,23 @@ function getMatch2() { function getOtherTableName() { return $this->otherTableName; } + + function getJoinType() { + return $this->joinType; + } + + function setJoinType($joinType) { + return $this->joinType = $joinType; + } + + + function getQueryFilters() { + return $this->queryFilters; + } + + function setQueryFilters(array $queryFilters) { + $this->queryFilters = $queryFilters; + } /** * @return AbstractModelFactory From 330c35fffe6b92b46c5479cf7aeb13c2639f107b Mon Sep 17 00:00:00 2001 From: jessevz Date: Wed, 17 Dec 2025 13:25:28 +0100 Subject: [PATCH 3/9] Added to the orm a coalesce order filter which is needed to order on taskwrappername or taskname --- src/dba/CoalesceOrderFilter.class.php | 20 ++++++++++++++++++++ src/dba/OrderFilter.class.php | 8 ++++++++ src/dba/init.php | 1 + 3 files changed, 29 insertions(+) create mode 100644 src/dba/CoalesceOrderFilter.class.php diff --git a/src/dba/CoalesceOrderFilter.class.php b/src/dba/CoalesceOrderFilter.class.php new file mode 100644 index 000000000..3895c3346 --- /dev/null +++ b/src/dba/CoalesceOrderFilter.class.php @@ -0,0 +1,20 @@ +columns = $columns; + $this->type = $type; + } + + function getQueryString(AbstractModelFactory $factory, bool $includeTable = false): string { + return "COALESCE(" . implode(", ", $this->columns) . ") " . $this->type; + } +} + + diff --git a/src/dba/OrderFilter.class.php b/src/dba/OrderFilter.class.php index 04b168b58..7ea05b1c8 100755 --- a/src/dba/OrderFilter.class.php +++ b/src/dba/OrderFilter.class.php @@ -15,6 +15,14 @@ function __construct($by, $type, $overrideFactory = null) { $this->type = $type; $this->overrideFactory = $overrideFactory; } + + function getBy(): string { + return $this->by; + } + + function getType(): string { + return $this->type; + } function getQueryString(AbstractModelFactory $factory, bool $includeTable = false): string { if ($this->overrideFactory != null) { diff --git a/src/dba/init.php b/src/dba/init.php index 04a043954..c08691c6e 100644 --- a/src/dba/init.php +++ b/src/dba/init.php @@ -13,6 +13,7 @@ require_once(dirname(__FILE__) . "/Aggregation.class.php"); require_once(dirname(__FILE__) . "/Filter.class.php"); require_once(dirname(__FILE__) . "/Order.class.php"); +require_once(dirname(__FILE__) . "/CoalesceOrderFilter.class.php"); require_once(dirname(__FILE__) . "/Join.class.php"); require_once(dirname(__FILE__) . "/Group.class.php"); require_once(dirname(__FILE__) . "/Limit.class.php"); From 6a0e694e067545e47b24ab736be6c251e77787da Mon Sep 17 00:00:00 2001 From: jessevz Date: Wed, 17 Dec 2025 13:27:02 +0100 Subject: [PATCH 4/9] Added different types of joins to the abstractmodelfactory --- src/dba/AbstractModelFactory.class.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/dba/AbstractModelFactory.class.php b/src/dba/AbstractModelFactory.class.php index 044521f5c..90c046eae 100755 --- a/src/dba/AbstractModelFactory.class.php +++ b/src/dba/AbstractModelFactory.class.php @@ -609,7 +609,6 @@ private function filterWithJoin(array $options): array|AbstractModel { if (array_key_exists("limit", $options)) { $query .= $this->applyLimit($options['limit']); } - $dbh = self::getDB(); $stmt = $dbh->prepare($query); $stmt->execute($vals); @@ -765,7 +764,7 @@ private function applyJoins($joins): string { } $match1 = self::getMappedModelKey($localFactory->getNullObject(), $join->getMatch1()); $match2 = self::getMappedModelKey($joinFactory->getNullObject(), $join->getMatch2()); - $query .= " INNER JOIN " . $joinFactory->getMappedModelTable() . " ON " . $localFactory->getMappedModelTable() . "." . $match1 . "=" . $joinFactory->getMappedModelTable() . "." . $match2 . " "; + $query .= " " . $join->getJoinType() . " JOIN " . $joinFactory->getMappedModelTable() . " ON " . $localFactory->getMappedModelTable() . "." . $match1 . "=" . $joinFactory->getMappedModelTable() . "." . $match2 . " "; } return $query; } From 11aa815b77c6e649a8eec0ac65083cfe50554a29 Mon Sep 17 00:00:00 2001 From: jessevz Date: Wed, 17 Dec 2025 13:37:50 +0100 Subject: [PATCH 5/9] Added an additional way to parse the filters, needed for the taskwrappers to handle the unconventional relation between taskwrapper and task --- .../apiv2/common/AbstractModelAPI.class.php | 14 ++++++- src/inc/apiv2/model/taskwrappers.routes.php | 40 +++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/src/inc/apiv2/common/AbstractModelAPI.class.php b/src/inc/apiv2/common/AbstractModelAPI.class.php index 293a10802..f034f5733 100644 --- a/src/inc/apiv2/common/AbstractModelAPI.class.php +++ b/src/inc/apiv2/common/AbstractModelAPI.class.php @@ -428,7 +428,7 @@ final protected function ResourceRecordArrayToUpdateArray($data, $parentId): arr } return $updates; } - + protected static function calculate_next_cursor(string|int $cursor, bool $ascending=true) { if (is_int($cursor)) { if ($ascending) { @@ -539,7 +539,7 @@ protected static function getMinMaxCursor($apiClass, string $sort, array $filter foreach ($orderTemplates as $orderTemplate) { $orderFilters[] = new OrderFilter($orderTemplate['by'], $orderTemplate['type'], $orderTemplate['factory']); if ($orderTemplate['factory'] !== null){ - // if factory of ordertemplate is not null, sort is happenning on joined table + // if factory of ordertemplate is not null, sort is happening on joined table $otherFactory = $orderTemplate['factory']; $joinFilters[] = new JoinFilter($otherFactory, $orderTemplate['joinKey'], $apiClass->getPrimaryKeyOther($otherFactory->getNullObject()::class)); } @@ -558,6 +558,14 @@ protected static function getMinMaxCursor($apiClass, string $sort, array $filter return $result[0]; } + /** + * overiddable function to parse filters, currently only needed for taskWrapper endpoint + * to handle the taskwrapper -> task relation, to be able to treat it as a to one relationship + */ + protected function parseFilters(array $filters) { + return $filters; + } + /** * API entry point for requesting multiple objects * @throws HttpError @@ -668,11 +676,13 @@ public static function getManyResources(object $apiClass, Request $request, Resp $joinFilters[] = new JoinFilter($otherFactory, $orderTemplate['joinKey'], $apiClass->getPrimaryKeyOther($otherFactory->getNullObject()::class)); } } + $aFs[Factory::ORDER] = $orderFilters; $aFs[Factory::JOIN] = $joinFilters; /* Include relation filters */ $finalFs = array_merge($aFs, $relationFs); + $finalFs = $apiClass->parseFilters($finalFs); //TODO it would be even better if its possible to see if the primary filter is unique, instead of primary key. //But this probably needs to be added in getFeatures() then. diff --git a/src/inc/apiv2/model/taskwrappers.routes.php b/src/inc/apiv2/model/taskwrappers.routes.php index 32cc44816..9bf07a71e 100644 --- a/src/inc/apiv2/model/taskwrappers.routes.php +++ b/src/inc/apiv2/model/taskwrappers.routes.php @@ -1,11 +1,13 @@ HashType::class, 'relationKey' => HashType::HASH_TYPE_ID, ], + 'task' => [ + 'key' => TaskWrapper::TASK_WRAPPER_ID, + + 'relationType' => Task::class, + 'relationKey' => Task::TASK_WRAPPER_ID, + 'readonly' => true // Not allowed to change tasks of a taskwrapper + ], ]; } @@ -97,6 +106,37 @@ public static function getToManyRelationships(): array { ]; } + protected function parseFilters(array $filters) { + //This is in order to handle filters and sorting on columds + if (isset($filters[Factory::JOIN])) { + $joinFilters = $filters[Factory::JOIN]; + //TODO no correct, should check if it is a query for 1 to 1 'task' and not 1 to many 'tasks' + foreach ($joinFilters as $joinFilter) { + if ($joinFilter->getOtherTableName() == "Task") { + // This is a leftjoin where the task type is 0 which means not a supertask. This is in order to + // create a to 1 relationship where the taskwrapper will have the normal task as a relation and a supertaks will have null + // This way it becomes possible to filter or sort on the included single task. + $joinFilter->setJoinType("left"); + $qf = new QueryFilter(TaskWrapper::TASK_TYPE, "0", "="); + $joinFilter->setQueryFilters([$qf]); + } + } + + // parse the order and filter + // Because the frontend shows taskwrappername for supertasks and taskname for normaltasks, the orders and filters for the + // name needs to be changed to coalesce filters to get the correct value between these 2. + // Another possibilty where this hack is not needed would be to also store the taskname of normal tasks in the + // taskwrapper + foreach ($filters[Factory::ORDER] as &$orderfilter) { + if ($orderfilter->getBy() == Task::TASK_NAME) { + $newOrderFilter = new CoalesceOrderFilter([Task::TASK_NAME, TaskWrapper::TASK_WRAPPER_NAME], $orderfilter->gettype()); + $orderfilter = $newOrderFilter; + } + } + unset($orderfilter); + } + return $filters; + } #[NoReturn] protected function createObject(array $data): int { assert(False, "TaskWrappers cannot be created via API"); From 5147e420ae545057cff9dd44c08d42f0d767135d Mon Sep 17 00:00:00 2001 From: jessevz Date: Wed, 17 Dec 2025 14:08:48 +0100 Subject: [PATCH 6/9] Fixed copilot suggestions --- src/dba/CoalesceOrderFilter.class.php | 4 +--- src/inc/apiv2/common/AbstractModelAPI.class.php | 4 ++-- src/inc/apiv2/model/tasks.routes.php | 2 +- src/inc/apiv2/model/taskwrappers.routes.php | 11 +++++------ 4 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/dba/CoalesceOrderFilter.class.php b/src/dba/CoalesceOrderFilter.class.php index 3895c3346..c11b3b1cb 100644 --- a/src/dba/CoalesceOrderFilter.class.php +++ b/src/dba/CoalesceOrderFilter.class.php @@ -15,6 +15,4 @@ function __construct($columns, $type) { function getQueryString(AbstractModelFactory $factory, bool $includeTable = false): string { return "COALESCE(" . implode(", ", $this->columns) . ") " . $this->type; } -} - - +} \ No newline at end of file diff --git a/src/inc/apiv2/common/AbstractModelAPI.class.php b/src/inc/apiv2/common/AbstractModelAPI.class.php index f034f5733..a1eb488b8 100644 --- a/src/inc/apiv2/common/AbstractModelAPI.class.php +++ b/src/inc/apiv2/common/AbstractModelAPI.class.php @@ -559,7 +559,7 @@ protected static function getMinMaxCursor($apiClass, string $sort, array $filter } /** - * overiddable function to parse filters, currently only needed for taskWrapper endpoint + * overridable function to parse filters, currently only needed for taskWrapper endpoint * to handle the taskwrapper -> task relation, to be able to treat it as a to one relationship */ protected function parseFilters(array $filters) { @@ -671,7 +671,7 @@ public static function getManyResources(object $apiClass, Request $request, Resp // $aFs[Factory::ORDER][] = new OrderFilter($orderTemplate['by'], $orderTemplate['type']); $orderFilters[] = new OrderFilter($orderTemplate['by'], $orderTemplate['type'], $orderTemplate['factory']); if ($orderTemplate['factory'] !== null) { - // if factory of ordertemplate is not null, sort is happenning on joined table + // if factory of ordertemplate is not null, sort is happening on joined table $otherFactory = $orderTemplate['factory']; $joinFilters[] = new JoinFilter($otherFactory, $orderTemplate['joinKey'], $apiClass->getPrimaryKeyOther($otherFactory->getNullObject()::class)); } diff --git a/src/inc/apiv2/model/tasks.routes.php b/src/inc/apiv2/model/tasks.routes.php index 3d85543d5..7f3b8bee5 100644 --- a/src/inc/apiv2/model/tasks.routes.php +++ b/src/inc/apiv2/model/tasks.routes.php @@ -120,7 +120,7 @@ public static function getToManyRelationships(): array { ] ]; } - + public function getFormFields(): array { // TODO Form declarations in more generic class to allow auto-generated OpenAPI specifications return [ diff --git a/src/inc/apiv2/model/taskwrappers.routes.php b/src/inc/apiv2/model/taskwrappers.routes.php index 9bf07a71e..1444535c0 100644 --- a/src/inc/apiv2/model/taskwrappers.routes.php +++ b/src/inc/apiv2/model/taskwrappers.routes.php @@ -107,17 +107,16 @@ public static function getToManyRelationships(): array { } protected function parseFilters(array $filters) { - //This is in order to handle filters and sorting on columds + //This is in order to handle filters and sorting on columns if (isset($filters[Factory::JOIN])) { $joinFilters = $filters[Factory::JOIN]; - //TODO no correct, should check if it is a query for 1 to 1 'task' and not 1 to many 'tasks' foreach ($joinFilters as $joinFilter) { if ($joinFilter->getOtherTableName() == "Task") { // This is a leftjoin where the task type is 0 which means not a supertask. This is in order to - // create a to 1 relationship where the taskwrapper will have the normal task as a relation and a supertaks will have null + // create a to 1 relationship where the taskwrapper will have the normal task as a relation and a supertask will have null // This way it becomes possible to filter or sort on the included single task. $joinFilter->setJoinType("left"); - $qf = new QueryFilter(TaskWrapper::TASK_TYPE, "0", "="); + $qf = new QueryFilter(TaskWrapper::TASK_TYPE, DTaskTypes::NORMAL, "="); $joinFilter->setQueryFilters([$qf]); } } @@ -125,11 +124,11 @@ protected function parseFilters(array $filters) { // parse the order and filter // Because the frontend shows taskwrappername for supertasks and taskname for normaltasks, the orders and filters for the // name needs to be changed to coalesce filters to get the correct value between these 2. - // Another possibilty where this hack is not needed would be to also store the taskname of normal tasks in the + // Another possibility where this hack is not needed would be to also store the taskname of normal tasks in the // taskwrapper foreach ($filters[Factory::ORDER] as &$orderfilter) { if ($orderfilter->getBy() == Task::TASK_NAME) { - $newOrderFilter = new CoalesceOrderFilter([Task::TASK_NAME, TaskWrapper::TASK_WRAPPER_NAME], $orderfilter->gettype()); + $newOrderFilter = new CoalesceOrderFilter([Task::TASK_NAME, TaskWrapper::TASK_WRAPPER_NAME], $orderfilter->getType()); $orderfilter = $newOrderFilter; } } From de40a6a8a9b5d7704fdfac1d3096c86743e1be2e Mon Sep 17 00:00:00 2001 From: jessevz Date: Thu, 18 Dec 2025 09:16:52 +0100 Subject: [PATCH 7/9] Fixed bug in creating join filter --- src/inc/apiv2/common/AbstractBaseAPI.class.php | 3 ++- src/inc/apiv2/common/AbstractModelAPI.class.php | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/inc/apiv2/common/AbstractBaseAPI.class.php b/src/inc/apiv2/common/AbstractBaseAPI.class.php index f773675bc..b721d8d9c 100644 --- a/src/inc/apiv2/common/AbstractBaseAPI.class.php +++ b/src/inc/apiv2/common/AbstractBaseAPI.class.php @@ -1192,6 +1192,7 @@ protected function makeOrderFilterTemplates(Request $request, array $features, s $relationFeatures = $this->getAliasedFeaturesOther($relationClass); $factory = $this->getModelFactory($relationClass); $joinKey = $relations[$relationString]['relationKey']; + $key = $relations[$relationString]['key']; $features_sort = $relationFeatures; $cast_key = $parts[1]; } @@ -1203,7 +1204,7 @@ protected function makeOrderFilterTemplates(Request $request, array $features, s if ($reverseSort) { $type = ($type == "ASC") ? "DESC" : "ASC"; } - $orderTemplates[] = ['by' => $remappedKey, 'type' => $type, 'factory' => $factory, 'joinKey' => $joinKey]; + $orderTemplates[] = ['by' => $remappedKey, 'type' => $type, 'factory' => $factory, 'joinKey' => $joinKey, 'key' => $key]; } else { throw new HttpForbidden("Ordering parameter '" . $order . "' is not valid"); diff --git a/src/inc/apiv2/common/AbstractModelAPI.class.php b/src/inc/apiv2/common/AbstractModelAPI.class.php index a1eb488b8..9fac4fc2e 100644 --- a/src/inc/apiv2/common/AbstractModelAPI.class.php +++ b/src/inc/apiv2/common/AbstractModelAPI.class.php @@ -673,7 +673,7 @@ public static function getManyResources(object $apiClass, Request $request, Resp if ($orderTemplate['factory'] !== null) { // if factory of ordertemplate is not null, sort is happening on joined table $otherFactory = $orderTemplate['factory']; - $joinFilters[] = new JoinFilter($otherFactory, $orderTemplate['joinKey'], $apiClass->getPrimaryKeyOther($otherFactory->getNullObject()::class)); + $joinFilters[] = new JoinFilter($otherFactory, $orderTemplate['joinKey'], $orderTemplate['key']); } } From 7ed22bcc7bcecdce5153e7bc1dc364c9b6e5bdfb Mon Sep 17 00:00:00 2001 From: jessevz Date: Thu, 18 Dec 2025 16:23:12 +0100 Subject: [PATCH 8/9] Fixed copilot suggestions --- src/dba/JoinFilter.class.php | 2 +- src/inc/apiv2/common/AbstractBaseAPI.class.php | 4 ++-- src/inc/apiv2/model/taskwrappers.routes.php | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/dba/JoinFilter.class.php b/src/dba/JoinFilter.class.php index 8e85bbf9b..970cfca00 100755 --- a/src/dba/JoinFilter.class.php +++ b/src/dba/JoinFilter.class.php @@ -81,7 +81,7 @@ function getJoinType() { } function setJoinType($joinType) { - return $this->joinType = $joinType; + $this->joinType = $joinType; } diff --git a/src/inc/apiv2/common/AbstractBaseAPI.class.php b/src/inc/apiv2/common/AbstractBaseAPI.class.php index b721d8d9c..26e0848c2 100644 --- a/src/inc/apiv2/common/AbstractBaseAPI.class.php +++ b/src/inc/apiv2/common/AbstractBaseAPI.class.php @@ -185,8 +185,8 @@ public function getAliasedFeatures(): array { return $this->mapFeatures($features); } - public function getAliasedFeaturesOther($dbaclass): array { - $features = $this->getFeaturesOther($dbaclass); + public function getAliasedFeaturesOther($dbaClass): array { + $features = $this->getFeaturesOther($dbaClass); return $this->mapFeatures($features); } diff --git a/src/inc/apiv2/model/taskwrappers.routes.php b/src/inc/apiv2/model/taskwrappers.routes.php index 1444535c0..4c389cd47 100644 --- a/src/inc/apiv2/model/taskwrappers.routes.php +++ b/src/inc/apiv2/model/taskwrappers.routes.php @@ -111,11 +111,11 @@ protected function parseFilters(array $filters) { if (isset($filters[Factory::JOIN])) { $joinFilters = $filters[Factory::JOIN]; foreach ($joinFilters as $joinFilter) { - if ($joinFilter->getOtherTableName() == "Task") { + if ($joinFilter->getOtherTableName() == Task::class) { // This is a leftjoin where the task type is 0 which means not a supertask. This is in order to // create a to 1 relationship where the taskwrapper will have the normal task as a relation and a supertask will have null // This way it becomes possible to filter or sort on the included single task. - $joinFilter->setJoinType("left"); + $joinFilter->setJoinType("LEFT"); $qf = new QueryFilter(TaskWrapper::TASK_TYPE, DTaskTypes::NORMAL, "="); $joinFilter->setQueryFilters([$qf]); } From cce60684cd251eef1194fd98f76f46b2e10b6f40 Mon Sep 17 00:00:00 2001 From: Sein Coray Date: Tue, 6 Jan 2026 12:32:46 +0100 Subject: [PATCH 9/9] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/dba/Join.class.php | 2 +- src/dba/JoinFilter.class.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dba/Join.class.php b/src/dba/Join.class.php index 006496f8f..eb89d69be 100644 --- a/src/dba/Join.class.php +++ b/src/dba/Join.class.php @@ -24,7 +24,7 @@ abstract function getMatch2(); abstract function getJoinType(); /** - * @return QueryFilter[] array of queryfilters that have to be perfromed on the join + * @return QueryFilter[] array of queryfilters that have to be performed on the join */ abstract function getQueryFilters(); } \ No newline at end of file diff --git a/src/dba/JoinFilter.class.php b/src/dba/JoinFilter.class.php index 970cfca00..d6358fc85 100755 --- a/src/dba/JoinFilter.class.php +++ b/src/dba/JoinFilter.class.php @@ -34,7 +34,7 @@ class JoinFilter extends Join { private $joinType; /** - * @var QueryFilter[] array of queryfilters that have to be perfromed on the join + * @var QueryFilter[] array of queryfilters that have to be performed on the join */ private $queryFilters;