]> BookStack Code Mirror - bookstack/commitdiff
ZIP Imports: Added API examples, finished testing 5721/head
authorDan Brown <redacted>
Fri, 18 Jul 2025 15:19:14 +0000 (16:19 +0100)
committerDan Brown <redacted>
Fri, 18 Jul 2025 15:19:14 +0000 (16:19 +0100)
Also updated some types on a couple of controllers.

13 files changed:
app/Entities/Controllers/ChapterApiController.php
app/Entities/Controllers/PageApiController.php
app/Exports/Controllers/ImportApiController.php
app/Permissions/ContentPermissionApiController.php
app/Search/SearchApiController.php
app/Users/Controllers/RoleApiController.php
dev/api/requests/imports-run.json [new file with mode: 0644]
dev/api/responses/imports-create.json [new file with mode: 0644]
dev/api/responses/imports-list.json [new file with mode: 0644]
dev/api/responses/imports-read.json [new file with mode: 0644]
dev/api/responses/imports-run.json [new file with mode: 0644]
routes/api.php
tests/Api/ImportsApiTest.php

index 430654330f36b97d5ec16dbaf22c12a56b337738..8ac0c7a60a22a41c4e96864553ba3c7088a88fcf 100644 (file)
@@ -9,12 +9,11 @@ use BookStack\Entities\Repos\ChapterRepo;
 use BookStack\Exceptions\PermissionsException;
 use BookStack\Http\ApiController;
 use Exception;
