]> BookStack Code Mirror - bookstack/commitdiff
Merge branch 'rashadkhan359/development' into development
authorDan Brown <redacted>
Tue, 3 Dec 2024 13:52:38 +0000 (13:52 +0000)
committerDan Brown <redacted>
Tue, 3 Dec 2024 13:52:38 +0000 (13:52 +0000)
app/Api/ApiEntityListFormatter.php
app/Search/SearchApiController.php
dev/api/requests/search-all.http
dev/api/responses/search-all.json
tests/Api/SearchApiTest.php

index 436d66d598e8f2fb86fc27622f9016900b2a7c8e..3c94d96ee600789ce212296b1ba31bb49059f8cb 100644 (file)
@@ -2,7 +2,9 @@
 
 namespace BookStack\Api;
 
+use BookStack\Entities\Models\BookChild;
 use BookStack\Entities\Models\Entity;
+use BookStack\Entities\Models\Page;
 
 class ApiEntityListFormatter
 {
@@ -20,8 +22,16 @@ 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)
@@ -62,6 +72,28 @@ class ApiEntityListFormatter
         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[]
index d1619e118672f7c0d217c4f4519ec6cbbe3458de..79cd8cfabd0e0166d082eb4c8eb8cc5e669f70d5 100644 (file)
@@ -9,21 +9,18 @@ use Illuminate\Http\Request;
 
 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
+    ) {
     }
 
     /**
@@ -50,16 +47,16 @@ class SearchApiController extends ApiController
         $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'],
         ]);
     }
index ee522381641827e6dc20277f3d3a3c7177182395..f9c17fa169638c6904e2cccd8c076bc34d32f5ce 100644 (file)
@@ -1 +1 @@
-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
index 2c7584e3fd0c5adf859b4fa697303ccff33cfc42..2ad89641693a1b71e4ba61ed2ba8aafb16b474ae 100644 (file)
@@ -8,7 +8,12 @@
       "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."
@@ -64,4 +89,4 @@
     }
   ],
   "total": 3
-}
\ No newline at end of file
+}
index 2a186e8d6328c4133b86eeb643d1436f40d5b78b..9da7900ca9a433b776952f79a2d4c9e22c80bccc 100644 (file)
@@ -13,7 +13,7 @@ class SearchApiTest extends TestCase
 {
     use TestsApi;
 
-    protected $baseEndpoint = '/api/search';
+    protected string $baseEndpoint = '/api/search';
 
     public function test_all_endpoint_returns_search_filtered_results_with_query()
     {
@@ -45,7 +45,7 @@ class SearchApiTest extends TestCase
         $resp = $this->actingAsApiAdmin()->getJson($this->baseEndpoint . '?query=superuniquevalue');
         $resp->assertJsonFragment([
             'type' => 'page',
-            'url'  => $page->getUrl(),
+            'url' => $page->getUrl(),
         ]);
     }
 
@@ -57,10 +57,10 @@ class SearchApiTest extends TestCase
 
         $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',
             ],
         ]);
@@ -74,4 +74,46 @@ class SearchApiTest extends TestCase
         $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');
+    }
 }