mirror of
https://github.com/BookStackApp/BookStack.git
synced 2026-10-12 02:01:37 +00:00
Search API: Review of #6182, to add page/book search API endpoints
- Updated examples to ensure they show results of the same page/book. - Added method to controller to share the same request data logic. - Tweaked tests.
This commit is contained in:
@@ -9,6 +9,7 @@ use BookStack\Entities\Models\Entity;
|
||||
use BookStack\Http\ApiController;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class SearchApiController extends ApiController
|
||||
{
|
||||
@@ -50,17 +51,14 @@ class SearchApiController extends ApiController
|
||||
{
|
||||
$this->validate($request, $this->rules['all']);
|
||||
|
||||
$options = SearchOptions::fromString($request->input('query') ?? '');
|
||||
$page = intval($request->input('page', '0')) ?: 1;
|
||||
$count = min(intval($request->input('count', '0')) ?: 20, 100);
|
||||
|
||||
[$options, $page, $count] = $this->getRequestData($request);
|
||||
$results = $this->searchRunner->searchEntities($options, 'all', $page, $count);
|
||||
|
||||
return $this->resultsResponse($results, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a search query against the contents of a single book: its pages and chapters.
|
||||
* Run a search query against the contents of a single book, searching its pages and chapters.
|
||||
* Takes the same input as the 'all' endpoint, and the same input as the search box
|
||||
* shown within a book in the BookStack interface.
|
||||
*
|
||||
@@ -71,10 +69,7 @@ class SearchApiController extends ApiController
|
||||
{
|
||||
$this->validate($request, $this->rules['book']);
|
||||
|
||||
$options = SearchOptions::fromString($request->input('query') ?? '');
|
||||
$page = intval($request->input('page', '0')) ?: 1;
|
||||
$count = min(intval($request->input('count', '0')) ?: 20, 100);
|
||||
|
||||
[$options, $page, $count] = $this->getRequestData($request);
|
||||
$results = $this->searchRunner->searchBook(intval($id), $options, $page, $count);
|
||||
|
||||
return $this->resultsResponse($results, $options);
|
||||
@@ -91,19 +86,28 @@ class SearchApiController extends ApiController
|
||||
{
|
||||
$this->validate($request, $this->rules['chapter']);
|
||||
|
||||
$options = SearchOptions::fromString($request->input('query') ?? '');
|
||||
$page = intval($request->input('page', '0')) ?: 1;
|
||||
$count = min(intval($request->input('count', '0')) ?: 20, 100);
|
||||
|
||||
[$options, $page, $count] = $this->getRequestData($request);
|
||||
$results = $this->searchRunner->searchChapter(intval($id), $options, $page, $count);
|
||||
|
||||
return $this->resultsResponse($results, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get common search request data.
|
||||
* @return array{0: SearchOptions, 1: int, 2: int}
|
||||
*/
|
||||
protected function getRequestData(Request $request): array
|
||||
{
|
||||
$options = SearchOptions::fromString($request->input('query') ?? '');
|
||||
$page = intval($request->input('page', '0')) ?: 1;
|
||||
$count = min(intval($request->input('count', '0')) ?: 20, 100);
|
||||
return [$options, $page, $count];
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a set of search results into the standard API response shape.
|
||||
*
|
||||
* @param array{total: int, results: \Illuminate\Support\Collection} $results
|
||||
* @param array{total: int, results: Collection} $results
|
||||
*/
|
||||
protected function resultsResponse(array $results, SearchOptions $options): JsonResponse
|
||||
{
|
||||
|
||||
@@ -58,35 +58,7 @@
|
||||
"order": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "How advanced are cats?",
|
||||
"id": 362,
|
||||
"slug": "how-advanced-are-cats",
|
||||
"book_id": 13,
|
||||
"chapter_id": 73,
|
||||
"draft": false,
|
||||
"template": false,
|
||||
"created_at": "2020-11-29T21:55:07.000000Z",
|
||||
"updated_at": "2021-11-14T16:02:39.000000Z",
|
||||
"type": "page",
|
||||
"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."
|
||||
},
|
||||
"tags": []
|
||||
}
|
||||
],
|
||||
"total": 3
|
||||
"total": 2
|
||||
}
|
||||
|
||||
@@ -43,23 +43,23 @@
|
||||
"name": "How advanced are cats?",
|
||||
"id": 362,
|
||||
"slug": "how-advanced-are-cats",
|
||||
"book_id": 13,
|
||||
"chapter_id": 73,
|
||||
"book_id": 1,
|
||||
"chapter_id": 75,
|
||||
"draft": false,
|
||||
"template": false,
|
||||
"created_at": "2020-11-29T21:55:07.000000Z",
|
||||
"updated_at": "2021-11-14T16:02:39.000000Z",
|
||||
"type": "page",
|
||||
"url": "https://example.com/books/big-cats/page/how-advanced-are-cats",
|
||||
"url": "https://example.com/books/cats/page/how-advanced-are-cats",
|
||||
"book": {
|
||||
"id": 13,
|
||||
"name": "Big Cats",
|
||||
"slug": "big-cats"
|
||||
"id": 1,
|
||||
"name": "Cats",
|
||||
"slug": "cats"
|
||||
},
|
||||
"chapter": {
|
||||
"id": 73,
|
||||
"name": "A chapter for bigger cats",
|
||||
"slug": "a-chapter-for-bigger-cats"
|
||||
"id": 75,
|
||||
"name": "A chapter for cats",
|
||||
"slug": "a-chapter-for-cats"
|
||||
},
|
||||
"preview_html": {
|
||||
"name": "How advanced are <strong>cats</strong>?",
|
||||
|
||||
@@ -68,9 +68,13 @@ class SearchApiTest extends TestCase
|
||||
$chapter = $book->chapters->first();
|
||||
$chapter->update(['name' => $uniqueTerm . ' chapter']);
|
||||
$chapter->indexForSearch();
|
||||
$page = $book->pages->first();
|
||||
$page->update(['name' => $uniqueTerm . ' page']);
|
||||
$page->indexForSearch();
|
||||
|
||||
$resp = $this->getJson("/api/search/book/{$book->id}?query=" . urlencode($uniqueTerm));
|
||||
$resp->assertJsonFragment(['name' => $chapter->name, 'type' => 'chapter']);
|
||||
$resp->assertJsonFragment(['name' => 'MyUniqueBookChapterApiTerm chapter', 'type' => 'chapter']);
|
||||
$resp->assertJsonFragment(['name' => 'MyUniqueBookChapterApiTerm page', 'type' => 'page']);
|
||||
}
|
||||
|
||||
public function test_book_endpoint_cannot_be_widened_to_other_types()
|
||||
@@ -86,12 +90,16 @@ class SearchApiTest extends TestCase
|
||||
$page->update(['name' => $uniqueTerm . ' a page inside']);
|
||||
$page->indexForSearch();
|
||||
|
||||
$otherBook = $this->entities->book();
|
||||
$otherBook->update(['name' => $uniqueTerm . ' the other book']);
|
||||
$otherBook->indexForSearch();
|
||||
|
||||
// A book cannot contain a book, so asking for one returns nothing rather than
|
||||
// quietly falling back to searching everything.
|
||||
$resp = $this->getJson("/api/search/book/{$book->id}?query=" . urlencode($uniqueTerm . ' {type:book}'));
|
||||
$resp->assertOk();
|
||||
$resp->assertJsonPath('total', 0);
|
||||
$resp->assertJsonMissing(['name' => $book->name]);
|
||||
$resp->assertDontSee($uniqueTerm);
|
||||
}
|
||||
|
||||
public function test_chapter_endpoint_limits_results_to_that_chapter()
|
||||
@@ -117,9 +125,10 @@ class SearchApiTest extends TestCase
|
||||
{
|
||||
$this->actingAsApiEditor();
|
||||
$book = $this->entities->book();
|
||||
$chapter = $this->entities->chapter();
|
||||
|
||||
$this->getJson("/api/search/book/{$book->id}")->assertStatus(422);
|
||||
$this->getJson("/api/search/chapter/1")->assertStatus(422);
|
||||
$this->getJson("/api/search/chapter/{$chapter->id}")->assertStatus(422);
|
||||
}
|
||||
|
||||
public function test_all_endpoint_returns_entity_url()
|
||||
|
||||
Reference in New Issue
Block a user