-use Illuminate\Database\Eloquent\Relations\HasMany;
 use Illuminate\Http\Request;
 
 class ChapterApiController extends ApiController
 {
-    protected $rules = [
+    protected array $rules = [
         'create' => [
             'book_id'             => ['required', 'integer'],
             'name'                => ['required', 'string', 'max:255'],
index 40598e2098375afc564f35a371eb22048ef50925..8fcba3dc6a1563e54075c6b9057831e4a16a39d5 100644 (file)
@@ -12,7 +12,7 @@ use Illuminate\Http\Request;
 
 class PageApiController extends ApiController
 {
-    protected $rules = [
+    protected array $rules = [
         'create' => [
             'book_id'    => ['required_without:chapter_id', 'integer'],
             'chapter_id' => ['required_without:book_id', 'integer'],
index 0749ff9330ac8f60cdc992e7a6eb8576c29cd663..cac155c7c1b13576e90be3769ec2a8465b5cead6 100644 (file)
@@ -23,6 +23,7 @@ class ImportApiController extends ApiController
 
     /**
      * List existing ZIP imports visible to the user.
+     * Requires permission to import content.
      */
     public function list(): JsonResponse
     {
@@ -34,12 +35,18 @@ class ImportApiController extends ApiController
     }
 
     /**
-     * Upload, validate and store a ZIP import file.
-     * This does not run the import. That is performed via a separate endpoint.
+     * Start a new import from a ZIP file.
+     * This does not actually run the import since that is performed via the "run" endpoint.
+     * This uploads, validates and stores the ZIP file so it's ready to be imported.
+     *
+     * This "file" parameter must be a BookStack-compatible ZIP file, and this must be
+     * sent via a 'multipart/form-data' type request.
+     *
+     * Requires permission to import content.
      */
-    public function upload(Request $request): JsonResponse
+    public function create(Request $request): JsonResponse
     {
-        $this->validate($request, $this->rules()['upload']);
+        $this->validate($request, $this->rules()['create']);
 
         $file = $request->file('file');
 
@@ -57,6 +64,7 @@ class ImportApiController extends ApiController
      * Read details of a pending ZIP import.
      * The "details" property contains high-level metadata regarding the ZIP import content,
      * and the structure of this will change depending on import "type".
+     * Requires permission to import content.
      */
     public function read(int $id): JsonResponse
     {
@@ -69,8 +77,9 @@ class ImportApiController extends ApiController
 
     /**
      * Run the import process for an uploaded ZIP import.
-     * The parent_id and parent_type parameters are required when the import type is 'chapter' or 'page'.
-     * On success, returns the imported item.
+     * The "parent_id" and "parent_type" parameters are required when the import type is "chapter" or "page".
+     * On success, this endpoint returns the imported item.
+     * Requires permission to import content.
      */
     public function run(int $id, Request $request): JsonResponse
     {
@@ -92,11 +101,12 @@ class ImportApiController extends ApiController
             return $this->jsonError($message);
         }
 
-        return response()->json($entity);
+        return response()->json($entity->withoutRelations());
     }
 
     /**
-     * Delete a pending ZIP import.
+     * Delete a pending ZIP import from the system.
+     * Requires permission to import content.
      */
     public function delete(int $id): Response
     {
@@ -109,7 +119,7 @@ class ImportApiController extends ApiController
     protected function rules(): array
     {
         return [
-            'upload' => [
+            'create' => [
                 'file' => ['required', ...AttachmentService::getFileValidationRules()],
             ],
             'run' => [
index 23b75db359a469962779c0d3cffffbdcd040ad1a..bddbc2c7d95ee6b80c530410c137544ee5453552 100644 (file)
@@ -16,7 +16,7 @@ class ContentPermissionApiController extends ApiController
     ) {
     }
 
-    protected $rules = [
+    protected array $rules = [
         'update' => [
             'owner_id'  => ['int'],
 
index 79cd8cfabd0e0166d082eb4c8eb8cc5e669f70d5..cd4a14a3931c2869d654c53c710c0af51c7890bf 100644 (file)
@@ -9,7 +9,7 @@ use Illuminate\Http\Request;
 
 class SearchApiController extends ApiController
 {
-    protected $rules = [
+    protected array $rules = [
         'all' => [
             'query' => ['required'],
             'page'  => ['integer', 'min:1'],
index 2e96602faae181c7d9b95ac4c5ecdb70fb3686ce..2f3638cd3e2777731cf19a551a46f42dc2855aa1 100644 (file)
@@ -16,7 +16,7 @@ class RoleApiController extends ApiController
         'display_name', 'description', 'mfa_enforced', 'external_auth_id', 'created_at', 'updated_at',
     ];
 
-    protected $rules = [
+    protected array $rules = [
         'create' => [
             'display_name'  => ['required', 'string', 'min:3', 'max:180'],
             'description'   => ['string', 'max:180'],
diff --git a/dev/api/requests/imports-run.json b/dev/api/requests/imports-run.json
new file mode 100644 (file)
index 0000000..836a66f
--- /dev/null
@@ -0,0 +1,4 @@
+{
+  "parent_type": "book",
+  "parent_id": 28
+}
\ No newline at end of file
diff --git a/dev/api/responses/imports-create.json b/dev/api/responses/imports-create.json
new file mode 100644 (file)
index 0000000..9977587
--- /dev/null
@@ -0,0 +1,10 @@
+{
+  "type": "chapter",
+  "name": "Pension Providers",
+  "created_by": 1,
+  "size": 2757,
+  "path": "uploads\/files\/imports\/ghnxmS3u9QxLWu82.zip",
+  "updated_at": "2025-07-18T14:50:27.000000Z",
+  "created_at": "2025-07-18T14:50:27.000000Z",
+  "id": 31
+}
\ No newline at end of file
diff --git a/dev/api/responses/imports-list.json b/dev/api/responses/imports-list.json
new file mode 100644 (file)
index 0000000..7451e44
--- /dev/null
@@ -0,0 +1,23 @@
+{
+  "data": [
+    {
+      "id": 25,
+      "name": "IT Department",
+      "size": 618462,
+      "type": "book",
+      "created_by": 1,
+      "created_at": "2024-12-20T18:40:38.000000Z",
+      "updated_at": "2024-12-20T18:40:38.000000Z"
+    },
+    {
+      "id": 27,
+      "name": "Clients",
+      "size": 15364,
+      "type": "chapter",
+      "created_by": 1,
+      "created_at": "2025-03-20T12:41:44.000000Z",
+      "updated_at": "2025-03-20T12:41:44.000000Z"
+    }
+  ],
+  "total": 2
+}
\ No newline at end of file
diff --git a/dev/api/responses/imports-read.json b/dev/api/responses/imports-read.json
new file mode 100644 (file)
index 0000000..e256854
--- /dev/null
@@ -0,0 +1,51 @@
+{
+  "id": 25,
+  "name": "IT Department",
+  "path": "uploads\/files\/imports\/7YOpZ6sGIEbYdRFL.zip",
+  "size": 618462,
+  "type": "book",
+  "created_by": 1,
+  "created_at": "2024-12-20T18:40:38.000000Z",
+  "updated_at": "2024-12-20T18:40:38.000000Z",
+  "details": {
+    "id": 4,
+    "name": "IT Department",
+    "chapters": [
+      {
+        "id": 3,
+        "name": "Server Systems",
+        "priority": 1,
+        "pages": [
+          {
+            "id": 22,
+            "name": "prod-aws-stonehawk",
+            "priority": 0,
+            "attachments": [],
+            "images": [],
+            "tags": []
+          }
+        ],
+        "tags": []
+      }
+    ],
+    "pages": [
+      {
+        "id": 23,
+        "name": "Member Onboarding Guide",
+        "priority": 0,
+        "attachments": [],
+        "images": [],
+        "tags": []
+      },
+      {
+        "id": 25,
+        "name": "IT Holiday Party Event",
+        "priority": 2,
+        "attachments": [],
+        "images": [],
+        "tags": []
+      }
+    ],
+    "tags": []
+  }
+}
\ No newline at end of file
diff --git a/dev/api/responses/imports-run.json b/dev/api/responses/imports-run.json
new file mode 100644 (file)
index 0000000..90b34d6
--- /dev/null
@@ -0,0 +1,14 @@
+{
+  "id": 1067,
+  "book_id": 28,
+  "slug": "pension-providers",
+  "name": "Pension Providers",
+  "description": "Details on the various pension providers that are available",
+  "priority": 7,
+  "created_at": "2025-07-18T14:53:35.000000Z",
+  "updated_at": "2025-07-18T14:53:36.000000Z",
+  "created_by": 1,
+  "updated_by": 1,
+  "owned_by": 1,
+  "default_template_id": null
+}
\ No newline at end of file
index 98af4bb2616e4b3481d2159fbeb2b1f9727efdc3..99df24aed0a67ced8a61e72eef38e53bcd213d1a 100644 (file)
@@ -89,7 +89,7 @@ Route::put('roles/{id}', [RoleApiController::class, 'update']);
 Route::delete('roles/{id}', [RoleApiController::class, 'delete']);
 
 Route::get('imports', [ExportControllers\ImportApiController::class, 'list']);
-Route::post('imports', [ExportControllers\ImportApiController::class, 'upload']);
+Route::post('imports', [ExportControllers\ImportApiController::class, 'create']);
 Route::get('imports/{id}', [ExportControllers\ImportApiController::class, 'read']);
 Route::post('imports/{id}', [ExportControllers\ImportApiController::class, 'run']);
 Route::delete('imports/{id}', [ExportControllers\ImportApiController::class, 'delete']);
index 523034324fc5c5ba4ba8b7f8ca1ee7cd2caf5d0c..f6df074ee388a33445f471dc2b59344286b0f772 100644 (file)
@@ -14,7 +14,7 @@ class ImportsApiTest extends TestCase
 
     protected string $baseEndpoint = '/api/imports';
 
-    public function test_upload_and_run(): void
+    public function test_create_and_run(): void
     {
         $book = $this->entities->book();
         $zip = ZipTestHelper::zipUploadFromData([
@@ -44,12 +44,13 @@ class ImportsApiTest extends TestCase
             'name' => 'My API import page',
             'book_id' => $book->id,
         ]);
+        $resp->assertJsonMissingPath('book');
 
         $page = Page::query()->where('name', '=', 'My API import page')->first();
         $this->assertEquals('My api tag', $page->tags()->first()->name);
     }
 
-    public function test_upload_validation_error(): void
+    public function test_create_validation_error(): void
     {
         $zip = ZipTestHelper::zipUploadFromData([
             'page' => [