Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
50b147c
DB: Planned out new entity table format via migrations
ssddanbrown Sep 15, 2025
4e935f7
DB: Created entity migration logic
ssddanbrown Sep 17, 2025
bc49db7
DB: Added change of entity relation columns to suit new entities table
ssddanbrown Sep 19, 2025
c767dde
DB: Got most view queries working for new stucture
ssddanbrown Sep 19, 2025
c60052b
Entities: Started logic change to new structure
ssddanbrown Sep 23, 2025
bf09b42
Entities: Been through repos to update for new format
ssddanbrown Sep 24, 2025
b866dee
Entities: Updated repos to act on refreshed clones
ssddanbrown Sep 24, 2025
5c2908e
Entities: Updated model classes & relations for changes
ssddanbrown Sep 25, 2025
06ec914
Entities: Changed from *Data to a common "contents" system
ssddanbrown Sep 25, 2025
4f21e54
Entities: Moved entity description/covers to own non-model classes
ssddanbrown Sep 29, 2025
1bd0dda
Entities: Removed use of contents system for data access
ssddanbrown Sep 30, 2025
daf0b84
Entities: Got most queries back to working order
ssddanbrown Sep 30, 2025
90b4157
Entities: Reverted back to data from contents, fixed various issues
ssddanbrown Oct 3, 2025
602d300
Entities: Started addressing issues from tests
ssddanbrown Oct 7, 2025
2daaa70
Entities: Addressed further tests/issues
ssddanbrown Oct 7, 2025
49bb5e1
Entities: Been through tests to get all passing in dev
ssddanbrown Oct 12, 2025
cbd817d
Entities: Addressed phpstan errors
ssddanbrown Oct 16, 2025
e4faf27
Entities: Reviewed TODO notes
ssddanbrown Oct 17, 2025
d0a4c9a
Entities: Ensured book/shelf relation data removed on destroy
ssddanbrown Oct 18, 2025
da87012
Entities: Been through API responses & adjusted field visibility
ssddanbrown Oct 18, 2025
b67110d
Entities: Added type index to massively improve query speed
ssddanbrown Oct 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion app/Access/Mfa/MfaSession.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ class MfaSession
*/
public function isRequiredForUser(User $user): bool
{
// TODO - Test both these cases
return $user->mfaValues()->exists() || $this->userRoleEnforcesMfa($user);
}

Expand Down
6 changes: 2 additions & 4 deletions app/Console/Commands/UpdateUrlCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,8 @@ public function handle(Connection $db): int

$columnsToUpdateByTable = [
'attachments' => ['path'],
'pages' => ['html', 'text', 'markdown'],
'chapters' => ['description_html'],
'books' => ['description_html'],
'bookshelves' => ['description_html'],
'entity_page_data' => ['html', 'text', 'markdown'],
'entity_container_data' => ['description_html'],
'page_revisions' => ['html', 'text', 'markdown'],
'images' => ['url'],
'settings' => ['value'],
Expand Down
7 changes: 4 additions & 3 deletions app/Entities/Controllers/BookApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,10 @@ protected function forJsonDisplay(Book $book): Book
$book = clone $book;
$book->unsetRelations()->refresh();

$book->load(['tags', 'cover']);
$book->makeVisible('description_html')
->setAttribute('description_html', $book->descriptionHtml());
$book->load(['tags']);
$book->makeVisible(['cover', 'description_html'])
->setAttribute('description_html', $book->descriptionInfo()->getHtml())
->setAttribute('cover', $book->coverInfo()->getImage());

return $book;
}
Expand Down
7 changes: 4 additions & 3 deletions app/Entities/Controllers/BookshelfApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,10 @@ protected function forJsonDisplay(Bookshelf $shelf): Bookshelf
$shelf = clone $shelf;
$shelf->unsetRelations()->refresh();

$shelf->load(['tags', 'cover']);
$shelf->makeVisible('description_html')
->setAttribute('description_html', $shelf->descriptionHtml());
$shelf->load(['tags']);
$shelf->makeVisible(['cover', 'description_html'])
->setAttribute('description_html', $shelf->descriptionInfo()->getHtml())
->setAttribute('cover', $shelf->coverInfo()->getImage());

return $shelf;
}
Expand Down
1 change: 1 addition & 0 deletions app/Entities/Controllers/BookshelfController.php
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ public function show(Request $request, ActivityQueries $activities, string $slug
]);

$sort = $listOptions->getSort();

$sortedVisibleShelfBooks = $shelf->visibleBooks()
->reorder($sort === 'default' ? 'order' : $sort, $listOptions->getOrder())
->get()
Expand Down
4 changes: 2 additions & 2 deletions app/Entities/Controllers/ChapterApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ public function update(Request $request, string $id)
$chapter = $this->queries->findVisibleByIdOrFail(intval($id));
$this->checkOwnablePermission(Permission::ChapterUpdate, $chapter);

