mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-10-29 23:22:34 +01:00
Added API search endpoint
Is a little awkward, emulates a 'list' API endpoint but has unstable paging and does not support filters/sort. This is detailed on the endpoint though. Made some updates to the docs system to better support parameters and examples on GET requests. Includes tests to cover. For #909
This commit is contained in:
parent
2051189921
commit
6f1bdbf771
@ -55,10 +55,16 @@ class ApiDocsGenerator
|
||||
{
|
||||
return $routes->map(function (array $route) {
|
||||
$exampleTypes = ['request', 'response'];
|
||||
$fileTypes = ['json', 'http'];
|
||||
foreach ($exampleTypes as $exampleType) {
|
||||
$exampleFile = base_path("dev/api/{$exampleType}s/{$route['name']}.json");
|
||||
$exampleContent = file_exists($exampleFile) ? file_get_contents($exampleFile) : null;
|
||||
$route["example_{$exampleType}"] = $exampleContent;
|
||||
foreach ($fileTypes as $fileType) {
|
||||
$exampleFile = base_path("dev/api/{$exampleType}s/{$route['name']}." . $fileType);
|
||||
if (file_exists($exampleFile)) {
|
||||
$route["example_{$exampleType}"] = file_get_contents($exampleFile);
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
$route["example_{$exampleType}"] = null;
|
||||
}
|
||||
|
||||
return $route;
|
||||
|
@ -56,6 +56,8 @@ class SearchRunner
|
||||
* Search all entities in the system.
|
||||
* The provided count is for each entity to search,
|
||||
* Total returned could be larger and not guaranteed.
|
||||
*
|
||||
* @return array{total: int, count: int, has_more: bool, results: Entity[]}
|
||||
*/
|
||||
public function searchEntities(SearchOptions $searchOpts, string $entityType = 'all', int $page = 1, int $count = 20, string $action = 'view'): array
|
||||
{
|
||||
|
67
app/Http/Controllers/Api/SearchApiController.php
Normal file
67
app/Http/Controllers/Api/SearchApiController.php
Normal file
@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Http\Controllers\Api;
|
||||
|
||||
use BookStack\Entities\Models\Entity;
|
||||
use BookStack\Entities\Tools\SearchOptions;
|
||||
use BookStack\Entities\Tools\SearchRunner;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class SearchApiController extends ApiController
|
||||
{
|
||||
protected $searchRunner;
|
||||
|
||||
protected $rules = [
|
||||
'all' => [
|
||||
'query' => ['required'],
|
||||
'page' => ['integer', 'min:1'],
|
||||
'count' => ['integer', 'min:1', 'max:100'],
|
||||
],
|
||||
];
|
||||
|
||||
public function __construct(SearchRunner $searchRunner)
|
||||
{
|
||||
$this->searchRunner = $searchRunner;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a search query against all main content types (shelves, books, chapters & pages)
|
||||
* in the system. Takes the same input as the main search bar within the BookStack
|
||||
* interface as a 'query' parameter. See https://www.bookstackapp.com/docs/user/searching/
|
||||
* for a full list of search term options. Results contain a 'type' property to distinguish
|
||||
* between: bookshelf, book, chapter & page.
|
||||
*
|
||||
* The paging parameters and response format emulates a standard listing endpoint
|
||||
* but standard sorting and filtering cannot be done on this endpoint. If a count value
|
||||
* is provided this will only be taken as a suggestion. The results in the response
|
||||
* may currently be up to 4x this value.
|
||||
*/
|
||||
public function all(Request $request)
|
||||
{
|
||||
$this->validate($request, $this->rules['all']);
|
||||
|
||||
$options = SearchOptions::fromString($request->get('query') ?? '');
|
||||
$page = intval($request->get('page', '0')) ?: 1;
|
||||
$count = min(intval($request->get('count', '0')) ?: 20, 100);
|
||||
|
||||
$results = $this->searchRunner->searchEntities($options, 'all', $page, $count);
|
||||
|
||||
/** @var Entity $result */
|
||||
foreach ($results['results'] as $result) {
|
||||
$result->setVisible([
|
||||
'id', 'name', 'slug', 'book_id',
|
||||
'chapter_id', 'draft', 'template',
|
||||
'created_at', 'updated_at',
|
||||
'tags', 'type',
|
||||
]);
|
||||
$result->setAttribute('type', $result->getType());
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'data' => $results['results'],
|
||||
'total' => $results['total'],
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
}
|
1
dev/api/requests/search-all.http
Normal file
1
dev/api/requests/search-all.http
Normal file
@ -0,0 +1 @@
|
||||
GET /api/search?query=cats+{created_by:me}&page=1&count=2
|
52
dev/api/responses/search-all.json
Normal file
52
dev/api/responses/search-all.json
Normal file
@ -0,0 +1,52 @@
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": 84,
|
||||
"book_id": 1,
|
||||
"slug": "a-chapter-for-cats",
|
||||
"name": "A chapter for cats",
|
||||
"created_at": "2021-11-14T15:57:35.000000Z",
|
||||
"updated_at": "2021-11-14T15:57:35.000000Z",
|
||||
"type": "chapter",
|
||||
"tags": []
|
||||
},
|
||||
{
|
||||
"name": "The hows and whys of cats",
|
||||
"id": 396,
|
||||
"slug": "the-hows-and-whys-of-cats",
|
||||
"book_id": 1,
|
||||
"chapter_id": 75,
|
||||
"draft": false,
|
||||
"template": false,
|
||||
"created_at": "2021-05-15T16:28:10.000000Z",
|
||||
"updated_at": "2021-11-14T15:56:49.000000Z",
|
||||
"type": "page",
|
||||
"tags": [
|
||||
{
|
||||
"name": "Animal",
|
||||
"value": "Cat",
|
||||
"order": 0
|
||||
},
|
||||
{
|
||||
"name": "Category",
|
||||
"value": "Top Content",
|
||||
"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",
|
||||
"tags": []
|
||||
}
|
||||
],
|
||||
"total": 3
|
||||
}
|
@ -13,7 +13,7 @@
|
||||
|
||||
@if($endpoint['body_params'] ?? false)
|
||||
<details class="mb-m">
|
||||
<summary class="text-muted">Body Parameters</summary>
|
||||
<summary class="text-muted">{{ $endpoint['method'] === 'GET' ? 'Query' : 'Body' }} Parameters</summary>
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>Param Name</th>
|
||||
|
@ -9,6 +9,7 @@ use BookStack\Http\Controllers\Api\ChapterApiController;
|
||||
use BookStack\Http\Controllers\Api\ChapterExportApiController;
|
||||
use BookStack\Http\Controllers\Api\PageApiController;
|
||||
use BookStack\Http\Controllers\Api\PageExportApiController;
|
||||
use BookStack\Http\Controllers\Api\SearchApiController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
/**
|
||||
@ -57,6 +58,8 @@ Route::get('pages/{id}/export/pdf', [PageExportApiController::class, 'exportPdf'
|
||||
Route::get('pages/{id}/export/plaintext', [PageExportApiController::class, 'exportPlainText']);
|
||||
Route::get('pages/{id}/export/markdown', [PageExportApiController::class, 'exportMarkDown']);
|
||||
|
||||
Route::get('search', [SearchApiController::class, 'all']);
|
||||
|
||||
Route::get('shelves', [BookshelfApiController::class, 'list']);
|
||||
Route::post('shelves', [BookshelfApiController::class, 'create']);
|
||||
Route::get('shelves/{id}', [BookshelfApiController::class, 'read']);
|
||||
|
47
tests/Api/SearchApiTest.php
Normal file
47
tests/Api/SearchApiTest.php
Normal file
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Api;
|
||||
|
||||
use BookStack\Entities\Models\Book;
|
||||
use BookStack\Entities\Models\Bookshelf;
|
||||
use BookStack\Entities\Models\Chapter;
|
||||
use BookStack\Entities\Models\Entity;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use Tests\TestCase;
|
||||
|
||||
class SearchApiTest extends TestCase
|
||||
{
|
||||
use TestsApi;
|
||||
|
||||
protected $baseEndpoint = '/api/search';
|
||||
|
||||
public function test_all_endpoint_returns_search_filtered_results_with_query()
|
||||
{
|
||||
$this->actingAsApiEditor();
|
||||
$uniqueTerm = 'MySuperUniqueTermForSearching';
|
||||
|
||||
/** @var Entity $entityClass */
|
||||
foreach ([Page::class, Chapter::class, Book::class, Bookshelf::class] as $entityClass) {
|
||||
/** @var Entity $first */
|
||||
$first = $entityClass::query()->first();
|
||||
$first->update(['name' => $uniqueTerm]);
|
||||
$first->indexForSearch();
|
||||
}
|
||||
|
||||
$resp = $this->getJson($this->baseEndpoint . '?query=' . $uniqueTerm . '&count=5&page=1');
|
||||
$resp->assertJsonCount(4, 'data');
|
||||
$resp->assertJsonFragment(['name' => $uniqueTerm, 'type' => 'book']);
|
||||
$resp->assertJsonFragment(['name' => $uniqueTerm, 'type' => 'chapter']);
|
||||
$resp->assertJsonFragment(['name' => $uniqueTerm, 'type' => 'page']);
|
||||
$resp->assertJsonFragment(['name' => $uniqueTerm, 'type' => 'bookshelf']);
|
||||
}
|
||||
|
||||
public function test_all_endpoint_requires_query_parameter()
|
||||
{
|
||||
$resp = $this->actingAsApiEditor()->get($this->baseEndpoint);
|
||||
$resp->assertStatus(422);
|
||||
|
||||
$resp = $this->actingAsApiEditor()->get($this->baseEndpoint . '?query=myqueryvalue');
|
||||
$resp->assertOk();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user