From 32317f5c88884d6dccc035772a5d577ae7d327a0 Mon Sep 17 00:00:00 2001 From: jessevz Date: Thu, 23 Jan 2025 15:58:27 +0100 Subject: [PATCH 1/2] Fixed duplicate entries in included section and including many to one through intermediate table --- .../apiv2/common/AbstractBaseAPI.class.php | 2 +- .../apiv2/common/AbstractModelAPI.class.php | 99 ++++++++++++++++++- src/inc/apiv2/model/tasks.routes.php | 6 ++ 3 files changed, 101 insertions(+), 6 deletions(-) diff --git a/src/inc/apiv2/common/AbstractBaseAPI.class.php b/src/inc/apiv2/common/AbstractBaseAPI.class.php index 7c355b438..2afa40cf3 100644 --- a/src/inc/apiv2/common/AbstractBaseAPI.class.php +++ b/src/inc/apiv2/common/AbstractBaseAPI.class.php @@ -872,7 +872,7 @@ protected function makeExpandables(Request $request, array $validExpandables): a protected function getPrimaryKey(): string { $features = $this->getFeatures(); - # Word-around required since getPrimaryKey is not static in dba/models/*.php + # Work-around required since getPrimaryKey is not static in dba/models/*.php foreach($features as $key => $value) { if ($value['pk'] == True) { return $key; diff --git a/src/inc/apiv2/common/AbstractModelAPI.class.php b/src/inc/apiv2/common/AbstractModelAPI.class.php index 608d55e2d..ef19f976b 100644 --- a/src/inc/apiv2/common/AbstractModelAPI.class.php +++ b/src/inc/apiv2/common/AbstractModelAPI.class.php @@ -58,6 +58,19 @@ protected static function fetchExpandObjects(array $objects, string $expand): mi $toOneRelationships = static::getToOneRelationships(); if (array_key_exists($expand, $toOneRelationships)) { $relationFactory = self::getModelFactory($toOneRelationships[$expand]['relationType']); + + if (array_key_exists('junctionTableType', $toOneRelationships[$expand])) { + $junctionTableFactory = self::getModelFactory($toOneRelationships[$expand]['junctionTableType']); + return self::getManyToOneRelationViaIntermediate( + $objects, + $toOneRelationships[$expand]['junctionTableJoinField'], + $junctionTableFactory, + $relationFactory, + $toOneRelationships[$expand]['relationKey'], + $toOneRelationships[$expand]['parentKey'] + ); + }; + return self::getForeignKeyRelation( $objects, $toOneRelationships[$expand]['key'], @@ -73,7 +86,7 @@ protected static function fetchExpandObjects(array $objects, string $expand): mi /* Associative entity */ if (array_key_exists('junctionTableType', $toManyRelationships[$expand])) { $junctionTableFactory = self::getModelFactory($toManyRelationships[$expand]['junctionTableType']); - return self::getManyToOneRelationViaIntermediate( + return self::getManyToManyRelationViaIntermediate( $objects, $toManyRelationships[$expand]['key'], $junctionTableFactory, @@ -148,6 +161,65 @@ protected function getPrimaryKeyOther(string $dbaClass): string } } + /** + * Retrieve ManyToOne relalation for $objects ('parents') of type $targetFactory via 'intermidate' + * of $intermediateFactory joining on $joinField (between 'intermediate' and 'target'). Filtered by + * $filterField at $intermediateFactory. + * + * @param array $objects Objects Fetch relation for selected Objects + * @param string $objectField Field to use as base for $objects + * @param object $intermediateFactory Factory used as intermediate between parentObject and targetObject + * @param string $filterField Filter field of intermadiateObject to filter against $objects field + * @param object $targetFactory Object properties of objects returned + * @param string $joinField Field to connect 'intermediate' to 'target' + + * @return array $many2One which is a map where the key is the id of the parent object and the value is an array of the included + * objects that are included for this parent object + */ + //A bit hacky solution to get a to one through an intermediate table, needed when from a task you need to include the hashlists + final protected static function getManyToOneRelationViaIntermediate( + array $objects, + string $objectField, + object $intermediateFactory, + object $targetFactory, + string $joinField, + string $parentKey + ): array { + assert($intermediateFactory instanceof AbstractModelFactory); + assert($targetFactory instanceof AbstractModelFactory); + $many2One = array(); + + /* Retrieve Parent -> Intermediate -> Target objects */ + $objectIds = []; + foreach($objects as $object) { + $kv = $object->getKeyValueDict(); + $objectIds[] = $kv[$objectField]; + } + $baseFactory = self::getModelFactory(static::getDBAClass()); + $qF = new ContainFilter($objectField, $objectIds, $intermediateFactory); + $jF = new JoinFilter($intermediateFactory, $joinField, $joinField); + $jF2 = new JoinFilter($baseFactory, $objectField, $objectField, $intermediateFactory); + $hO = $targetFactory->filter([Factory::FILTER => $qF, Factory::JOIN => [$jF, $jF2]]); + + $intermediateObjectList = $hO[$intermediateFactory->getModelName()]; + $targetObjectList = $hO[$targetFactory->getModelName()]; + $baseObjectList = $hO[$baseFactory->getModelName()]; + + $intermediateObject = current($intermediateObjectList); + $targetObject = current($targetObjectList); + $baseObject = current($baseObjectList); + + while ($intermediateObject && $targetObject && $baseObject) { + $kv = $baseObject->getKeyValueDict(); + $many2One[$kv[$parentKey]] = $targetObject; + + $intermediateObject = next($intermediateObjectList); + $targetObject = next($targetObjectList); + $baseObject = next($baseObjectList); + } + return $many2One; + } + /** * Retrieve ForeignKey Relation * @@ -243,9 +315,10 @@ final protected static function getManyToOneRelation( * @param object $targetFactory Object properties of objects returned * @param string $joinField Field to connect 'intermediate' to 'target' - * @return array + * @return array $many2many which is a map where the key is the id of the parent object and the value is an array of the included + * objects that are included for this parent object */ - final protected static function getManyToOneRelationViaIntermediate( + final protected static function getManyToManyRelationViaIntermediate( array $objects, string $objectField, object $intermediateFactory, @@ -372,6 +445,22 @@ final protected function ResourceRecordArrayToUpdateArray($data, $parentId) return $updates; } + protected static function addToRelatedResources(array $relatedResources, array $relatedResource) { + $alreadyExists = false; + $searchType = $relatedResource["type"]; + $searchId = $relatedResource["id"]; + foreach ($relatedResources as $resource) { + if ($resource["id"] == $searchId && $resource["type"] == $searchType) { + $alreadyExists = true; + break; + } + } + if (!$alreadyExists) { + $relatedResources[] = $relatedResource; + } + return $relatedResources; + } + /** * API entry point for requesting multiple objects */ @@ -484,14 +573,14 @@ public static function getManyResources(object $apiClass, Request $request, Resp $expandResultObject = $expandResult[$expand][$object->getId()]; if (is_array($expandResultObject)) { foreach ($expandResultObject as $expandObject) { - $includedResources[] = $apiClass->obj2Resource($expandObject); + $includedResources = self::addToRelatedResources($includedResources, $apiClass->obj2Resource($expandObject)); } } else { if ($expandResultObject === null) { // to-only relation which is nullable continue; } - $includedResources[] = $apiClass->obj2Resource($expandResultObject); + $includedResources = self::addToRelatedResources($includedResources, $apiClass->obj2Resource($expandResultObject)); } } } diff --git a/src/inc/apiv2/model/tasks.routes.php b/src/inc/apiv2/model/tasks.routes.php index 978a14dc1..dada4d77b 100644 --- a/src/inc/apiv2/model/tasks.routes.php +++ b/src/inc/apiv2/model/tasks.routes.php @@ -47,6 +47,12 @@ public static function getToOneRelationships(): array { 'intermediateType' => TaskWrapper::class, 'joinField' => Task::TASK_WRAPPER_ID, 'joinFieldRelation' => TaskWrapper::TASK_WRAPPER_ID, + + 'junctionTableType' => TaskWrapper::class, + 'junctionTableFilterField' => TaskWrapper::HASHLIST_ID, + 'junctionTableJoinField' => TaskWrapper::TASK_WRAPPER_ID, + + 'parentKey' => Task::TASK_ID ], ]; } From 8d090a476ca919d289b4217255f8bb3854dde530 Mon Sep 17 00:00:00 2001 From: jessevz Date: Thu, 23 Jan 2025 16:18:53 +0100 Subject: [PATCH 2/2] Fixed bug where user endpoint used wrong include code --- src/inc/apiv2/common/AbstractModelAPI.class.php | 3 ++- src/inc/apiv2/model/users.routes.php | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/inc/apiv2/common/AbstractModelAPI.class.php b/src/inc/apiv2/common/AbstractModelAPI.class.php index ef19f976b..9f3dc35c6 100644 --- a/src/inc/apiv2/common/AbstractModelAPI.class.php +++ b/src/inc/apiv2/common/AbstractModelAPI.class.php @@ -176,7 +176,8 @@ protected function getPrimaryKeyOther(string $dbaClass): string * @return array $many2One which is a map where the key is the id of the parent object and the value is an array of the included * objects that are included for this parent object */ - //A bit hacky solution to get a to one through an intermediate table, needed when from a task you need to include the hashlists + //A bit hacky solution to get a to one through an intermediate table, currently only used by tasks to include a hashlist through the taskwrapper + //another solution can be to overwrite fetchExpandObjects() in tasks.routes final protected static function getManyToOneRelationViaIntermediate( array $objects, string $objectField, diff --git a/src/inc/apiv2/model/users.routes.php b/src/inc/apiv2/model/users.routes.php index 2f92db5ef..492bfe3dc 100644 --- a/src/inc/apiv2/model/users.routes.php +++ b/src/inc/apiv2/model/users.routes.php @@ -55,7 +55,7 @@ protected static function fetchExpandObjects(array $objects, string $expand): mi /* Expand requested section */ switch($expand) { case 'accessGroups': - return self::getManyToOneRelationViaIntermediate( + return self::getManyToManyRelationViaIntermediate( $objects, User::USER_ID, Factory::getAccessGroupUserFactory(),