if ($request->has('book_id') && $chapter->book_id !== intval($requestData['book_id'])) {
if ($request->has('book_id') && $chapter->book_id !== (intval($requestData['book_id']) ?: null)) {
$this->checkOwnablePermission(Permission::ChapterDelete, $chapter);

try {
Expand Down Expand Up @@ -144,7 +144,7 @@ protected function forJsonDisplay(Chapter $chapter): Chapter

$chapter->load(['tags']);
$chapter->makeVisible('description_html');
$chapter->setAttribute('description_html', $chapter->descriptionHtml());
$chapter->setAttribute('description_html', $chapter->descriptionInfo()->getHtml());

/** @var Book $book */
$book = $chapter->book()->first();
Expand Down
2 changes: 1 addition & 1 deletion app/Entities/Controllers/ChapterController.php
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ public function update(Request $request, string $bookSlug, string $chapterSlug)
$chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
$this->checkOwnablePermission(Permission::ChapterUpdate, $chapter);

$this->chapterRepo->update($chapter, $validated);
$chapter = $this->chapterRepo->update($chapter, $validated);

return redirect($chapter->getUrl());
}
Expand Down
1 change: 1 addition & 0 deletions app/Entities/Controllers/PageController.php
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ public function store(Request $request, string $bookSlug, int $pageId)
$this->validate($request, [
'name' => ['required', 'string', 'max:255'],
]);

$draftPage = $this->queries->findVisibleByIdOrFail($pageId);
$this->checkOwnablePermission(Permission::PageCreate, $draftPage->getParent());

Expand Down
20 changes: 20 additions & 0 deletions app/Entities/EntityExistsRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace BookStack\Entities;

use Illuminate\Validation\Rules\Exists;

class EntityExistsRule implements \Stringable
{
public function __construct(
protected string $type,
) {
}

public function __toString()
{
$existsRule = (new Exists('entities', 'id'))
->where('type', $this->type);
return $existsRule->__toString();
}
}
91 changes: 33 additions & 58 deletions app/Entities/Models/Book.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

namespace BookStack\Entities\Models;

use BookStack\Entities\Tools\EntityCover;
use BookStack\Entities\Tools\EntityDefaultTemplate;
use BookStack\Sorting\SortRule;
use BookStack\Uploads\Image;
use Exception;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
Expand All @@ -15,26 +16,25 @@
* Class Book.
*
* @property string $description
* @property string $description_html
* @property int $image_id
* @property ?int $default_template_id
* @property ?int $sort_rule_id
* @property Image|null $cover
* @property \Illuminate\Database\Eloquent\Collection $chapters
* @property \Illuminate\Database\Eloquent\Collection $pages
* @property \Illuminate\Database\Eloquent\Collection $directPages
* @property \Illuminate\Database\Eloquent\Collection $shelves
* @property ?Page $defaultTemplate
* @property ?SortRule $sortRule
* @property ?SortRule $sortRule
*/
class Book extends Entity implements CoverImageInterface, HtmlDescriptionInterface
class Book extends Entity implements HasDescriptionInterface, HasCoverInterface, HasDefaultTemplateInterface
{
use HasFactory;
use HtmlDescriptionTrait;
use ContainerTrait;

public float $searchFactor = 1.2;

protected $hidden = ['pivot', 'deleted_at', 'description_html', 'entity_id', 'entity_type', 'chapter_id', 'book_id', 'priority'];
protected $fillable = ['name'];
protected $hidden = ['pivot', 'image_id', 'deleted_at', 'description_html'];

/**
* Get the url for this book.
Expand All @@ -44,55 +44,6 @@ public function getUrl(string $path = ''): string
return url('/books/' . implode('/', [urlencode($this->slug), trim($path, '/')]));
}

/**
* Returns book cover image, if book cover not exists return default cover image.
*/
public function getBookCover(int $width = 440, int $height = 250): string
{
$default = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
if (!$this->image_id || !$this->cover) {
return $default;
}

try {
return $this->cover->getThumb($width, $height, false) ?? $default;
} catch (Exception $err) {
return $default;
}
}

/**
* Get the cover image of the book.
*/
public function cover(): BelongsTo
{
return $this->belongsTo(Image::class, 'image_id');
}

/**
* Get the type of the image model that is used when storing a cover image.
*/
public function coverImageTypeKey(): string
{
return 'cover_book';
}

/**
* Get the Page that is used as default template for newly created pages within this Book.
*/
public function defaultTemplate(): BelongsTo
{
return $this->belongsTo(Page::class, 'default_template_id');
}

/**
* Get the sort set assigned to this book, if existing.
*/
public function sortRule(): BelongsTo
{
return $this->belongsTo(SortRule::class);
}

/**
* Get all pages within this book.
* @return HasMany<Page, $this>
Expand All @@ -107,7 +58,7 @@ public function pages(): HasMany
*/
public function directPages(): HasMany
{
return $this->pages()->where('chapter_id', '=', '0');
return $this->pages()->whereNull('chapter_id');
}

/**
Expand All @@ -116,7 +67,8 @@ public function directPages(): HasMany
*/
public function chapters(): HasMany
{
return $this->hasMany(Chapter::class);
return $this->hasMany(Chapter::class)
->where('type', '=', 'chapter');
}

/**
Expand All @@ -137,4 +89,27 @@ public function getDirectVisibleChildren(): Collection

return $pages->concat($chapters)->sortBy('priority')->sortByDesc('draft');
}

public function defaultTemplate(): EntityDefaultTemplate
{
return new EntityDefaultTemplate($this);
}

public function cover(): BelongsTo
{
return $this->belongsTo(Image::class, 'image_id');
}

public function coverInfo(): EntityCover
{
return new EntityCover($this);
}

/**
* Get the sort rule assigned to this container, if existing.
*/
public function sortRule(): BelongsTo
{
return $this->belongsTo(SortRule::class);
}
}
5 changes: 2 additions & 3 deletions app/Entities/Models/BookChild.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
namespace BookStack\Entities\Models;

use BookStack\References\ReferenceUpdater;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

/**
Expand All @@ -27,13 +26,13 @@ public function book(): BelongsTo
/**
* Change the book that this entity belongs to.
*/
public function changeBook(int $newBookId): Entity
public function changeBook(int $newBookId): self
{
$oldUrl = $this->getUrl();
$this->book_id = $newBookId;
$this->unsetRelation('book');
$this->refreshSlug();
$this->save();
$this->refresh();

if ($oldUrl !== $this->getUrl()) {
app()->make(ReferenceUpdater::class)->updateEntityReferences($this, $oldUrl);
Expand Down
Loading
Loading