Skip to content

Commit 0b26573

Browse files
committed
Search: Started work to make search result size consistent
1 parent c21c36e commit 0b26573

File tree

6 files changed

+284
-79
lines changed

6 files changed

+284
-79
lines changed

app/Entities/Models/Entity.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,4 +471,17 @@ protected function getContentsAttributes(): array
471471

472472
return $contentFields;
473473
}
474+
475+
/**
476+
* Create a new instance for the given entity type.
477+
*/
478+
public static function instanceFromType(string $type): self
479+
{
480+
return match ($type) {
481+
'page' => new Page(),
482+
'chapter' => new Chapter(),
483+
'book' => new Book(),
484+
'bookshelf' => new Bookshelf(),
485+
};
486+
}
474487
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
namespace BookStack\Entities\Models;
4+
5+
use BookStack\App\Model;
6+
use BookStack\Permissions\Models\JointPermission;
7+
use BookStack\Permissions\PermissionApplicator;
8+
use Illuminate\Database\Eloquent\Builder;
9+
use Illuminate\Database\Eloquent\Relations\HasMany;
10+
use Illuminate\Database\Eloquent\SoftDeletes;
11+
12+
/**
13+
* This is a simplistic model interpretation of a generic Entity used to query and represent
14+
* that database abstractly. Generally, this should rarely be used outside queries.
15+
*/
16+
class EntityTable extends Model
17+
{
18+
use SoftDeletes;
19+
20+
protected $table = 'entities';
21+
22+
/**
23+
* Get the entities that are visible to the current user.
24+
*/
25+
public function scopeVisible(Builder $query): Builder
26+
{
27+
return app()->make(PermissionApplicator::class)->restrictEntityQuery($query);
28+
}
29+
30+
/**
31+
* Get the entity jointPermissions this is connected to.
32+
*/
33+
public function jointPermissions(): HasMany
34+
{
35+
return $this->hasMany(JointPermission::class, 'entity_id')->whereColumn('entity_type', '=', 'entities.type');
36+
}
37+
}

app/Entities/Queries/EntityQueries.php

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
namespace BookStack\Entities\Queries;
44

55
use BookStack\Entities\Models\Entity;
6+
use BookStack\Entities\Models\EntityTable;
67
use Illuminate\Database\Eloquent\Builder;
8+
use Illuminate\Database\Query\JoinClause;
9+
use Illuminate\Support\Facades\DB;
710
use InvalidArgumentException;
811

912
class EntityQueries
@@ -32,12 +35,31 @@ public function findVisibleByStringIdentifier(string $identifier): ?Entity
3235
return $queries->findVisibleById($entityId);
3336
}
3437

