namespace BookStack\Api;
+use BookStack\Entities\Models\BookChild;
use BookStack\Entities\Models\Entity;
+use BookStack\Entities\Models\Page;
class ApiEntityListFormatter
{
* @var array<string|int, string|callable>
*/
protected array $fields = [
- 'id', 'name', 'slug', 'book_id', 'chapter_id', 'draft',
- 'template', 'priority', 'created_at', 'updated_at',
+ 'id',
+ 'name',
+ 'slug',
+ 'book_id',
+ 'chapter_id',
+ 'draft',
+ 'template',
+ 'priority',
+ 'created_at',
+ 'updated_at',
];
public function __construct(array $list)
return $this;
}
+ /**
+ * Include parent book/chapter info in the formatted data.
+ */
+ public function withParents(): self
+ {
+ $this->withField('book', function (Entity $entity) {
+ if ($entity instanceof BookChild && $entity->book) {
+ return $entity->book->only(['id', 'name', 'slug']);
+ }
+ return null;
+ });
+
+ $this->withField('chapter', function (Entity $entity) {
+ if ($entity instanceof Page && $entity->chapter) {
+ return $entity->chapter->only(['id', 'name', 'slug']);
+ }
+ return null;
+ });
+
+ return $this;
+ }
+
/**
* Format the data and return an array of formatted content.
* @return array[]
class SearchApiController extends ApiController
{
- protected SearchRunner $searchRunner;
- protected SearchResultsFormatter $resultsFormatter;
-
protected $rules = [
'all' => [
- 'query' => ['required'],
- 'page' => ['integer', 'min:1'],
- 'count' => ['integer', 'min:1', 'max:100'],
+ 'query' => ['required'],
+ 'page' => ['integer', 'min:1'],
+ 'count' => ['integer', 'min:1', 'max:100'],
],
];
- public function __construct(SearchRunner $searchRunner, SearchResultsFormatter $resultsFormatter)
- {
- $this->searchRunner = $searchRunner;
- $this->resultsFormatter = $resultsFormatter;
+ public function __construct(
+ protected SearchRunner $searchRunner,
+ protected SearchResultsFormatter $resultsFormatter
+ ) {
}
/**
$this->resultsFormatter->format($results['results']->all(), $options);
$data = (new ApiEntityListFormatter($results['results']->all()))
- ->withType()->withTags()
+ ->withType()->withTags()->withParents()
->withField('preview_html', function (Entity $entity) {
return [
- 'name' => (string) $entity->getAttribute('preview_name'),
+ 'name' => (string) $entity->getAttribute('preview_name'),
'content' => (string) $entity->getAttribute('preview_content'),
];
})->format();
return response()->json([
- 'data' => $data,
+ 'data' => $data,
'total' => $results['total'],
]);
}
-GET /api/search?query=cats+{created_by:me}&page=1&count=2
\ No newline at end of file
+GET /api/search?query=cats+{created_by:me}&page=1&count=2
"created_at": "2021-11-14T15:57:35.000000Z",
"updated_at": "2021-11-14T15:57:35.000000Z",
"type": "chapter",
- "url": "https://example.com/books/my-book/chapter/a-chapter-for-cats",
+ "url": "https://example.com/books/cats/chapter/a-chapter-for-cats",
+ "book": {
+ "id": 1,
+ "name": "Cats",
+ "slug": "cats"
+ },
"preview_html": {
"name": "A chapter for <strong>cats</strong>",
"content": "...once a bunch of <strong>cats</strong> named tony...behaviour of <strong>cats</strong> is unsuitable"
"created_at": "2021-05-15T16:28:10.000000Z",
"updated_at": "2021-11-14T15:56:49.000000Z",
"type": "page",
- "url": "https://example.com/books/my-book/page/the-hows-and-whys-of-cats",
+ "url": "https://example.com/books/cats/page/the-hows-and-whys-of-cats",
+ "book": {
+ "id": 1,
+ "name": "Cats",
+ "slug": "cats"
+ },
+ "chapter": {
+ "id": 75,
+ "name": "A chapter for cats",
+ "slug": "a-chapter-for-cats"
+ },
"preview_html": {
"name": "The hows and whys of <strong>cats</strong>",
"content": "...people ask why <strong>cats</strong>? but there are...the reason that <strong>cats</strong> are fast are due to..."
"created_at": "2020-11-29T21:55:07.000000Z",
"updated_at": "2021-11-14T16:02:39.000000Z",
"type": "page",
- "url": "https://example.com/books/my-book/page/how-advanced-are-cats",
+ "url": "https://example.com/books/big-cats/page/how-advanced-are-cats",
+ "book": {
+ "id": 13,
+ "name": "Big Cats",
+ "slug": "big-cats"
+ },
+ "chapter": {
+ "id": 73,
+ "name": "A chapter for bigger cats",
+ "slug": "a-chapter-for-bigger-cats"
+ },
"preview_html": {
"name": "How advanced are <strong>cats</strong>?",
"content": "<strong>cats</strong> are some of the most advanced animals in the world."
}
],
"total": 3
-}
\ No newline at end of file
+}
{
use TestsApi;
- protected $baseEndpoint = '/api/search';
+ protected string $baseEndpoint = '/api/search';
public function test_all_endpoint_returns_search_filtered_results_with_query()
{
$resp = $this->actingAsApiAdmin()->getJson($this->baseEndpoint . '?query=superuniquevalue');
$resp->assertJsonFragment([
'type' => 'page',
- 'url' => $page->getUrl(),
+ 'url' => $page->getUrl(),
]);
}
$resp = $this->actingAsApiAdmin()->getJson($this->baseEndpoint . '?query=superuniquevalue');
$resp->assertJsonFragment([
- 'type' => 'book',
- 'url' => $book->getUrl(),
+ 'type' => 'book',
+ 'url' => $book->getUrl(),
'preview_html' => [
- 'name' => 'name with <strong>superuniquevalue</strong> within',
+ 'name' => 'name with <strong>superuniquevalue</strong> within',
'content' => 'Description with <strong>superuniquevalue</strong> within',
],
]);
$resp = $this->actingAsApiEditor()->get($this->baseEndpoint . '?query=myqueryvalue');
$resp->assertOk();
}
+
+ public function test_all_endpoint_includes_parent_details_where_visible()
+ {
+ $page = $this->entities->pageWithinChapter();
+ $chapter = $page->chapter;
+ $book = $page->book;
+
+ $page->update(['name' => 'name with superextrauniquevalue within']);
+ $page->indexForSearch();
+
+ $editor = $this->users->editor();
+ $this->actingAsApiEditor();
+ $resp = $this->getJson($this->baseEndpoint . '?query=superextrauniquevalue');
+ $resp->assertJsonFragment([
+ 'id' => $page->id,
+ 'type' => 'page',
+ 'book' => [
+ 'id' => $book->id,
+ 'name' => $book->name,
+ 'slug' => $book->slug,
+ ],
+ 'chapter' => [
+ 'id' => $chapter->id,
+ 'name' => $chapter->name,
+ 'slug' => $chapter->slug,
+ ],
+ ]);
+
+ $this->permissions->disableEntityInheritedPermissions($chapter);
+ $this->permissions->setEntityPermissions($page, ['view'], [$editor->roles()->first()]);
+
+ $resp = $this->getJson($this->baseEndpoint . '?query=superextrauniquevalue');
+ $resp->assertJsonPath('data.0.id', $page->id);
+ $resp->assertJsonPath('data.0.book.name', $book->name);
+ $resp->assertJsonMissingPath('data.0.chapter');
+
+ $this->permissions->disableEntityInheritedPermissions($book);
+
+ $resp = $this->getJson($this->baseEndpoint . '?query=superextrauniquevalue');
+ $resp->assertJsonPath('data.0.id', $page->id);
+ $resp->assertJsonMissingPath('data.0.book.name');
+ }
}