mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-05-05 08:40:11 +00:00
Filled out base Book API endpoints, added example responses
This commit is contained in:
parent
a8595d8aaf
commit
04a8614136
12 changed files with 284 additions and 20 deletions
app
Auth
Entities
Http/Controllers/Api
Uploads
dev/api/responses
routes
tests
|
@ -47,7 +47,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||||
* The attributes excluded from the model's JSON form.
|
* The attributes excluded from the model's JSON form.
|
||||||
* @var array
|
* @var array
|
||||||
*/
|
*/
|
||||||
protected $hidden = ['password', 'remember_token'];
|
protected $hidden = ['password', 'remember_token', 'system_name', 'email_confirmed', 'external_auth_id', 'email'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This holds the user's permissions when loaded.
|
* This holds the user's permissions when loaded.
|
||||||
|
|
|
@ -18,7 +18,8 @@ class Book extends Entity implements HasCoverImage
|
||||||
{
|
{
|
||||||
public $searchFactor = 2;
|
public $searchFactor = 2;
|
||||||
|
|
||||||
protected $fillable = ['name', 'description', 'image_id'];
|
protected $fillable = ['name', 'description'];
|
||||||
|
protected $hidden = ['restricted'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the url for this book.
|
* Get the url for this book.
|
||||||
|
|
|
@ -8,6 +8,8 @@ use Illuminate\Http\JsonResponse;
|
||||||
class ApiController extends Controller
|
class ApiController extends Controller
|
||||||
{
|
{
|
||||||
|
|
||||||
|
protected $rules = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provide a paginated listing JSON response in a standard format
|
* Provide a paginated listing JSON response in a standard format
|
||||||
* taking into account any pagination parameters passed by the user.
|
* taking into account any pagination parameters passed by the user.
|
||||||
|
@ -17,4 +19,12 @@ class ApiController extends Controller
|
||||||
$listing = new ListingResponseBuilder($query, request(), $fields);
|
$listing = new ListingResponseBuilder($query, request(), $fields);
|
||||||
return $listing->toResponse();
|
return $listing->toResponse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules for this controller.
|
||||||
|
*/
|
||||||
|
public function getValdationRules(): array
|
||||||
|
{
|
||||||
|
return $this->rules;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,47 +1,99 @@
|
||||||
<?php namespace BookStack\Http\Controllers\Api;
|
<?php namespace BookStack\Http\Controllers\Api;
|
||||||
|
|
||||||
use BookStack\Entities\Book;
|
use BookStack\Entities\Book;
|
||||||
|
use BookStack\Entities\Repos\BookRepo;
|
||||||
|
use BookStack\Facades\Activity;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
class BooksApiController extends ApiController
|
class BooksApiController extends ApiController
|
||||||
{
|
{
|
||||||
public $validation = [
|
|
||||||
|
protected $bookRepo;
|
||||||
|
|
||||||
|
protected $rules = [
|
||||||
'create' => [
|
'create' => [
|
||||||
// TODO
|
'name' => 'required|string|max:255',
|
||||||
|
'description' => 'string|max:1000',
|
||||||
],
|
],
|
||||||
'update' => [
|
'update' => [
|
||||||
// TODO
|
'name' => 'string|min:1|max:255',
|
||||||
|
'description' => 'string|max:1000',
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BooksApiController constructor.
|
||||||
|
*/
|
||||||
|
public function __construct(BookRepo $bookRepo)
|
||||||
|
{
|
||||||
|
$this->bookRepo = $bookRepo;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a listing of books visible to the user.
|
* Get a listing of books visible to the user.
|
||||||
|
* @api listing
|
||||||
*/
|
*/
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
$books = Book::visible();
|
$books = Book::visible();
|
||||||
return $this->apiListingResponse($books, [
|
return $this->apiListingResponse($books, [
|
||||||
'id', 'name', 'slug', 'description', 'created_at', 'updated_at', 'created_by', 'updated_by',
|
'id', 'name', 'slug', 'description', 'created_at', 'updated_at', 'created_by', 'updated_by', 'image_id',
|
||||||
'restricted', 'image_id',
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function create()
|
/**
|
||||||
|
* Create a new book.
|
||||||
|
* @throws \Illuminate\Validation\ValidationException
|
||||||
|
*/
|
||||||
|
public function create(Request $request)
|
||||||
{
|
{
|
||||||
// TODO -
|
$this->checkPermission('book-create-all');
|
||||||
|
$requestData = $this->validate($request, $this->rules['create']);
|
||||||
|
|
||||||
|
$book = $this->bookRepo->create($requestData);
|
||||||
|
Activity::add($book, 'book_create', $book->id);
|
||||||
|
|
||||||
|
return response()->json($book);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function read()
|
/**
|
||||||
|
* View the details of a single book.
|
||||||
|
*/
|
||||||
|
public function read(string $id)
|
||||||
{
|
{
|
||||||
// TODO -
|
$book = Book::visible()->with(['tags', 'cover', 'createdBy', 'updatedBy'])->findOrFail($id);
|
||||||
|
return response()->json($book);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update()
|
/**
|
||||||
|
* Update the details of a single book.
|
||||||
|
* @throws \Illuminate\Validation\ValidationException
|
||||||
|
*/
|
||||||
|
public function update(Request $request, string $id)
|
||||||
{
|
{
|
||||||
// TODO -
|
$book = Book::visible()->findOrFail($id);
|
||||||
|
$this->checkOwnablePermission('book-update', $book);
|
||||||
|
|
||||||
|
$requestData = $this->validate($request, $this->rules['update']);
|
||||||
|
$book = $this->bookRepo->update($book, $requestData);
|
||||||
|
Activity::add($book, 'book_update', $book->id);
|
||||||
|
|
||||||
|
return response()->json($book);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete()
|
/**
|
||||||
|
* Delete a book from the system.
|
||||||
|
* @throws \BookStack\Exceptions\NotifyException
|
||||||
|
* @throws \Illuminate\Contracts\Container\BindingResolutionException
|
||||||
|
*/
|
||||||
|
public function delete(string $id)
|
||||||
{
|
{
|
||||||
// TODO -
|
$book = Book::visible()->findOrFail($id);
|
||||||
|
$this->checkOwnablePermission('book-delete', $book);
|
||||||
|
|
||||||
|
$this->bookRepo->destroy($book);
|
||||||
|
Activity::addMessage('book_delete', $book->name);
|
||||||
|
|
||||||
|
return response('', 204);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -8,6 +8,7 @@ class Image extends Ownable
|
||||||
{
|
{
|
||||||
|
|
||||||
protected $fillable = ['name'];
|
protected $fillable = ['name'];
|
||||||
|
protected $hidden = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a thumbnail for this image.
|
* Get a thumbnail for this image.
|
||||||
|
|
10
dev/api/responses/books-create.json
Normal file
10
dev/api/responses/books-create.json
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"name": "My new book",
|
||||||
|
"description": "This is a book created via the API",
|
||||||
|
"created_by": 1,
|
||||||
|
"updated_by": 1,
|
||||||
|
"slug": "my-new-book",
|
||||||
|
"updated_at": "2020-01-12 14:05:11",
|
||||||
|
"created_at": "2020-01-12 14:05:11",
|
||||||
|
"id": 15
|
||||||
|
}
|
27
dev/api/responses/books-index.json
Normal file
27
dev/api/responses/books-index.json
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "BookStack User Guide",
|
||||||
|
"slug": "bookstack-user-guide",
|
||||||
|
"description": "This is a general guide on using BookStack on a day-to-day basis.",
|
||||||
|
"created_at": "2019-05-05 21:48:46",
|
||||||
|
"updated_at": "2019-12-11 20:57:31",
|
||||||
|
"created_by": 1,
|
||||||
|
"updated_by": 1,
|
||||||
|
"image_id": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"name": "Inventore inventore quia voluptatem.",
|
||||||
|
"slug": "inventore-inventore-quia-voluptatem",
|
||||||
|
"description": "Veniam nihil voluptas enim laborum corporis quos sint. Ab rerum voluptas ut iste voluptas magni quibusdam ut. Amet omnis enim voluptate neque facilis.",
|
||||||
|
"created_at": "2019-05-05 22:10:14",
|
||||||
|
"updated_at": "2019-12-11 20:57:23",
|
||||||
|
"created_by": 4,
|
||||||
|
"updated_by": 3,
|
||||||
|
"image_id": 34
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 14
|
||||||
|
}
|
47
dev/api/responses/books-read.json
Normal file
47
dev/api/responses/books-read.json
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
{
|
||||||
|
"id": 16,
|
||||||
|
"name": "My own book",
|
||||||
|
"slug": "my-own-book",
|
||||||
|
"description": "This is my own little book",
|
||||||
|
"created_at": "2020-01-12 14:09:59",
|
||||||
|
"updated_at": "2020-01-12 14:11:51",
|
||||||
|
"created_by": {
|
||||||
|
"id": 1,
|
||||||
|
"name": "Admin",
|
||||||
|
"created_at": "2019-05-05 21:15:13",
|
||||||
|
"updated_at": "2019-12-16 12:18:37",
|
||||||
|
"image_id": 48
|
||||||
|
},
|
||||||
|
"updated_by": {
|
||||||
|
"id": 1,
|
||||||
|
"name": "Admin",
|
||||||
|
"created_at": "2019-05-05 21:15:13",
|
||||||
|
"updated_at": "2019-12-16 12:18:37",
|
||||||
|
"image_id": 48
|
||||||
|
},
|
||||||
|
"image_id": 452,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"id": 13,
|
||||||
|
"entity_id": 16,
|
||||||
|
"entity_type": "BookStack\\Book",
|
||||||
|
"name": "Category",
|
||||||
|
"value": "Guide",
|
||||||
|
"order": 0,
|
||||||
|
"created_at": "2020-01-12 14:11:51",
|
||||||
|
"updated_at": "2020-01-12 14:11:51"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cover": {
|
||||||
|
"id": 452,
|
||||||
|
"name": "sjovall_m117hUWMu40.jpg",
|
||||||
|
"url": "http:\/\/bookstack.local\/uploads\/images\/cover_book\/2020-01\/sjovall_m117hUWMu40.jpg",
|
||||||
|
"created_at": "2020-01-12 14:11:51",
|
||||||
|
"updated_at": "2020-01-12 14:11:51",
|
||||||
|
"created_by": 1,
|
||||||
|
"updated_by": 1,
|
||||||
|
"path": "\/uploads\/images\/cover_book\/2020-01\/sjovall_m117hUWMu40.jpg",
|
||||||
|
"type": "cover_book",
|
||||||
|
"uploaded_to": 16
|
||||||
|
}
|
||||||
|
}
|
11
dev/api/responses/books-update.json
Normal file
11
dev/api/responses/books-update.json
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"id": 16,
|
||||||
|
"name": "My own book",
|
||||||
|
"slug": "my-own-book",
|
||||||
|
"description": "This is my own little book - updated",
|
||||||
|
"created_at": "2020-01-12 14:09:59",
|
||||||
|
"updated_at": "2020-01-12 14:16:10",
|
||||||
|
"created_by": 1,
|
||||||
|
"updated_by": 1,
|
||||||
|
"image_id": 452
|
||||||
|
}
|
|
@ -2,11 +2,12 @@
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Routes for the BookStack API.
|
* Routes for the BookStack API.
|
||||||
*
|
|
||||||
* Routes have a uri prefix of /api/.
|
* Routes have a uri prefix of /api/.
|
||||||
|
* Controllers are all within app/Http/Controllers/Api
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
Route::get('books', 'BooksApiController@index');
|
||||||
// TODO - Authenticate middleware
|
Route::post('books', 'BooksApiController@create');
|
||||||
|
Route::get('books/{id}', 'BooksApiController@read');
|
||||||
Route::get('books', 'BooksApiController@index');
|
Route::put('books/{id}', 'BooksApiController@update');
|
||||||
|
Route::delete('books/{id}', 'BooksApiController@delete');
|
||||||
|
|
87
tests/Api/BooksApiTest.php
Normal file
87
tests/Api/BooksApiTest.php
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
<?php namespace Tests;
|
||||||
|
|
||||||
|
use BookStack\Entities\Book;
|
||||||
|
|
||||||
|
class ApiAuthTest extends TestCase
|
||||||
|
{
|
||||||
|
use TestsApi;
|
||||||
|
|
||||||
|
protected $baseEndpoint = '/api/books';
|
||||||
|
|
||||||
|
public function test_index_endpoint_returns_expected_book()
|
||||||
|
{
|
||||||
|
$this->actingAsApiEditor();
|
||||||
|
$firstBook = Book::query()->orderBy('id', 'asc')->first();
|
||||||
|
|
||||||
|
$resp = $this->getJson($this->baseEndpoint . '?count=1&sort=+id');
|
||||||
|
$resp->assertJson(['data' => [
|
||||||
|
[
|
||||||
|
'id' => $firstBook->id,
|
||||||
|
'name' => $firstBook->name,
|
||||||
|
'slug' => $firstBook->slug,
|
||||||
|
]
|
||||||
|
]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_create_endpoint()
|
||||||
|
{
|
||||||
|
$this->actingAsApiEditor();
|
||||||
|
$details = [
|
||||||
|
'name' => 'My API book',
|
||||||
|
'description' => 'A book created via the API',
|
||||||
|
];
|
||||||
|
|
||||||
|
$resp = $this->postJson($this->baseEndpoint, $details);
|
||||||
|
$resp->assertStatus(200);
|
||||||
|
$newItem = Book::query()->orderByDesc('id')->where('name', '=', $details['name'])->first();
|
||||||
|
$resp->assertJson(array_merge($details, ['id' => $newItem->id, 'slug' => $newItem->slug]));
|
||||||
|
$this->assertActivityExists('book_create', $newItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_read_endpoint()
|
||||||
|
{
|
||||||
|
$this->actingAsApiEditor();
|
||||||
|
$book = Book::visible()->first();
|
||||||
|
|
||||||
|
$resp = $this->getJson($this->baseEndpoint . "/{$book->id}");
|
||||||
|
|
||||||
|
$resp->assertStatus(200);
|
||||||
|
$resp->assertJson([
|
||||||
|
'id' => $book->id,
|
||||||
|
'slug' => $book->slug,
|
||||||
|
'created_by' => [
|
||||||
|
'name' => $book->createdBy->name,
|
||||||
|
],
|
||||||
|
'updated_by' => [
|
||||||
|
'name' => $book->createdBy->name,
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_update_endpoint()
|
||||||
|
{
|
||||||
|
$this->actingAsApiEditor();
|
||||||
|
$book = Book::visible()->first();
|
||||||
|
$details = [
|
||||||
|
'name' => 'My updated API book',
|
||||||
|
'description' => 'A book created via the API',
|
||||||
|
];
|
||||||
|
|
||||||
|
$resp = $this->putJson($this->baseEndpoint . "/{$book->id}", $details);
|
||||||
|
$book->refresh();
|
||||||
|
|
||||||
|
$resp->assertStatus(200);
|
||||||
|
$resp->assertJson(array_merge($details, ['id' => $book->id, 'slug' => $book->slug]));
|
||||||
|
$this->assertActivityExists('book_update', $book);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_delete_endpoint()
|
||||||
|
{
|
||||||
|
$this->actingAsApiEditor();
|
||||||
|
$book = Book::visible()->first();
|
||||||
|
$resp = $this->deleteJson($this->baseEndpoint . "/{$book->id}");
|
||||||
|
|
||||||
|
$resp->assertStatus(204);
|
||||||
|
$this->assertActivityExists('book_delete');
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
<?php namespace Tests;
|
<?php namespace Tests;
|
||||||
|
|
||||||
|
use BookStack\Entities\Entity;
|
||||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||||
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
||||||
|
|
||||||
|
@ -60,4 +61,20 @@ abstract class TestCase extends BaseTestCase
|
||||||
{
|
{
|
||||||
return TestResponse::fromBaseResponse($response);
|
return TestResponse::fromBaseResponse($response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert that an activity entry exists of the given key.
|
||||||
|
* Checks the activity belongs to the given entity if provided.
|
||||||
|
*/
|
||||||
|
protected function assertActivityExists(string $key, Entity $entity = null)
|
||||||
|
{
|
||||||
|
$detailsToCheck = ['key' => $key];
|
||||||
|
|
||||||
|
if ($entity) {
|
||||||
|
$detailsToCheck['entity_type'] = $entity->getMorphClass();
|
||||||
|
$detailsToCheck['entity_id'] = $entity->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('activities', $detailsToCheck);
|
||||||
|
}
|
||||||
}
|
}
|
Loading…
Add table
Add a link
Reference in a new issue