diff --git a/API_documentation/v3/api.yaml b/API_documentation/v3/api.yaml index b5eb563fba..dfc4a8e5ae 100644 --- a/API_documentation/v3/api.yaml +++ b/API_documentation/v3/api.yaml @@ -118,3 +118,46 @@ paths: description: Record not found default: $ref: 'responses.yaml#/error_response' + '/stories/{sbas_id}/{record_id}/children': + get: + tags: + - story + summary: Returns uri of each record (child) contained in the story ; Optional pagination + description: Returns children of a story + operationId: getRecordById + parameters: + - name: sbas_id + in: path + description: ID of the databox + required: true + schema: + $ref: schemas.yaml#/ID + - name: record_id + in: path + description: ID of the story record + required: true + schema: + $ref: schemas.yaml#/ID + - name: page + in: query + description: page number (default 1) + required: false + schema: + type: integer + - name: per_page + in: query + description: number of children (records uri) per page (default 10) + required: false + schema: + type: integer + responses: + 200: + description: ok + content: + application/json: + schema: + $ref: schemas.yaml#/ApiResponse_RecordsUriArray + 404: + description: Story (record) not found + default: + $ref: 'responses.yaml#/error_response' diff --git a/API_documentation/v3/responses.yaml b/API_documentation/v3/responses.yaml index 5cf544e8f0..1ac2155f3e 100644 --- a/API_documentation/v3/responses.yaml +++ b/API_documentation/v3/responses.yaml @@ -1,2 +1,15 @@ +record_response: + type: object + properties: + 200: + description: successful operation + content: + application/json: + schema: + $ref: schemas.yaml#/ApiResponse_record + 400: + description: Any other error + 404: + description: Record not found error_response: description: Any (other) error diff --git a/API_documentation/v3/schemas.yaml b/API_documentation/v3/schemas.yaml index 77c30061e1..108e6f7580 100644 --- a/API_documentation/v3/schemas.yaml +++ b/API_documentation/v3/schemas.yaml @@ -169,4 +169,19 @@ Record: - video uuid: type: string +RecordUri: + type: string + example: + '/api/v3/records/1/48' +RecordsUriArray: + type: array + items: + $ref: '#/RecordUri' +ApiResponse_RecordsUriArray: + type: object + properties: + meta: + $ref: '#/ApiResponse_meta' + response: + $ref: '#/RecordsUriArray' diff --git a/lib/Alchemy/Phrasea/Databox/Record/LegacyRecordRepository.php b/lib/Alchemy/Phrasea/Databox/Record/LegacyRecordRepository.php index 30feada346..f52cbac467 100644 --- a/lib/Alchemy/Phrasea/Databox/Record/LegacyRecordRepository.php +++ b/lib/Alchemy/Phrasea/Databox/Record/LegacyRecordRepository.php @@ -12,8 +12,11 @@ namespace Alchemy\Phrasea\Databox\Record; use Alchemy\Phrasea\Application; use Alchemy\Phrasea\Cache\Exception; use Alchemy\Phrasea\Model\Entities\User; +use databox; use Doctrine\DBAL\Connection; use Doctrine\DBAL\Query\QueryBuilder; +use record_adapter; +use set_selection; class LegacyRecordRepository implements RecordRepository { @@ -23,7 +26,7 @@ class LegacyRecordRepository implements RecordRepository private $app; /** - * @var \databox + * @var databox */ private $databox; @@ -32,7 +35,7 @@ class LegacyRecordRepository implements RecordRepository */ private $site; - public function __construct(Application $app, \databox $databox, $site) + public function __construct(Application $app, databox $databox, $site) { $this->app = $app; $this->databox = $databox; @@ -41,7 +44,7 @@ class LegacyRecordRepository implements RecordRepository public function find($record_id, $number = null) { - $record = new \record_adapter($this->app, $this->databox->get_sbas_id(), $record_id, $number, false); + $record = new record_adapter($this->app, $this->databox->get_sbas_id(), $record_id, $number, false); try { $data = $record->get_data_from_cache(); } catch (Exception $exception) { @@ -67,7 +70,7 @@ class LegacyRecordRepository implements RecordRepository /** * @param string $sha256 - * @return \record_adapter[] + * @return record_adapter[] */ public function findBySha256($sha256) { @@ -112,7 +115,7 @@ class LegacyRecordRepository implements RecordRepository /** * @param string $uuid - * @return \record_adapter[] + * @return record_adapter[] */ public function findByUuid($uuid) { @@ -130,7 +133,7 @@ class LegacyRecordRepository implements RecordRepository /** * @param string $uuid * @param array $excludedCollIds - * @return \record_adapter[] + * @return record_adapter[] */ public function findByUuidWithExcludedCollIds($uuid, $excludedCollIds = []) { @@ -190,85 +193,86 @@ class LegacyRecordRepository implements RecordRepository $connection = $this->databox->get_connection(); + // the columns we want from the record + // $selects = $this->getRecordSelects(); + array_unshift($selects, 'r.rid_parent AS story_id'); // add this to default + + // sql parameters will be completed depending of (paginated / not paginated) and/or (user / no user) + // + $parmValues = [ + ':storyIds' => $storyIds, + ]; + $parmTypes = [ + ':storyIds' => Connection::PARAM_INT_ARRAY, + ]; + + // if there is a user, we must join collusr to filter results depending on coll/masks + // + $userFilter = ""; + if(!is_null($user)) { + $userFilter = " INNER JOIN collusr c ON c.site = :site AND c.usr_id = :userId AND c.coll_id=r.coll_id AND ((r.status ^ c.mask_xor) & c.mask_and) = 0\n"; + $parmValues[':site'] = $this->site; + $parmValues[':userId'] = $user instanceof User ? $user->getId() : (int)$user; + } if ($max_items) { - array_unshift($selects, 'sr.rid_parent as story_id'); - - $subBuilder = $connection->createQueryBuilder(); - - $subBuilder - ->select('s.*, - IF(@old_rid_parent != s.rid_parent, @cpt := 1, @cpt := @cpt+1) AS CPT') - ->addSelect("IF(@old_rid_parent != s.rid_parent, IF(@old_rid_parent:=s.rid_parent,'NEW PARENT',0), '----------') AS Y") - ->from('regroup', 's') - ->where('s.rid_parent IN (:storyIds)') - ->setParameter('storyIds', $storyIds, Connection::PARAM_INT_ARRAY) - ->orderBy('s.rid_parent, s.ord') + // + // we want paginated results AFTER applying all filters, we build a dynamic cptr + // WARNING : due to bugs (?) in mysql optimizer, do NOT try to optimize this sql (e.g. removing a sub-q, or moving cpt to anothe sub-q) + // + $sql = "SELECT " . join(', ', $selects) . "\n" + . "FROM (\n" + . " SELECT t.*,\n" + . " IF(@old_rid_parent != t.rid_parent, @cpt := 1, @cpt := @cpt+1) AS CPT,\n" + . " IF(@old_rid_parent != t.rid_parent, IF(@old_rid_parent:=t.rid_parent,'NEW PARENT',0), '----------') AS Y\n" + . " FROM (\n" + . " SELECT g.ord, g.rid_parent, r.coll_id, r.record_id, r.credate, r.status, r.uuid, r.moddate, r.parent_record_id,r.type, r.originalname, r.sha256, r.mime\n" + . " FROM regroup g\n" + . " INNER JOIN record r ON r.record_id=g.rid_child\n" + . $userFilter + . " WHERE g.rid_parent IN ( :storyIds )\n" + . " ORDER BY g.rid_parent, g.ord ASC\n" + . " ) t\n" + . ") r\n" + . "WHERE CPT BETWEEN :offset AND :maxresult" ; - $builder = $subBuilder->getConnection()->createQueryBuilder(); - - $builder->select($selects) - ->from(sprintf('( %s )', $subBuilder->getSQL()), 'sr') - ->innerJoin('sr', 'record', 'r', 'r.record_id = sr.rid_child') - ->where('sr.CPT BETWEEN :offset AND :maxresult') - ->andWhere('r.parent_record_id = 0') - ->setParameter('offset', $offset) - ->setParameter('maxresult', ($offset + $max_items -1)) - ->orderBy('story_id, sr.CPT') - ; - - if (null !== $user) { - $this->addUserFilter($builder, $user); - } + $parmValues[':offset'] = $offset; + $parmValues[':maxresult'] = ($offset + $max_items -1); $connection->executeQuery('SET @cpt = 1'); - $connection->executeQuery('SET @old_rid_parent = -1'); - - - $data = $connection->fetchAll( - $builder->getSQL(), - array_merge($subBuilder->getParameters(), $builder->getParameters()), - array_merge($subBuilder->getParameterTypes(), $builder->getParameterTypes()) - ); - - } else { - array_unshift($selects, 's.rid_parent as story_id'); - - $builder = $connection->createQueryBuilder(); - - $builder - ->select($selects) - ->from('regroup', 's') - ->innerJoin('s', 'record', 'r', 'r.record_id = s.rid_child') - ->where( - 's.rid_parent IN (:storyIds)', - 'r.parent_record_id = 0' - ) - ->orderBy('s.ord', 'ASC') - ->setParameter('storyIds', $storyIds, Connection::PARAM_INT_ARRAY) - ; - - if (null !== $user) { - $this->addUserFilter($builder, $user); - } - - $data = $connection->fetchAll($builder->getSQL(), $builder->getParameters(), $builder->getParameterTypes()); } + else { + // + // se want all children, easy + // + $sql = "SELECT " . join(', ', $selects) . "\n" + . "FROM (\n" + . " SELECT g.ord, g.rid_parent, r.coll_id, r.record_id, r.credate, r.status, r.uuid, r.moddate, r.parent_record_id,r.type, r.originalname, r.sha256, r.mime\n" + . " FROM regroup g\n" + . " INNER JOIN record r ON r.record_id=g.rid_child\n" + . $userFilter + . " WHERE g.rid_parent IN ( :storyIds )\n" + . " ORDER BY g.rid_parent, g.ord ASC\n" + . ") r\n" + ; + } + + $data = $connection->fetchAll($sql, $parmValues, $parmTypes); $records = $this->mapRecordsFromResultSet($data); $selections = array_map( function () { - return new \set_selection($this->app); + return new set_selection($this->app); }, array_flip($storyIds) ); foreach ($records as $index => $child) { - /** @var \set_selection $selection */ + /** @var set_selection $selection */ $selection = $selections[$data[$index]['story_id']]; $child->setNumber($selection->get_count() + 1); @@ -312,12 +316,12 @@ class LegacyRecordRepository implements RecordRepository $stories = $this->mapRecordsFromResultSet($data); $selections = array_map(function () { - return new \set_selection($this->app); + return new set_selection($this->app); }, array_flip($recordIds)); foreach ($stories as $index => $child) { - /** @var \set_selection $selection */ + /** @var set_selection $selection */ $selection = $selections[$data[$index]['child_id']]; $selection->add_element($child); @@ -357,7 +361,7 @@ class LegacyRecordRepository implements RecordRepository /** * @param array $result - * @return \record_adapter[] + * @return record_adapter[] */ private function mapRecordsFromResultSet(array $result) { @@ -372,13 +376,13 @@ class LegacyRecordRepository implements RecordRepository /** * @param array $row - * @param \record_adapter|null $record - * @return \record_adapter + * @param record_adapter|null $record + * @return record_adapter */ - private function mapRecordFromResultRow(array $row, \record_adapter $record = null) + private function mapRecordFromResultRow(array $row, record_adapter $record = null) { if (null === $record) { - $record = new \record_adapter($this->app, $this->databox->get_sbas_id(), $row['record_id'], null, false); + $record = new record_adapter($this->app, $this->databox->get_sbas_id(), $row['record_id'], null, false); } $record->mapFromData($row); @@ -400,15 +404,13 @@ class LegacyRecordRepository implements RecordRepository ->select('1') ->from('collusr', 'c') ->where( - 'c.usr_id = :userId', - 'c.site = :site', + 'c.usr_id = ' . ($user instanceof User ? $user->getId() : (int)$user), + 'c.site = \'' . $this->site . '\'', '((r.status ^ c.mask_xor) & c.mask_and) = 0', 'c.coll_id = r.coll_id' ); $builder - ->andWhere(sprintf('EXISTS(%s)', $subBuilder->getSQL())) - ->setParameter('userId', $user instanceof User ? $user->getId() : (int)$user) - ->setParameter('site', $this->site); + ->andWhere(sprintf('EXISTS(%s)', $subBuilder->getSQL())); } }