38+
/**
39+
* Start a query across all entity types.
40+
* Combines the description/text fields into a single 'description' field.
41+
* @return Builder<EntityTable>
42+
*/
43+
public function visibleForList(): Builder
44+
{
45+
$rawDescriptionField = DB::raw('COALESCE(description, text) as description');
46+
return EntityTable::query()->scopes('visible')
47+
->select(['id', 'type', 'name', 'slug', 'book_id', 'chapter_id', 'created_at', 'updated_at', 'draft', $rawDescriptionField])
48+
->leftJoin('entity_container_data', function (JoinClause $join) {
49+
$join->on('entity_container_data.entity_id', '=', 'entities.id')
50+
->on('entity_container_data.entity_type', '=', 'entities.type');
51+
})->leftJoin('entity_page_data', function (JoinClause $join) {
52+
$join->on('entity_page_data.page_id', '=', 'entities.id')
53+
->where('entities.type', '=', 'page');
54+
});
55+
}
56+
3557
/**
3658
* Start a query of visible entities of the given type,
3759
* suitable for listing display.
3860
* @return Builder<Entity>
3961
*/
40-
public function visibleForList(string $entityType): Builder
62+
public function visibleForListForType(string $entityType): Builder
4163
{
4264
$queries = $this->getQueriesForType($entityType);
4365
return $queries->visibleForList();
@@ -48,7 +70,7 @@ public function visibleForList(string $entityType): Builder
4870
* suitable for using the contents of the items.
4971
* @return Builder<Entity>
5072
*/
51-
public function visibleForContent(string $entityType): Builder
73+
public function visibleForContentForType(string $entityType): Builder
5274
{
5375
$queries = $this->getQueriesForType($entityType);
5476
return $queries->visibleForContent();
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
<?php
2+
3+
namespace BookStack\Entities\Tools;
4+
5+
use BookStack\Activity\Models\Tag;
6+
use BookStack\Entities\Models\Chapter;
7+
use BookStack\Entities\Models\Entity;
8+
use BookStack\Entities\Models\EntityTable;
9+
use BookStack\Entities\Models\Page;
10+
use BookStack\Entities\Queries\EntityQueries;
11+
use Illuminate\Database\Eloquent\Collection;
12+
13+
class EntityHydrator
14+
{
15+
/**
16+
* @var EntityTable[] $entities
17+
*/
18+
protected array $entities;
19+
20+
protected bool $loadTags = false;
21+
protected bool $loadParents = false;
22+
23+
public function __construct(array $entities, bool $loadTags = false, bool $loadParents = false)
24+
{
25+
$this->entities = $entities;
26+
$this->loadTags = $loadTags;
27+
$this->loadParents = $loadParents;
28+
}
29+
30+
/**
31+
* Hydrate the entities of this hydrator to return a list of entities represented
32+
* in their original intended models.
33+
* @return Entity[]
34+
*/
35+
public function hydrate(): array
36+
{
37+
$hydrated = [];
38+
39+
foreach ($this->entities as $entity) {
40+
$data = $entity->toArray();
41+
$instance = Entity::instanceFromType($entity->type);
42+
43+
if ($instance instanceof Page) {
44+
$data['text'] = $data['description'];
45+
unset($data['description']);
46+
}
47+
48+
$instance->forceFill($data);
49+
$hydrated[] = $instance;
50+
}
51+
52+
if ($this->loadTags) {
53+
$this->loadTagsIntoModels($hydrated);
54+
}
55+
56+
if ($this->loadParents) {
57+
$this->loadParentsIntoModels($hydrated);
58+
}
59+
60+
return $hydrated;
61+
}
62+
63+
/**
64+
* @param Entity[] $entities
65+
*/
66+
protected function loadTagsIntoModels(array $entities): void
67+
{
68+
$idsByType = [];
69+
$entityMap = [];
70+
foreach ($entities as $entity) {
71+
if (!isset($idsByType[$entity->type])) {
72+
$idsByType[$entity->type] = [];
73+
}
74+
$idsByType[$entity->type][] = $entity->id;
75+
$entityMap[$entity->type . ':' . $entity->id] = $entity;
76+
}
77+
78+
$query = Tag::query();
79+
foreach ($idsByType as $type => $ids) {
80+
$query->orWhere(function ($query) use ($type, $ids) {
81+
$query->where('entity_type', '=', $type)
82+
->whereIn('entity_id', $ids);
83+
});
84+
}
85+
86+
$tags = empty($idsByType) ? [] : $query->get()->all();
87+
$tagMap = [];
88+
foreach ($tags as $tag) {
89+
$key = $tag->entity_type . ':' . $tag->entity_id;
90+
if (!isset($tagMap[$key])) {
91+
$tagMap[$key] = [];
92+
}
93+
$tagMap[$key][] = $tag;
94+
}
95+
96+
foreach ($entityMap as $key => $entity) {
97+
$entityTags = new Collection($tagMap[$key] ?? []);
98+
$entity->setRelation('tags', $entityTags);
99+
}
100+
}
101+
102+
/**
103+
* @param Entity[] $entities
104+
*/
105+
protected function loadParentsIntoModels(array $entities): void
106+
{
107+
$parentsByType = ['book' => [], 'chapter' => []];
108+
109+
foreach ($entities as $entity) {
110+
if ($entity->getAttribute('book_id') !== null) {
111+
$parentsByType['book'][] = $entity->getAttribute('book_id');
112+
}
113+
if ($entity->getAttribute('chapter_id') !== null) {
114+
$parentsByType['chapter'][] = $entity->getAttribute('chapter_id');
115+
}
116+
}
117+
118+
// TODO - Inject in?
119+
$queries = app()->make(EntityQueries::class);
120+
121+
$parentQuery = $queries->visibleForList();
122+
$filtered = count($parentsByType['book']) > 0 || count($parentsByType['chapter']) > 0;
123+
$parentQuery = $parentQuery->where(function ($query) use ($parentsByType) {
124+
foreach ($parentsByType as $type => $ids) {
125+
if (count($ids) > 0) {
126+
$query = $query->orWhere(function ($query) use ($type, $ids) {
127+
$query->where('type', '=', $type)
128+
->whereIn('id', $ids);
129+
});
130+
}
131+
}
132+
});
133+
134+
$parents = $filtered ? (new EntityHydrator($parentQuery->get()->all()))->hydrate() : [];
135+
$parentMap = [];
136+
foreach ($parents as $parent) {
137+
$parentMap[$parent->type . ':' . $parent->id] = $parent;
138+
}
139+
140+
foreach ($entities as $entity) {
141+
if ($entity instanceof Page || $entity instanceof Chapter) {
142+
$key = 'book:' . $entity->getAttribute('book_id');
143+
$entity->setRelation('book', $parentMap[$key] ?? null);
144+
}
145+
if ($entity instanceof Page) {
146+
$key = 'chapter:' . $entity->getAttribute('chapter_id');
147+
$entity->setRelation('chapter', $parentMap[$key] ?? null);
148+
}
149+
}
150+
}
151+
}

app/Entities/Tools/MixedEntityListLoader.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ protected function idsByTypeToModelMap(array $idsByType, bool $eagerLoadParents,
5454
$modelMap = [];
5555

5656
foreach ($idsByType as $type => $ids) {
57-
$base = $withContents ? $this->queries->visibleForContent($type) : $this->queries->visibleForList($type);
57+
$base = $withContents ? $this->queries->visibleForContentForType($type) : $this->queries->visibleForListForType($type);
5858
$models = $base->whereIn('id', $ids)
5959
->with($eagerLoadParents ? $this->getRelationsToEagerLoad($type) : [])
6060
->get();

0 commit comments

Comments
 (0)