├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── composer.json
├── composer.lock
├── phpunit.xml
├── src
├── Exceptions
│ ├── ApiException.php
│ ├── BadRequestException.php
│ ├── Handler.php
│ ├── InvalidAttributeException.php
│ ├── NotAcceptableException.php
│ ├── UnauthorizedHttpException.php
│ └── UnsupportedMediaTypeException.php
├── Helpers
│ ├── Api.php
│ ├── ApiController.php
│ ├── ApiObjects.php
│ └── ApiValidation.php
├── Http
│ ├── Middleware
│ │ ├── Auth
│ │ │ └── JsonApiAuthBasicMiddleware.php
│ │ └── JsonApiMiddleware.php
│ └── Responses
│ │ └── ApiResponse.php
├── Providers
│ ├── .gitkeep
│ └── GenericServiceProvider.php
├── Traits
│ ├── ControllerTrait.php
│ ├── ModelTrait.php
│ └── SearchableTrait.php
├── Transformers
│ ├── ApiTransformer.php
│ └── KeysTransformer.php
├── config
│ └── jsonapi.php
└── lang
│ └── en
│ └── errors.php
└── tests
├── AcceptanceTestCase.php
├── AcceptanceTests
├── AuthTest.php
├── CrudTest.php
├── HeadersTest.php
├── IncludesTest.php
├── JsonTest.php
├── ListTest.php
├── PatchTest.php
└── PostTest.php
├── App
├── Database
│ ├── .gitignore
│ └── Migrations
│ │ ├── .gitkeep
│ │ ├── 2014_10_12_000000_create_users_table.php
│ │ ├── 2016_03_09_190517_create_profiles_table.php
│ │ └── 2016_03_09_223910_create_profiles_lookup_table.php
├── Http
│ ├── Controllers
│ │ ├── ProfileController.php
│ │ └── UserController.php
│ └── routes.php
├── Profiles.php
├── ProfilesAlt.php
├── Providers
│ └── RouteServiceProvider.php
├── User.php
└── UserAlt.php
├── BaseTestCase.php
├── IntegrationTestCase.php
├── IntegrationTests
├── CrudTest.php
└── SearchTest.php
├── Main.json.postman_collection
├── SeeOrSaveJsonStructure.php
├── UnitTestCase.php
└── UnitTests
└── ExceptionTest.php
/.gitignore:
--------------------------------------------------------------------------------
1 | vendor/
2 | node_modules/
3 |
4 | # Laravel 4 specific
5 | bootstrap/compiled.php
6 | app/storage/
7 |
8 | # Laravel 5 & Lumen specific
9 | bootstrap/cache/
10 | storage/
11 | .env.*.php
12 | .env.php
13 | .env
14 | .env.example
15 | logs
16 | build
17 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 | php:
3 | - "5.6"
4 | - "7.0"
5 | - "hhvm"
6 |
7 | before_script:
8 | - curl -sS https://getcomposer.org/installer | php -- --filename=composer
9 | - chmod +x composer
10 | - composer install -n
11 |
12 | script:
13 | - php vendor/bin/phpunit
14 |
15 | after_script:
16 | - php vendor/bin/codacycoverage clover build/logs/clover.xml
17 |
18 | matrix:
19 | allow_failures:
20 | - php: "hhvm"
21 | branches:
22 | only:
23 | - master
24 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Askedio
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | A really simple package that provides a CRUD JSON API for your Laravel 5 application.
4 |
5 | [](https://travis-ci.org/Askedio/laravel-Cruddy)
6 | [](https://styleci.io/repos/52752552)
7 | [](https://codeclimate.com/github/Askedio/laravel-Cruddy)
8 | [](https://www.codacy.com/app/gcphost/laravel-Cruddy)
9 | [](https://www.codacy.com/app/gcphost/laravel-Cruddy)
10 |
11 | * [Live Demo](https://cruddy.io/app/).
12 | * [Laravel 5.2 Example Package](https://github.com/Askedio/Laravel-5-CRUD-Example).
13 | * Plays well with [jQuery CRUDdy](https://github.com/Askedio/jQuery-Cruddy).
14 |
15 |
16 |
17 | # Installation
18 | ### Composer: require
19 | ~~~
20 | composer require askedio/laravel-cruddy:dev-master
21 | ~~~
22 |
23 |
24 | ### Providers: config/app.php
25 | Add the Service Provider to your providers array.
26 | ~~~
27 | 'providers' => [
28 | Askedio\Laravel5ApiController\Providers\GenericServiceProvider::class,
29 | ...
30 | ~~~
31 |
32 |
33 |
34 |
35 | ### Model: app/User.php
36 | Add the traits to your Model to enable the Api and Search features. [More Details & Options.](https://github.com/Askedio/laravel-Cruddy/wiki/Models)
37 | ~~~
38 | class User extends Authenticatable
39 | {
40 |
41 | use \Askedio\Laravel5ApiController\Traits\ModelTrait;
42 | use \Askedio\Laravel5ApiController\Traits\SearchableTrait;
43 | ...
44 | ~~~
45 |
46 |
47 |
48 |
49 | ### Controller: app/Http/Controllers/Api/UserController.php
50 | Create a new controller for your API. [More Details & Options](https://github.com/Askedio/laravel-Cruddy/wiki/Controllers).
51 | ~~~
52 | 'api', 'middleware' => ['api', 'jsonapi']], function()
69 | {
70 | Route::resource('user', 'Api\UserController');
71 | });
72 | ~~~
73 |
74 |
75 |
76 |
77 |
78 |
79 | # Usage
80 | Consume the API using Laravels resource routes, GET, PATCH, POST and DELETE. [More Details & Options](https://github.com/Askedio/laravel-Cruddy/wiki/Usage).
81 |
82 | ### Example
83 | ~~~
84 | GET /api/user/1
85 | ~~~
86 |
87 | ~~~
88 | HTTP/1.1 200 OK
89 | Content-Type: application/vnd.api+json
90 |
91 | {
92 | "data": {
93 | "type": "users",
94 | "id": 1,
95 | "attributes": {
96 | "id": 1,
97 | "name": "Test User",
98 | "email": "test@test.com"
99 | }
100 | },
101 | "links": {
102 | "self": "/api/user/1"
103 | },
104 | "jsonapi": {
105 | "version": "1.0",
106 | "self": "v1"
107 | }
108 | }
109 | ~~~
110 |
111 |
112 |
113 |
114 |
115 | # Comments
116 | My goal is a plug-n-play json api for Laravel. You shouldn't need to configure much of anything to enable the api on your models but if you still want advanced features like relations, searching, etc, you get that too.
117 |
118 | If you have any comments, opinions or can code review please reach me here or on twitter, [@asked_io](https://twitter.com/asked_io). You can also follow me on my website, [asked.io](https://asked.io).
119 |
120 |
121 | Thank you.
122 |
123 | -William
124 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "askedio/laravel-cruddy",
3 | "description": "A JSON API CRUD Package for Laravel 5",
4 | "keywords": ["laravel", "json", "jsonapi", "crud"],
5 | "license": "MIT",
6 | "type": "library",
7 | "require": {
8 | "php": ">=5.5.9",
9 | "laravel/framework": "5.2.*"
10 | },
11 | "require-dev": {
12 | "laravel/laravel": "5.*",
13 | "phpunit/phpunit": "4.*",
14 | "codacy/coverage": "dev-master"
15 | },
16 | "autoload": {
17 | "psr-4": {
18 | "Askedio\\Laravel5ApiController\\": "src/"
19 | }
20 | },
21 | "autoload-dev": {
22 | "psr-4": {
23 | "Askedio\\Tests\\" : "tests"
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
15 |
16 |
17 | ./tests/
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | ./src/
30 |
31 | ./vendor/
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/src/Exceptions/ApiException.php:
--------------------------------------------------------------------------------
1 | getDetails($this->error);
33 | }
34 |
35 | /**
36 | * Get the status.
37 | *
38 | * @return int
39 | */
40 | public function getStatusCode()
41 | {
42 | return (int) $this->status;
43 | }
44 |
45 | /**
46 | * Store exception details.
47 | *
48 | * @param mixed $details
49 | */
50 | public function withDetails($details)
51 | {
52 | $this->exceptionDetails = $details;
53 |
54 | return $this;
55 | }
56 |
57 | /**
58 | * Store exception errors details.
59 | *
60 | * @param array $details
61 | */
62 | public function withErrors($errors)
63 | {
64 | $this->exceptionErrors = $errors;
65 |
66 | return $this;
67 | }
68 |
69 | /**
70 | * Build the error results.
71 | *
72 | * @return array
73 | */
74 | public function getDetails($template)
75 | {
76 | if ($this->exceptionErrors) {
77 | return $this->exceptionErrors;
78 | }
79 | $details = $this->exceptionDetails;
80 |
81 | /* Not pre-rendered errors, build from template */
82 | if (! is_array($details)) {
83 | $details = [$details];
84 | }
85 |
86 | return array_map(function ($detail) use ($template) {
87 | return $this->item($template, $detail);
88 | }, $details);
89 | }
90 |
91 | /**
92 | * Render the item.
93 | *
94 | * @return array
95 | */
96 | private function item($template, $detail)
97 | {
98 | $insert = $template;
99 | $replace = $template['detail'];
100 |
101 | $insert['detail'] = vsprintf($replace, $detail);
102 | if (isset($template['source'])) {
103 | $insert['source'] = [];
104 | $insert['source'][$template['source']['type']] = vsprintf($template['source']['value'], $detail);
105 | }
106 |
107 | return $insert;
108 | }
109 |
110 | /**
111 | * Build the Exception details from the custom exception class.
112 | *
113 | * @return void
114 | */
115 | protected function build(array $args)
116 | {
117 |
118 | /* Nothing to build if no type. */
119 | if (! isset($args[0])) {
120 | return false;
121 | }
122 |
123 | $settings = $this->settings($args);
124 | $this->error = $settings;
125 | $this->status = $settings['code'];
126 | }
127 |
128 | /**
129 | * Generate settings array from errors config.
130 | *
131 | * @return array
132 | */
133 | private function settings($args)
134 | {
135 | $base = [
136 | 'title' => '',
137 | 'detail' => '',
138 | 'code' => isset($args[1]) ? $args[1] : $this->status,
139 | ];
140 |
141 | $tpl = trans(sprintf('jsonapi::errors.%s', $args[0]));
142 |
143 | return array_merge($base, is_array($tpl) ? $tpl : []);
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/src/Exceptions/BadRequestException.php:
--------------------------------------------------------------------------------
1 | build(func_get_args());
18 |
19 | parent::__construct();
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Exceptions/Handler.php:
--------------------------------------------------------------------------------
1 | is(config('jsonapi.url'))) {
44 | return parent::render($request, $exception);
45 | }
46 |
47 | return $this->handle($request, $exception);
48 | }
49 |
50 | /**
51 | * Convert the Exception into a JSON HTTP Response.
52 | *
53 | * @param Request $request
54 | * @param Exception $exception
55 | *
56 | * @return ApiResponse
57 | */
58 | private function handle($code, Exception $exception)
59 | {
60 |
61 | /* custom exception class */
62 | if ($exception instanceof ApiException) {
63 | return response()->jsonapi($exception->getStatusCode(), ['errors' => $exception->getErrors()]);
64 | }
65 |
66 | /* not an exception we manage so generic error or if debug, the real exception */
67 | // TO-DO: Need a way to get coverage on something like.. if (!env('APP_DEBUG', false)) {
68 | $code = method_exists($exception, 'getStatusCode') ? $exception->getStatusCode() : 500;
69 | $detail = method_exists($exception, 'getMessage') ? $exception->getMessage() : 'Unknown Exception.';
70 | $data = array_filter([
71 | 'status' => $code,
72 | 'detail' => $detail,
73 | ]);
74 |
75 | return response()->jsonapi($code, ['errors' => $data]);
76 | // }
77 | // return parent::render($code, $exception);
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/Exceptions/InvalidAttributeException.php:
--------------------------------------------------------------------------------
1 | build(func_get_args());
18 |
19 | parent::__construct();
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Exceptions/NotAcceptableException.php:
--------------------------------------------------------------------------------
1 | build(func_get_args());
18 |
19 | parent::__construct();
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Exceptions/UnauthorizedHttpException.php:
--------------------------------------------------------------------------------
1 | build(func_get_args());
18 |
19 | parent::__construct();
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Exceptions/UnsupportedMediaTypeException.php:
--------------------------------------------------------------------------------
1 | build(func_get_args());
18 |
19 | parent::__construct();
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Helpers/Api.php:
--------------------------------------------------------------------------------
1 | version ?: config('jsonapi.version');
18 | }
19 |
20 | /**
21 | * Set version.
22 | *
23 | * @param string $version
24 | *
25 | * @return void
26 | */
27 | public function setVersion($version)
28 | {
29 | $this->version = $version;
30 | }
31 |
32 | /**
33 | * List of included options from input.
34 | *
35 | * @return Illuminate\Http\Request
36 | */
37 | public function includes()
38 | {
39 | return collect(request()->input('include') ? explode(',', request()->input('include')) : []);
40 | }
41 |
42 | /**
43 | * List of fields from input.
44 | *
45 | * @return array
46 | */
47 | public function fields()
48 | {
49 | $results = [];
50 | foreach (array_filter(request()->input('fields', [])) as $type => $members) {
51 | foreach (explode(',', $members) as $member) {
52 | $results[$type][] = $member;
53 | }
54 | }
55 |
56 | return collect($results);
57 | }
58 |
59 | public function jsonBody()
60 | {
61 | return collect(request()->json()->all())->get('data');
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/Helpers/ApiController.php:
--------------------------------------------------------------------------------
1 | model = new $parent->model();
21 | $this->object = $this->model;
22 |
23 | new ApiValidation($this->model->getObjects());
24 |
25 | $this->setAuth($parent);
26 | }
27 |
28 | private function setAuth($parent)
29 | {
30 | if ($parent->getAuth()) {
31 | $table = $this->model->getTable();
32 | $user = auth()->user();
33 | $this->object = $user->$table();
34 | }
35 | }
36 |
37 | /**
38 | * index.
39 | *
40 | * @return pagination class..
41 | */
42 | public function index()
43 | {
44 | $results = $this->object->setSort(request()->input('sort'));
45 |
46 | if (request()->input('search') && $this->model->isSearchable()) {
47 | $results->search(request()->input('search'));
48 | }
49 |
50 | return $results->paginate(request()->input('page.size', 10), ['*'], 'page', request()->input('page.number', 1)
51 | );
52 | }
53 |
54 | /**
55 | * Store.
56 | *
57 | * @return Illuminate\Database\Eloquent\Model
58 | */
59 | public function store()
60 | {
61 | $this->validate('create');
62 |
63 | return $this->object->create($this->getRequest());
64 | }
65 |
66 | /**
67 | * Show.
68 | *
69 | * @return Illuminate\Database\Eloquent\Model
70 | */
71 | public function show($idd)
72 | {
73 | return $this->object->find($idd);
74 | }
75 |
76 | /**
77 | * Update.
78 | *
79 | * @return Illuminate\Database\Eloquent\Model
80 | */
81 | public function update($idd)
82 | {
83 | $this->validate('update');
84 |
85 | if ($model = $this->object->find($idd)) {
86 | return $model->update($this->getRequest()) ? $model : false;
87 | }
88 |
89 | return false;
90 | }
91 |
92 | /**
93 | * Destroy.
94 | *
95 | * @return Illuminate\Database\Eloquent\Model
96 | */
97 | public function destroy($idd)
98 | {
99 | $model = $this->object->find($idd);
100 |
101 | return $model ? $model->delete() : false;
102 | }
103 |
104 | /**
105 | * Get request body data->attibutes.
106 | *
107 | * @return array
108 | */
109 | private function getRequest()
110 | {
111 | $requst = app('api')->jsonBody();
112 |
113 | return isset($requst['attributes']) ? $requst['attributes'] : [];
114 | }
115 |
116 | /**
117 | * Validate Form.
118 | *
119 | * @param string $action
120 | *
121 | * @return void
122 | */
123 | private function validate($action)
124 | {
125 | $validator = validator()->make($this->getRequest(), $this->model->getRule($action));
126 | $errors = [];
127 | foreach ($validator->errors()->toArray() as $field => $err) {
128 | array_push($errors, [
129 | //'code' => 0, # TO-DO: report valid json api error code base on validation error.
130 | 'source' => ['pointer' => $field],
131 | 'title' => trans('jsonapi::errors.invalid_attribute.title'),
132 | 'detail' => implode(' ', $err),
133 | ]);
134 | }
135 |
136 | if (! empty($errors)) {
137 | throw (new InvalidAttributeException('invalid_attribute', 403))->withErrors($errors);
138 | }
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/src/Helpers/ApiObjects.php:
--------------------------------------------------------------------------------
1 | baseObject = $object;
33 | $this->fillables = collect([]);
34 | $this->includes = collect([]);
35 | $this->columns = collect([]);
36 | $this->relations = collect($this->includes($object));
37 | }
38 |
39 | /**
40 | * Return a collection of all fillable items.
41 | *
42 | * @return collection
43 | */
44 | public function getFillables()
45 | {
46 | return $this->fillables;
47 | }
48 |
49 | /**
50 | * Return a collection of all includes.
51 | *
52 | * @return collection
53 | */
54 | public function getIncludes()
55 | {
56 | return $this->includes;
57 | }
58 |
59 | /**
60 | * Return a collection of all columns.
61 | *
62 | * @return collection
63 | */
64 | public function getColumns()
65 | {
66 | return $this->columns;
67 | }
68 |
69 | /**
70 | * Itterate over object, build a relations, fillable and includes collection.
71 | *
72 | * @param model $object the model to iterate over
73 | *
74 | * @return array
75 | */
76 | private function includes($object)
77 | {
78 | $fillable = $object->getFillable();
79 | $includes = $object->getIncludes();
80 | $table = $object->getTable();
81 | $columns = $object->columns();
82 |
83 | $results[$table] = [];
84 |
85 | if (! empty($includes)) {
86 | foreach ($includes as $include) {
87 | $results[$table] = [
88 | 'object' => $object,
89 | 'includes' => $this->includes(new $include()),
90 | ];
91 | }
92 | }
93 |
94 | $this->fillables->put($table, $fillable);
95 | $this->includes->push($table, $table);
96 | $this->columns->put($table, $columns);
97 |
98 | return $results;
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/src/Helpers/ApiValidation.php:
--------------------------------------------------------------------------------
1 | objects = $objects;
23 | $this->validateIncludes();
24 | $this->validateFields();
25 | $this->validateRequests();
26 | }
27 |
28 | /**
29 | * Validate post and patch requests to make sure the elements are fillable (needs updating to json api spec posts/patch).
30 | *
31 | * @return [type] [description]
32 | */
33 | public function validateRequests()
34 | {
35 | if (! request()->isMethod('post') && ! request()->isMethod('patch')) {
36 | return;
37 | }
38 |
39 | $request = app('api')->jsonBody();
40 |
41 | $fillable = $this->objects->getFillables();
42 |
43 | if (! isset($request['attributes'])) {
44 | throw (new BadRequestException('invalid_filter'))->withDetails(['data.attributes']);
45 | }
46 |
47 | $errors = array_diff(array_keys($request['attributes']), $fillable->flatten()->all());
48 | if (! empty($errors)) {
49 | throw (new BadRequestException('invalid_filter'))->withDetails($errors);
50 | }
51 | }
52 |
53 | /**
54 | * Validate include= variables to make sure our models have them (needs updated for sub includes, like include=users,profiles.addresses - by passes profiles for addresses).
55 | *
56 | * @return void
57 | */
58 | public function validateIncludes()
59 | {
60 | $allowed = $this->objects->getIncludes();
61 | $includes = app('api')->includes();
62 |
63 | $errors = array_diff($includes->all(), $allowed->all());
64 |
65 | if (! empty($errors)) {
66 | throw (new BadRequestException('invalid_include'))->withDetails($errors);
67 | }
68 | }
69 |
70 | /**
71 | * Validate that fields[]= variables are in our models.
72 | *
73 | * @return array
74 | */
75 | public function validateFields()
76 | {
77 | $fields = app('api')->fields();
78 | $columns = $this->objects->getColumns();
79 | $includes = $this->objects->getIncludes();
80 |
81 | $errors = array_diff($fields->keys()->all(), $includes->all());
82 | if (! empty($errors)) {
83 | throw (new BadRequestException('invalid_filter'))->withDetails($errors);
84 | }
85 |
86 | $errors = $fields->map(function ($item, $key) use ($columns) {
87 | return array_diff($item, $columns->get($key));
88 | })->flatten()->all();
89 |
90 | if (! empty($errors)) {
91 | throw (new BadRequestException('invalid_filter'))->withDetails($errors);
92 | }
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/Http/Middleware/Auth/JsonApiAuthBasicMiddleware.php:
--------------------------------------------------------------------------------
1 | onceBasic())) {
20 | throw new UnauthorizedHttpException('invalid-credentials');
21 | }
22 |
23 | return $next($request);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Http/Middleware/JsonApiMiddleware.php:
--------------------------------------------------------------------------------
1 | request = $request;
25 |
26 | $this->checkGetVars();
27 | $this->checkAccept();
28 | $this->checkContentType();
29 |
30 | return $next($request);
31 | }
32 |
33 | /**
34 | * Check GET input variaibles.
35 | *
36 | * @return void
37 | */
38 | private function checkGetVars()
39 | {
40 | if (! $this->request->isMethod('get')) {
41 | return false;
42 | }
43 |
44 | $badRequestInput = array_except($this->request->all(), array_keys(config('jsonapi.allowed_get')));
45 |
46 | if (request()->input('page')) {
47 | $badRequestInput = array_merge($badRequestInput, array_except(request()->input('page'), config('jsonapi.allowed_get.page')));
48 | }
49 |
50 | if (empty($badRequestInput)) {
51 | return false;
52 | }
53 |
54 | $errors = [];
55 |
56 | foreach (array_keys($badRequestInput) as $field) {
57 | array_push($errors, [
58 | //'code' => 0,
59 | 'source' => ['pointer' => $field],
60 | 'title' => trans('jsonapi::errors.invalid_get.title'),
61 | ]);
62 | }
63 |
64 | throw (new BadRequestException('invalid_get'))->withErrors($errors);
65 | }
66 |
67 | /**
68 | * Check Accept Header.
69 | *
70 | * @return void
71 | */
72 | private function checkAccept()
73 | {
74 | preg_match('/application\/vnd\.api\.([\w\d\.]+)\+([\w]+)/', $this->request->header('Accept'), $matches);
75 |
76 | app('api')->setVersion(isset($matches[1]) ? $matches[1] : config('jsonapi.version'));
77 |
78 | if ($matches || $this->request->header('Accept') === config('jsonapi.accept') || ! config('jsonapi.strict')) {
79 | return;
80 | }
81 |
82 | throw (new NotAcceptableException('not-acceptable'))->withDetails(config('jsonapi.accept'));
83 | }
84 |
85 | /**
86 | * Check Content-Type Header.
87 | *
88 | * @return void
89 | */
90 | private function checkContentType()
91 | {
92 | if ($this->request->header('Content-Type') === config('jsonapi.content_type') || ! config('jsonapi.strict')) {
93 | return;
94 | }
95 |
96 | throw (new UnsupportedMediaTypeException('unsupported-media-type'))->withDetails(config('jsonapi.content_type'));
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/Http/Responses/ApiResponse.php:
--------------------------------------------------------------------------------
1 | json($this->jsonapiData($results), $code, [
21 | 'Content-Type' => config('jsonapi.content_type'),
22 | ], true);
23 | }
24 |
25 | /**
26 | * Render the output for the json api.
27 | *
28 | * @return array
29 | */
30 | public function jsonapiData($data = [])
31 | {
32 | return array_merge($data, [
33 | 'jsonapi' => [
34 | 'version' => config('jsonapi.json_version', '1.0'),
35 | 'self' => app('api')->getVersion(),
36 | ],
37 | ]);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/Providers/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Askedio/laravel-Cruddy/3077f8b619b68b9d1a4873d56a0f415c207b46c0/src/Providers/.gitkeep
--------------------------------------------------------------------------------
/src/Providers/GenericServiceProvider.php:
--------------------------------------------------------------------------------
1 | app->singleton(
18 | \Illuminate\Contracts\Debug\ExceptionHandler::class,
19 | \Askedio\Laravel5ApiController\Exceptions\Handler::class
20 | );
21 |
22 | $this->app->singleton('api', function () {
23 | return new \Askedio\Laravel5ApiController\Helpers\Api();
24 | });
25 |
26 | $this->mergeConfigFrom(
27 | __DIR__.'/../config/jsonapi.php', 'jsonapi'
28 | );
29 | }
30 |
31 | /**
32 | * Register routes, translations, views and publishers.
33 | *
34 | * @return void
35 | */
36 | public function boot(Router $router)
37 | {
38 | $this->loadTranslationsFrom(__DIR__.'/../lang', 'jsonapi');
39 |
40 | $this->publishes([
41 | __DIR__.'/../lang' => resource_path('lang/vendor/jsonapi'),
42 | __DIR__.'/../config/jsonapi.php' => config_path('jsonapi.php'),
43 | ]);
44 |
45 | $router->middleware('jsonapi', \Askedio\Laravel5ApiController\Http\Middleware\JsonApiMiddleware::class);
46 | $router->middleware('jsonapi.auth.basic', \Askedio\Laravel5ApiController\Http\Middleware\Auth\JsonApiAuthBasicMiddleware::class);
47 |
48 | response()->macro('jsonapi', function ($code, $value) {
49 | $apiResponse = new \Askedio\Laravel5ApiController\Http\Responses\ApiResponse();
50 |
51 | return $apiResponse->jsonapi($code, $value);
52 | });
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/Traits/ControllerTrait.php:
--------------------------------------------------------------------------------
1 | version) && app('api')->getVersion() !== $this->version) {
17 | throw (new NotAcceptableException('not-acceptable'))->withDetails('/application/vnd.api.'.$this->version.'+json');
18 | }
19 |
20 | $this->results = new ApiController($this);
21 | }
22 |
23 | public function index()
24 | {
25 | return $this->render([
26 | 'success' => 200,
27 | 'error' => [
28 | 'class' => \Symfony\Component\HttpKernel\Exception\HttpException::class,
29 | 'message' => 500,
30 | ],
31 | 'results' => $this->results->index(),
32 | ]);
33 | }
34 |
35 | public function store()
36 | {
37 | return $this->render([
38 | 'success' => 200,
39 | 'error' => [
40 | 'class' => \Symfony\Component\HttpKernel\Exception\HttpException::class,
41 | 'message' => 500,
42 | ],
43 | 'results' => $this->results->store(),
44 | ]);
45 | }
46 |
47 | public function show($idd)
48 | {
49 | return $this->render([
50 | 'success' => 200,
51 | 'error' => [
52 | 'class' => \Symfony\Component\HttpKernel\Exception\NotFoundHttpException::class,
53 | 'message' => trans('jsonapi::errors.not_found'),
54 | ],
55 | 'results' => $this->results->show($idd),
56 | ]);
57 | }
58 |
59 | public function update($idd)
60 | {
61 | return $this->render([
62 | 'success' => 200,
63 | 'error' => [
64 | 'class' => \Symfony\Component\HttpKernel\Exception\HttpException::class,
65 | 'message' => 500,
66 | ],
67 | 'results' => $this->results->update($idd),
68 | ]);
69 | }
70 |
71 | public function destroy($idd)
72 | {
73 | return $this->render([
74 | 'success' => 200,
75 | 'error' => [
76 | 'class' => \Symfony\Component\HttpKernel\Exception\NotFoundHttpException::class,
77 | 'message' => trans('jsonapi::errors.not_found'),
78 | ],
79 | 'data' => $this->results->show($idd),
80 | 'results' => $this->results->destroy($idd),
81 | ]);
82 | }
83 |
84 | private function render($data)
85 | {
86 | if ($data['results']) {
87 | return response()->jsonapi($data['success'], (new ApiTransformer())->transform(isset($data['data']) ? $data['data'] : $data['results']));
88 | }
89 |
90 | throw new $data['error']['class']($data['error']['message']);
91 | }
92 |
93 | public function getAuth()
94 | {
95 | return isset($this->auth) ? $this->auth : false;
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/src/Traits/ModelTrait.php:
--------------------------------------------------------------------------------
1 | includes) ? $this->includes : [];
17 | }
18 |
19 | public function getObjects()
20 | {
21 | if (! $this->objects) {
22 | $this->objects = new ApiObjects($this);
23 | }
24 |
25 | return $this->objects;
26 | }
27 |
28 | /**
29 | * The validation rules assigned in model.
30 | *
31 | * @var string
32 | *
33 | * @return array
34 | */
35 | public function getRule($rule)
36 | {
37 | return isset($this->rules[$rule]) ? $this->rules[$rule] : [];
38 | }
39 |
40 | /**
41 | * The id_field defined in the model.
42 | *
43 | * @return string
44 | */
45 | public function getId()
46 | {
47 | return isset($this->primaryKey) ? $this->primaryKey : 'id';
48 | }
49 |
50 | /**
51 | * Return if Model has searchable flag.
52 | *
53 | * @return bool
54 | */
55 | public function isSearchable()
56 | {
57 | return isset($this->searchable);
58 | }
59 |
60 | /**
61 | * Set order/sort as per json spec.
62 | * TO-DO: Should go into the ApiValidation class so it can manage relational sorts, ie sort=-profiles.id,users.id.
63 | *
64 | * @param string $query
65 | * @param string $sort
66 | *
67 | * @return object
68 | */
69 | public function scopesetSort($query, $sort)
70 | {
71 | if (empty($sort) || ! is_string($sort) || empty($sorted = explode(',', $sort))) {
72 | return $query;
73 | }
74 |
75 | $columns = $this->columns();
76 |
77 | $errors = array_filter(array_diff(array_map(function ($string) {
78 | return ltrim($string, '-');
79 | }, $sorted), $columns));
80 |
81 | if (! empty($errors)) {
82 | throw (new BadRequestException('invalid_sort'))->withDetails([[$this->getTable(), implode(' ', $errors)]]);
83 | }
84 |
85 | array_map(function ($column) use ($query) {
86 | return $query->orderBy(ltrim($column, '-'), ('-' === $column[0]) ? 'DESC' : 'ASC');
87 | }, $sorted);
88 |
89 | return $query;
90 | }
91 |
92 | /**
93 | * Filter results based on filter get variable and transform them if enabled.
94 | *
95 | * @return array
96 | */
97 | public function scopefilterAndTransform()
98 | {
99 | $fields = app('api')->fields();
100 |
101 | $key = $this->getTable();
102 |
103 | $results = $this->isTransformable($this) ? $this->transform($this) : $this;
104 |
105 | if ($fields->has($key)) {
106 | $results = array_diff_key($results, array_flip(array_diff(array_keys($results), $fields->get($key))));
107 | }
108 |
109 | return $results;
110 | }
111 |
112 | /**
113 | * List of columns related to this Model, Cached.
114 | *
115 | * @return array
116 | */
117 | private $cols;
118 |
119 | public function columns()
120 | {
121 | if (! $this->cols) {
122 | $this->cols = DB::connection()->getSchemaBuilder()->getColumnListing($this->getTable());
123 | }
124 |
125 | return $this->cols;
126 | }
127 |
128 | /**
129 | * Checks whether the object is transformable or not.
130 | *
131 | * @param $item
132 | *
133 | * @return bool
134 | */
135 | private function isTransformable($item)
136 | {
137 | return is_object($item) && method_exists($item, 'transform');
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/src/Traits/SearchableTrait.php:
--------------------------------------------------------------------------------
1 | scopeSearchRestricted($qry, $search, $threshold, $entireText);
39 | }
40 |
41 | public function scopeSearchRestricted(Builder $qry, $search, $threshold = null, $entireText = null)
42 | {
43 | $query = clone $qry;
44 | $query->select($this->getTable().'.*');
45 | $this->makeJoins($query);
46 |
47 | $search = mb_strtolower(trim($search));
48 | $words = explode(' ', $search);
49 |
50 | $selects = [];
51 | $this->search_bindings = [];
52 | $relevanceCount = 0;
53 |
54 | foreach ($this->getColumns() as $column => $relevance) {
55 | $relevanceCount += $relevance;
56 | $queries = $this->getSearchQueriesForColumn($column, $relevance, $words);
57 |
58 | if ($entireText) {
59 | $queries[] = $this->getSearchQuery($column, $relevance, [$search], 30, '', '%');
60 | }
61 |
62 | foreach ($queries as $select) {
63 | $selects[] = $select;
64 | }
65 | }
66 |
67 | $this->addSelectsToQuery($query, $selects);
68 |
69 | // Default the threshold if no value was passed.
70 | if (is_null($threshold)) {
71 | $threshold = $relevanceCount / 4;
72 | }
73 |
74 | $this->filterQueryWithRelevance($query, $selects, $threshold);
75 |
76 | $this->makeGroupBy($query);
77 |
78 | $this->addBindingsToQuery($query, $this->search_bindings);
79 |
80 | $this->mergeQueries($query, $qry);
81 |
82 | return $qry;
83 | }
84 |
85 | /**
86 | * Returns database driver Ex: mysql, pgsql, sqlite.
87 | *
88 | * @return array
89 | */
90 | protected function getDatabaseDriver()
91 | {
92 | $key = $this->connection ?: config('database.default');
93 |
94 | return config('database.connections.'.$key.'.driver');
95 | }
96 |
97 | /**
98 | * Returns the search columns.
99 | *
100 | * @return array
101 | */
102 | protected function getColumns()
103 | {
104 | if (isset($this->searchable) && array_key_exists('columns', $this->searchable)) {
105 | return $this->searchable['columns'];
106 | }
107 |
108 | return DB::connection()->getSchemaBuilder()->getColumnListing($this->getTable());
109 | }
110 |
111 | /**
112 | * Returns whether or not to keep duplicates.
113 | *
114 | * @return array
115 | */
116 | protected function getGroupBy()
117 | {
118 | if (isset($this->searchable) && array_key_exists('groupBy', $this->searchable)) {
119 | return $this->searchable['groupBy'];
120 | }
121 |
122 | return false;
123 | }
124 |
125 | /**
126 | * Returns the tables that are to be joined.
127 | *
128 | * @return array
129 | */
130 | protected function getJoins()
131 | {
132 | return array_get($this->searchable, 'joins', []);
133 | }
134 |
135 | /**
136 | * Adds the sql joins to the query.
137 | *
138 | * @param \Illuminate\Database\Eloquent\Builder $query
139 | */
140 | protected function makeJoins(Builder $query)
141 | {
142 | foreach ($this->getJoins() as $table => $keys) {
143 | $query->leftJoin($table, function ($join) use ($keys) {
144 | $join->on($keys[0], '=', $keys[1]);
145 | if (array_key_exists(2, $keys) && array_key_exists(3, $keys)) {
146 | $join->where($keys[2], '=', $keys[3]);
147 | }
148 | });
149 | }
150 | }
151 |
152 | /**
153 | * Makes the query not repeat the results.
154 | *
155 | * @param \Illuminate\Database\Eloquent\Builder $query
156 | */
157 | protected function makeGroupBy(Builder $query)
158 | {
159 | if ($groupBy = $this->getGroupBy()) {
160 | $query->groupBy($groupBy);
161 |
162 | return $query;
163 | }
164 |
165 | $columns = $this->getTable().'.'.$this->primaryKey;
166 |
167 | $query->groupBy($columns);
168 |
169 | $joins = array_keys(($this->getJoins()));
170 |
171 | foreach (array_keys($this->getColumns()) as $column) {
172 | array_map(function ($join) use ($column, $query) {
173 | if (str_contains($column, $join)) {
174 | $query->groupBy($column);
175 | }
176 | }, $joins);
177 | }
178 | }
179 |
180 | /**
181 | * Puts all the select clauses to the main query.
182 | *
183 | * @param \Illuminate\Database\Eloquent\Builder $query
184 | * @param array $selects
185 | */
186 | protected function addSelectsToQuery(Builder $query, array $selects)
187 | {
188 | $selects = new Expression('max('.implode(' + ', $selects).') as relevance');
189 | $query->addSelect($selects);
190 | }
191 |
192 | /**
193 | * Adds the relevance filter to the query.
194 | *
195 | * @param \Illuminate\Database\Eloquent\Builder $query
196 | * @param array $selects
197 | * @param float $relevanceCount
198 | */
199 | protected function filterQueryWithRelevance(Builder $query, array $selects, $relevanceCount)
200 | {
201 | $comparator = $this->getDatabaseDriver() !== 'mysql' ? implode(' + ', $selects) : 'relevance';
202 |
203 | $relevanceCount = number_format($relevanceCount, 2, '.', '');
204 |
205 | $query->havingRaw("$comparator > $relevanceCount");
206 | $query->orderBy('relevance', 'desc');
207 |
208 | // add bindings to postgres
209 | }
210 |
211 | /**
212 | * Returns the search queries for the specified column.
213 | *
214 | * @param \Illuminate\Database\Eloquent\Builder $query
215 | * @param string $column
216 | * @param float $relevance
217 | * @param array $words
218 | *
219 | * @return array
220 | */
221 | protected function getSearchQueriesForColumn($column, $relevance, array $words)
222 | {
223 | $queries = [];
224 |
225 | $queries[] = $this->getSearchQuery($column, $relevance, $words, 15);
226 | $queries[] = $this->getSearchQuery($column, $relevance, $words, 5, '', '%');
227 | $queries[] = $this->getSearchQuery($column, $relevance, $words, 1, '%', '%');
228 |
229 | return $queries;
230 | }
231 |
232 | /**
233 | * Returns the sql string for the given parameters.
234 | *
235 | * @param \Illuminate\Database\Eloquent\Builder $query
236 | * @param string $column
237 | * @param string $relevance
238 | * @param array $words
239 | * @param string $compare
240 | * @param float $relevanceMultiplier
241 | * @param string $preWord
242 | * @param string $postWord
243 | *
244 | * @return string
245 | */
246 | protected function getSearchQuery($column, $relevance, array $words, $relevanceMultiplier, $preWord = '', $postWord = '')
247 | {
248 | $likeComparator = $this->getDatabaseDriver() === 'pgsql' ? 'ILIKE' : 'LIKE';
249 | $cases = [];
250 |
251 | foreach ($words as $word) {
252 | $cases[] = $this->getCaseCompare($column, $likeComparator, $relevance * $relevanceMultiplier);
253 | $this->search_bindings[] = $preWord.$word.$postWord;
254 | }
255 |
256 | return implode(' + ', $cases);
257 | }
258 |
259 | /**
260 | * Returns the comparison string.
261 | *
262 | * @param string $column
263 | * @param string $compare
264 | * @param float $relevance
265 | *
266 | * @return string
267 | */
268 | protected function getCaseCompare($column, $compare, $relevance)
269 | {
270 | /* commented out for CI
271 | }
272 | */
273 | $column = str_replace('.', '`.`', $column);
274 | $field = 'LOWER(`'.$column.'`) '.$compare.' ?';
275 |
276 | return '(case when '.$field.' then '.$relevance.' else 0 end)';
277 | }
278 |
279 | /**
280 | * Adds the bindings to the query.
281 | *
282 | * @param \Illuminate\Database\Eloquent\Builder $query
283 | * @param array $bindings
284 | */
285 | protected function addBindingsToQuery(Builder $query, array $bindings)
286 | {
287 | $count = $this->getDatabaseDriver() !== 'mysql' ? 2 : 1;
288 | for ($i = 0; $i < $count; $i++) {
289 | foreach ($bindings as $binding) {
290 | $type = $i === 0 ? 'where' : 'having';
291 | $query->addBinding($binding, $type);
292 | }
293 | }
294 | }
295 |
296 | /**
297 | * Merge our cloned query builder with the original one.
298 | *
299 | * @param \Illuminate\Database\Eloquent\Builder $clone
300 | * @param \Illuminate\Database\Eloquent\Builder $original
301 | */
302 | protected function mergeQueries(Builder $clone, Builder $original)
303 | {
304 | $tableName = DB::connection($this->connection)->getTablePrefix().$this->getTable();
305 |
306 | $original->from(DB::connection($this->connection)->raw("({$clone->toSql()}) as `{$tableName}`"));
307 |
308 | $original->mergeBindings($clone->getQuery());
309 | }
310 | }
311 |
--------------------------------------------------------------------------------
/src/Transformers/ApiTransformer.php:
--------------------------------------------------------------------------------
1 | object = $object;
28 |
29 | $results = $this->isPaginator() ? $this->transformPaginator() : $this->transformObject();
30 |
31 | return (new KeysTransformer())->transform($results);
32 | }
33 |
34 | /**
35 | * Transform Pagination.
36 | *
37 | * @return array
38 | */
39 | private function transformPaginator()
40 | {
41 | $results = array_map(function ($object) {
42 | return $this->transformation($object, false);
43 | }, $this->object->all());
44 |
45 | return array_merge(['data' => $results], $this->getPaginationMeta());
46 | }
47 |
48 | /**
49 | * Transform objects.
50 | *
51 | * @return transformation
52 | */
53 | private function transformObject()
54 | {
55 | return $this->transformation($this->object, true);
56 | }
57 |
58 | /**
59 | * Build the transformed results.
60 | *
61 | * @param object $object
62 | * @param bool $single
63 | *
64 | * @return array
65 | */
66 | private function transformation($object, $single)
67 | {
68 | $includes = $this->objectIncludes($object);
69 |
70 | $item = $single ? ['data' => $this->item($object)] : $this->item($object);
71 | $data = array_merge($item, ['relationships' => $this->relations($includes)]);
72 |
73 | return array_filter(array_merge(
74 | $data,
75 | ['included' => $includes],
76 | $single ? ['links' => ['self' => request()->url()]] : []
77 | ));
78 | }
79 |
80 | /**
81 | * Build a list of includes for this object.
82 | *
83 | * @param object $object
84 | *
85 | * @return array
86 | */
87 | private function objectIncludes($object)
88 | {
89 | $results = [];
90 |
91 | foreach (app('api')->includes() as $include) {
92 | if (is_object($object->$include)) {
93 | foreach ($object->$include as $included) {
94 | $results[] = $this->item($included);
95 | }
96 | }
97 | }
98 |
99 | return $results;
100 | }
101 |
102 | /**
103 | * Build json api style results per item.
104 | *
105 | * @param $object
106 | *
107 | * @return array
108 | */
109 | private function item($object)
110 | {
111 | $pimaryId = $object->getId();
112 |
113 | return [
114 | 'type' => $object->getTable(),
115 | 'id' => $object->$pimaryId,
116 | 'attributes' => $object->filterAndTransform(),
117 | ];
118 | }
119 |
120 | /**
121 | * Get relations for the included items.
122 | *
123 | * @param [type] $includes [description]
124 | * @param [type] $object [description]
125 | *
126 | * @return [type] [description]
127 | */
128 | private function relations($includes)
129 | {
130 | return array_map(function ($inc) {
131 | return [$inc['type'] => ['data' => ['id' => $inc['attributes']['id'], 'type' => $inc['type']]]];
132 | }, $includes);
133 | }
134 |
135 | /**
136 | * @param $object
137 | *
138 | * @return bool
139 | */
140 | private function isPaginator()
141 | {
142 | return $this->object instanceof LengthAwarePaginator;
143 | }
144 |
145 | /**
146 | * Gets the pagination meta data. Assumes that a paginator
147 | * instance is passed \Illuminate\Pagination\LengthAwarePaginator.
148 | *
149 | * @param $paginator
150 | *
151 | * @return array
152 | */
153 | private function getPaginationMeta()
154 | {
155 | $object = $this->object;
156 |
157 | return [
158 | 'meta' => [
159 | 'total' => $object->total(),
160 | 'currentPage' => $object->currentPage(),
161 | 'perPage' => $object->perPage(),
162 | 'hasMorePages' => $object->hasMorePages(),
163 | 'hasPages' => $object->hasPages(),
164 | ],
165 | 'links' => [
166 | 'self' => $object->url($object->currentPage()),
167 | 'first' => $object->url(1),
168 | 'last' => $object->url($object->lastPage()),
169 | 'next' => $object->nextPageUrl(),
170 | 'prev' => $object->previousPageUrl(),
171 | ],
172 | ];
173 | }
174 | }
175 |
--------------------------------------------------------------------------------
/src/Transformers/KeysTransformer.php:
--------------------------------------------------------------------------------
1 | ',
41 | '?',
42 | '@',
43 | '\\',
44 | '^',
45 | '`',
46 | '{',
47 | '|',
48 | '}',
49 | '~',
50 | ];
51 | /**
52 | * @link http://jsonapi.org/format/#document-member-names-allowed-characters
53 | *
54 | * @var array
55 | */
56 | protected $forbiddenFirstOrLast = [
57 | '-',
58 | '_',
59 | ' ',
60 | ];
61 |
62 | /**
63 | * Convert array indexes to json api spec indexes.
64 | *
65 | * @param array $array [description]
66 | *
67 | * @return array
68 | */
69 | public function transform($array)
70 | {
71 | $results = [];
72 | foreach ($array as $key => $value) {
73 | $results[$this->convert($key)] = is_array($value) ? $this->transform($value) : $value;
74 | }
75 |
76 | return $results;
77 | }
78 |
79 | /**
80 | * Do the actual conversion.
81 | *
82 | * @param
83 | *
84 | * @return
85 | */
86 | private function convert($key)
87 | {
88 | if (! is_string($key)) {
89 | return $key;
90 | }
91 |
92 | $firstLast = implode('', $this->forbiddenFirstOrLast);
93 |
94 | return str_replace($this->forbiddenCharacters, '', ltrim(rtrim($key, $firstLast), $firstLast));
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/src/config/jsonapi.php:
--------------------------------------------------------------------------------
1 | env('JSONAPI_STRICT', false),
5 | 'version' => 'v1',
6 | 'json_version' => '1.0',
7 | 'url' => 'api/*',
8 | 'accept' => 'application/vnd.api+json',
9 | 'content_type' => 'application/vnd.api+json',
10 | 'allowed_get' => [
11 | 'include' => '',
12 | 'fields' => '',
13 | 'page' => ['size', 'number'],
14 | 'sort' => '',
15 | 'search' => '',
16 | ],
17 | ];
18 |
--------------------------------------------------------------------------------
/src/lang/en/errors.php:
--------------------------------------------------------------------------------
1 | 'Page not found',
6 |
7 | 'invalid-credentials' => [
8 | 'title' => 'Unauthorized',
9 | 'detail' => 'Invalid credentials.',
10 | ],
11 |
12 | 'not-acceptable' => [
13 | 'title' => 'Not Acceptable',
14 | 'detail' => 'Accept was not %s.',
15 | ],
16 |
17 | 'unsupported-media-type' => [
18 | 'title' => 'Unsupported Media Type',
19 | 'detail' => 'Content-Type was not'.config('jsonapi.content-type'),
20 | ],
21 |
22 | 'invalid_sort' => [
23 | 'title' => 'Invalid Query Parameter.',
24 | 'detail' => 'The resource `%s` does not have an `%s` sorting option.',
25 | 'source' => ['type' => 'parameter', 'value' => '%s.%s'],
26 | ],
27 |
28 | 'invalid_filter' => [
29 | 'title' => 'Invalid Query Parameter.',
30 | 'detail' => 'The resource does not have an `%s` filter option.',
31 | 'source' => ['type' => 'parameter', 'value' => '%s'],
32 | ],
33 |
34 | 'invalid_include' => [
35 | 'title' => 'Invalid Query Parameter',
36 | 'detail' => 'The resource does not have an `%s` relationship path.',
37 | 'source' => ['type' => 'parameter', 'value' => '%s'],
38 | ],
39 |
40 | 'invalid_get' => [
41 | 'title' => 'Invalid Query Parameter',
42 | 'detail' => '%s is not an allowed query parameter.',
43 | 'source' => ['type' => 'parameter', 'value' => '%s'],
44 | ],
45 |
46 | 'invalid_attribute' => [
47 | 'title' => 'Invalid Attribute',
48 | 'detail' => '%s',
49 | 'source' => ['type' => 'pointer', 'value' => '%s'],
50 | ],
51 |
52 | ];
53 |
--------------------------------------------------------------------------------
/tests/AcceptanceTestCase.php:
--------------------------------------------------------------------------------
1 | config('jsonapi.content_type'), 'Accept' => config('jsonapi.accept')], $headers));
13 |
14 | return parent::json($method, $uri, $data, $headers);
15 | }
16 |
17 | /**
18 | * Create User Helpers.
19 | *
20 | * @return json
21 | */
22 | public function createUser()
23 | {
24 | return $this->json('POST', '/api/user', [
25 | 'data' => [
26 | 'type' => 'users',
27 | 'attributes' => [
28 | 'name' => 'Ember Hamster',
29 | 'email' => 'test@test.com',
30 | 'password' => bcrypt('password'),
31 | ],
32 | ],
33 | ]);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/tests/AcceptanceTests/AuthTest.php:
--------------------------------------------------------------------------------
1 | createUserRaw();
12 | $this->json('GET', '/api/me/profile', [], ['Authorization' => 'Basic YWRtaW5AbG9jYWxob3N0LmNvbTpwYXNzd29yZA==']);
13 | $response = $this->response;
14 | $this->assertEquals(200, $response->getStatusCode());
15 | $this->assertEquals(config('jsonapi.content_type'), $response->headers->get('Content-type'));
16 | $this->seeOrSaveJsonStructure($response);
17 | }
18 |
19 | public function testBadAuth()
20 | {
21 | $this->createUserRaw();
22 | $this->json('GET', '/api/me/profile', [], ['Authorization' => 'Basic ZWRtaW5AbG9jYWxob3N0LmNvbTpwYXNzd29yZA==']);
23 | $response = $this->response;
24 |
25 | $this->assertEquals(401, $response->getStatusCode());
26 | $this->assertEquals(config('jsonapi.content_type'), $response->headers->get('Content-type'));
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/tests/AcceptanceTests/CrudTest.php:
--------------------------------------------------------------------------------
1 | createUser();
12 | $response = $results->response;
13 | $this->assertEquals(200, $response->getStatusCode());
14 | $this->assertEquals(config('jsonapi.content_type'), $response->headers->get('Content-type'));
15 | }
16 |
17 | public function testRead()
18 | {
19 | $this->createUser();
20 | $this->json('GET', '/api/user/1');
21 | $response = $this->response;
22 | $this->assertEquals(200, $response->getStatusCode());
23 | $this->assertEquals(config('jsonapi.content_type'), $response->headers->get('Content-type'));
24 | $this->seeOrSaveJsonStructure($response);
25 | }
26 |
27 | public function testDelete()
28 | {
29 | $this->createUser();
30 |
31 | $this->json('DELETE', '/api/user/1');
32 | $response = $this->response;
33 | $this->assertEquals(200, $response->getStatusCode());
34 | $this->assertEquals(config('jsonapi.content_type'), $response->headers->get('Content-type'));
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/tests/AcceptanceTests/HeadersTest.php:
--------------------------------------------------------------------------------
1 | json('GET', '/api/user/', [], ['Content-Type' => 'test']);
12 | $response = $this->response;
13 | $this->assertEquals(415, $response->getStatusCode());
14 | }
15 |
16 | public function testBadContentAccept()
17 | {
18 | $this->json('GET', '/api/user/', [], ['Accept' => 'test']);
19 | $response = $this->response;
20 | $this->assertEquals(406, $response->getStatusCode());
21 | }
22 |
23 | public function testVersionContentType()
24 | {
25 | $this->json('GET', '/api/user/', [], ['Accept' => 'application/vnd.api.v1+json']);
26 | $response = $this->response;
27 | $this->assertEquals(200, $response->getStatusCode());
28 | }
29 |
30 | public function testBadVersion()
31 | {
32 | $this->json('GET', '/api/user/', [], ['Accept' => 'application/vnd.api.v2+json']);
33 | $response = $this->response;
34 | $this->assertEquals(406, $response->getStatusCode());
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/tests/AcceptanceTests/IncludesTest.php:
--------------------------------------------------------------------------------
1 | json('GET', '/api/user/?include=badtest');
12 | $response = $this->response;
13 | $this->assertEquals(400, $response->getStatusCode());
14 | $this->assertEquals(config('jsonapi.content_type'), $response->headers->get('Content-type'));
15 | }
16 |
17 | public function testInclude()
18 | {
19 | $this->createUserRaw();
20 | $this->json('GET', '/api/user/?include=profiles');
21 | $response = $this->response;
22 | $this->assertEquals(200, $response->getStatusCode());
23 | $this->assertEquals(config('jsonapi.content_type'), $response->headers->get('Content-type'));
24 | }
25 |
26 | public function testListWithIncludeFields()
27 | {
28 | $this->createUserRaw();
29 |
30 | $this->json('GET', '/api/user?fields[profiles]=id,phone');
31 | $response = $this->response;
32 | $this->assertEquals(200, $response->getStatusCode());
33 | $this->assertEquals(config('jsonapi.content_type'), $response->headers->get('Content-type'));
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/tests/AcceptanceTests/JsonTest.php:
--------------------------------------------------------------------------------
1 | json('GET', '/api/404');
12 | $response = $this->response;
13 | $this->assertEquals(404, $response->getStatusCode());
14 | $this->assertEquals(config('jsonapi.content_type'), $response->headers->get('Content-type'));
15 | }
16 |
17 | public function testError404User()
18 | {
19 | $this->json('GET', '/api/user/404');
20 | $response = $this->response;
21 | $this->assertEquals(404, $response->getStatusCode());
22 | $this->assertEquals(config('jsonapi.content_type'), $response->headers->get('Content-type'));
23 | }
24 |
25 | public function testBadQueryVar()
26 | {
27 | $this->json('GET', '/api/user/?badtest');
28 | $response = $this->response;
29 | $this->assertEquals(400, $response->getStatusCode());
30 | $this->assertEquals(config('jsonapi.content_type'), $response->headers->get('Content-type'));
31 | }
32 |
33 | public function testNonApiException()
34 | {
35 | $this->json('GET', '/not-the-api');
36 | $response = $this->response;
37 | $this->assertEquals(404, $response->getStatusCode());
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/tests/AcceptanceTests/ListTest.php:
--------------------------------------------------------------------------------
1 | json('GET', '/api/user');
12 | $response = $this->response;
13 | $this->assertEquals(200, $response->getStatusCode());
14 | $this->assertEquals(config('jsonapi.content_type'), $response->headers->get('Content-type'));
15 | $this->seeOrSaveJsonStructure();
16 | }
17 |
18 | public function testSort()
19 | {
20 | $this->json('GET', '/api/user?sort=-id');
21 | $response = $this->response;
22 | $this->assertEquals(200, $response->getStatusCode());
23 | $this->assertEquals(config('jsonapi.content_type'), $response->headers->get('Content-type'));
24 | $this->seeOrSaveJsonStructure();
25 | }
26 |
27 | public function testBadSort()
28 | {
29 | $this->json('GET', '/api/user?sort=-test');
30 | $response = $this->response;
31 | $this->assertEquals(400, $response->getStatusCode());
32 | $this->assertEquals(config('jsonapi.content_type'), $response->headers->get('Content-type'));
33 | }
34 |
35 | public function testListWithFields()
36 | {
37 | $this->createUser();
38 |
39 | $this->json('GET', '/api/user?fields[users]=id,name');
40 | $response = $this->response;
41 | $this->assertEquals(200, $response->getStatusCode());
42 | $this->assertEquals(config('jsonapi.content_type'), $response->headers->get('Content-type'));
43 | }
44 |
45 | public function testListWithBadFields()
46 | {
47 | $this->createUser();
48 |
49 | $this->json('GET', '/api/user?fields[users]=id,name,badtest');
50 | $response = $this->response;
51 | $this->assertEquals(400, $response->getStatusCode());
52 | $this->assertEquals(config('jsonapi.content_type'), $response->headers->get('Content-type'));
53 | }
54 |
55 | public function testListWithBadFieldName()
56 | {
57 | $this->createUser();
58 |
59 | $this->json('GET', '/api/user?fields[badtest]=id,name,bad');
60 | $response = $this->response;
61 | $this->assertEquals(400, $response->getStatusCode());
62 | $this->assertEquals(config('jsonapi.content_type'), $response->headers->get('Content-type'));
63 | }
64 |
65 | public function testSearch()
66 | {
67 | $this->json('GET', '/api/user?search=test');
68 | $response = $this->response;
69 | $this->assertEquals(200, $response->getStatusCode());
70 | $this->assertEquals(config('jsonapi.content_type'), $response->headers->get('Content-type'));
71 | }
72 |
73 | public function testSearchEmpty()
74 | {
75 | $this->json('GET', '/api/user?search=');
76 | $response = $this->response;
77 | $this->assertEquals(200, $response->getStatusCode());
78 | $this->assertEquals(config('jsonapi.content_type'), $response->headers->get('Content-type'));
79 | }
80 |
81 | public function testPagination()
82 | {
83 | $this->json('GET', '/api/user?page[size]=1&page[number]=1');
84 | $response = $this->response;
85 | $this->assertEquals(200, $response->getStatusCode());
86 | $this->assertEquals(config('jsonapi.content_type'), $response->headers->get('Content-type'));
87 | }
88 |
89 | public function testPaginationBadField()
90 | {
91 | $this->json('GET', '/api/user?page[badtest]=1&page[number]=1');
92 | $response = $this->response;
93 | $this->assertEquals(400, $response->getStatusCode());
94 | $this->assertEquals(config('jsonapi.content_type'), $response->headers->get('Content-type'));
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/tests/AcceptanceTests/PatchTest.php:
--------------------------------------------------------------------------------
1 | createUser();
12 | $this->json('PATCH', '/api/user/1', [
13 | 'data' => [
14 | 'type' => 'users',
15 | 'attributes' => [
16 | 'name' => 'testupdate',
17 | ],
18 | ],
19 | ]);
20 | $response = $this->response;
21 | $this->assertEquals(200, $response->getStatusCode());
22 | $this->assertEquals(config('jsonapi.content_type'), $response->headers->get('Content-type'));
23 | $this->seeJson(['name' => 'testupdate']);
24 | }
25 |
26 | public function testBadPatchField()
27 | {
28 | $this->createUser();
29 | $this->json('PATCH', '/api/user/1', [
30 | 'test' => 'test',
31 | ]);
32 |
33 | $response = $this->response;
34 | $this->assertEquals(400, $response->getStatusCode());
35 | $this->assertEquals(config('jsonapi.content_type'), $response->headers->get('Content-type'));
36 | }
37 |
38 | public function testPatchValidation()
39 | {
40 | $this->createUser();
41 | $this->json('PATCH', '/api/user/1', [
42 | 'data' => [
43 | 'type' => 'users',
44 | 'attributes' => [
45 | 'email' => 'notanemail',
46 | ],
47 | ],
48 | ]);
49 |
50 | $response = $this->response;
51 | $this->assertEquals(403, $response->getStatusCode());
52 | $this->assertEquals(config('jsonapi.content_type'), $response->headers->get('Content-type'));
53 | }
54 |
55 | public function testPatch404()
56 | {
57 | $this->json('PATCH', '/api/user/404', [
58 | 'data' => [
59 | 'type' => 'users',
60 | 'attributes' => [
61 | 'name' => 'Ember Hamster kpok',
62 | ],
63 | ],
64 | ]);
65 |
66 | $response = $this->response;
67 | $this->assertEquals(500, $response->getStatusCode());
68 | $this->assertEquals(config('jsonapi.content_type'), $response->headers->get('Content-type'));
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/tests/AcceptanceTests/PostTest.php:
--------------------------------------------------------------------------------
1 | json('POST', '/api/user', [
12 | 'data' => [
13 | 'type' => 'users',
14 | 'attributes' => [
15 | 'badfield' => 'Ember Hamster kpok',
16 | ],
17 | ],
18 | ]);
19 |
20 | $response = $this->response;
21 | $this->assertEquals(400, $response->getStatusCode());
22 | $this->assertEquals(config('jsonapi.content_type'), $response->headers->get('Content-type'));
23 | }
24 |
25 | public function testPostValidation()
26 | {
27 | $this->json('POST', '/api/user', [
28 | 'data' => [
29 | 'type' => 'users',
30 | 'attributes' => [
31 | 'email' => 'test',
32 | ],
33 | ],
34 | ]);
35 |
36 | $response = $this->response;
37 | $this->assertEquals(403, $response->getStatusCode());
38 | $this->assertEquals(config('jsonapi.content_type'), $response->headers->get('Content-type'));
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/tests/App/Database/.gitignore:
--------------------------------------------------------------------------------
1 | *.sqlite
2 |
--------------------------------------------------------------------------------
/tests/App/Database/Migrations/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Askedio/laravel-Cruddy/3077f8b619b68b9d1a4873d56a0f415c207b46c0/tests/App/Database/Migrations/.gitkeep
--------------------------------------------------------------------------------
/tests/App/Database/Migrations/2014_10_12_000000_create_users_table.php:
--------------------------------------------------------------------------------
1 | getSchemaBuilder()->create('users', function (Blueprint $table) {
16 | $table->increments('id');
17 | $table->string('name');
18 | $table->string('email')->unique();
19 | $table->string('password', 60);
20 | $table->rememberToken();
21 | $table->timestamps();
22 | });
23 | }
24 |
25 | /**
26 | * Reverse the migrations.
27 | *
28 | * @return void
29 | */
30 | public function down()
31 | {
32 | DB::connection()->getSchemaBuilder()->dropIfExists('users');
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/tests/App/Database/Migrations/2016_03_09_190517_create_profiles_table.php:
--------------------------------------------------------------------------------
1 | getSchemaBuilder()->create('profiles', function (Blueprint $table) {
16 | $table->increments('id');
17 | $table->integer('user_id');
18 | $table->string('phone');
19 | $table->timestamps();
20 | });
21 | }
22 |
23 | /**
24 | * Reverse the migrations.
25 | *
26 | * @return void
27 | */
28 | public function down()
29 | {
30 | DB::connection()->getSchemaBuilder()->dropIfExists('profiles');
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/tests/App/Database/Migrations/2016_03_09_223910_create_profiles_lookup_table.php:
--------------------------------------------------------------------------------
1 | getSchemaBuilder()->create('profiles_user', function (Blueprint $table) {
16 | $table->integer('user_id');
17 | $table->integer('profiles_id');
18 | });
19 | }
20 |
21 | /**
22 | * Reverse the migrations.
23 | *
24 | * @return void
25 | */
26 | public function down()
27 | {
28 | DB::connection()->getSchemaBuilder()->dropIfExists('profiles_user');
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/tests/App/Http/Controllers/ProfileController.php:
--------------------------------------------------------------------------------
1 | 'api', 'middleware' => ['api', 'jsonapi']], function () {
15 | Route::resource('user', 'Askedio\Tests\App\Http\Controllers\UserController');
16 | Route::resource('profile', 'Askedio\Tests\App\Http\Controllers\ProfileController');
17 | });
18 |
19 | Route::group(['prefix' => 'api/me', 'middleware' => ['api', 'jsonapi', 'jsonapi.auth.basic']], function () {
20 | Route::resource('profile', 'Askedio\Tests\App\Http\Controllers\ProfileController');
21 | });
22 |
--------------------------------------------------------------------------------
/tests/App/Profiles.php:
--------------------------------------------------------------------------------
1 | belongsTo('Askedio\Tests\App\User');
18 | }
19 |
20 | /**
21 | * The attributes that are mass assignable.
22 | *
23 | * @var array
24 | */
25 | protected $fillable = [
26 | 'phone',
27 | ];
28 | }
29 |
--------------------------------------------------------------------------------
/tests/App/ProfilesAlt.php:
--------------------------------------------------------------------------------
1 | belongsTo('Askedio\Tests\App\User');
18 | }
19 |
20 | /**
21 | * The attributes that are mass assignable.
22 | *
23 | * @var array
24 | */
25 | protected $fillable = [
26 | 'phone',
27 | ];
28 | }
29 |
--------------------------------------------------------------------------------
/tests/App/Providers/RouteServiceProvider.php:
--------------------------------------------------------------------------------
1 | publishes([
18 | realpath(__DIR__.'/../Database/Migrations') => database_path('migrations'),
19 | ], 'migrations');
20 | $this->publishes([
21 | realpath(__DIR__.'/../Database/Seeds') => database_path('seeds'),
22 | ], 'seeds');
23 |
24 | parent::boot($router);
25 | }
26 |
27 | /**
28 | * Define the routes for the application.
29 | *
30 | * @param \Illuminate\Routing\Router $router
31 | */
32 | public function map(Router $router)
33 | {
34 | $router->group(['namespace' => $this->namespace], function ($router) {
35 | require dirname(__FILE__).'/../Http/routes.php';
36 | });
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/tests/App/User.php:
--------------------------------------------------------------------------------
1 | [
36 | 'email' => 'email|unique:users,email',
37 | ],
38 | 'create' => [
39 | 'email' => 'email|required|unique:users,email',
40 | ],
41 | ];
42 |
43 | protected $searchable = [
44 | 'columns' => [
45 | 'users.name' => 10,
46 | 'users.email' => 5,
47 | 'profiles.user_id' => 5,
48 | ],
49 | 'joins' => [
50 | 'profiles' => ['users.id', 'profiles.user_id'],
51 | ],
52 | 'groupBy' => 'profiles.user_id',
53 | ];
54 |
55 | protected $primaryKey = 'id';
56 |
57 | public function transform(User $user)
58 | {
59 | return [
60 | 'id' => $user->id,
61 | 'name' => $user->name,
62 | 'email' => $user->email,
63 | ];
64 | }
65 |
66 | public function profiles()
67 | {
68 | return $this->hasMany('Askedio\Tests\App\Profiles');
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/tests/App/UserAlt.php:
--------------------------------------------------------------------------------
1 | [
38 | 'email' => 'email|unique:users,email',
39 | ],
40 | 'create' => [
41 | 'email' => 'email|required|unique:users,email',
42 | ],
43 | ];
44 |
45 | protected $searchable = [
46 | 'columns' => [
47 | 'users.name' => 10,
48 | 'users.email' => 5,
49 | 'profiles.phone' => 5,
50 | ],
51 | 'joins' => [
52 | 'profiles' => ['users.id', 'profiles.user_id', 'users.id', 'profiles.user_id'],
53 | ],
54 | ];
55 |
56 | protected $primaryKey = 'id';
57 |
58 | public function transform(User $user)
59 | {
60 | return [
61 | 'id' => $user->id,
62 | 'name' => $user->name,
63 | 'email' => $user->email,
64 | ];
65 | }
66 |
67 | public function profiles()
68 | {
69 | return $this->hasMany('Askedio\Tests\App\Profiles');
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/tests/BaseTestCase.php:
--------------------------------------------------------------------------------
1 | app['config']->set('database.default', 'sqlite');
25 | $this->app['config']->set('database.connections.sqlite.database', ':memory:');
26 | $this->app['config']->set('app.url', 'http://localhost/');
27 | $this->app['config']->set('app.debug', false);
28 | $this->app['config']->set('app.key', env('APP_KEY', '1234567890123456'));
29 | $this->app['config']->set('app.cipher', 'AES-128-CBC');
30 | $this->app['config']->set('auth.providers.users.model', \Askedio\Tests\App\User::class);
31 |
32 | $this->app->boot();
33 |
34 | $this->migrate();
35 | }
36 |
37 | /**
38 | * run package database migrations.
39 | */
40 | public function migrate()
41 | {
42 | $fileSystem = new Filesystem();
43 | $classFinder = new ClassFinder();
44 |
45 | foreach ($fileSystem->files(__DIR__.'/App/Database/Migrations') as $file) {
46 | $fileSystem->requireOnce($file);
47 | $migrationClass = $classFinder->findClass($file);
48 | (new $migrationClass())->down();
49 | (new $migrationClass())->up();
50 | }
51 | }
52 |
53 | /**
54 | * Boots the application.
55 | *
56 | * @return \Illuminate\Foundation\Application
57 | */
58 | public function createApplication()
59 | {
60 | /** @var $app \Illuminate\Foundation\Application */
61 | $app = require __DIR__.'/../vendor/laravel/laravel/bootstrap/app.php';
62 |
63 | $this->setUpHttpKernel($app);
64 | $app->register(\Askedio\Laravel5ApiController\Providers\GenericServiceProvider::class);
65 | $app->register(\Askedio\Tests\App\Providers\RouteServiceProvider::class);
66 |
67 | return $app;
68 | }
69 |
70 | /**
71 | * @return Router
72 | */
73 | protected function getRouter()
74 | {
75 | $router = new Router(new Dispatcher());
76 |
77 | return $router;
78 | }
79 |
80 | /**
81 | * @param \Illuminate\Foundation\Application $app
82 | */
83 | private function setUpHttpKernel($app)
84 | {
85 | $app->instance('request', (new \Illuminate\Http\Request())->instance());
86 | $app->make('Illuminate\Foundation\Http\Kernel', [$app, $this->getRouter()])->bootstrap();
87 | }
88 |
89 | /**
90 | * Temporary.
91 | */
92 | public function createUserRaw()
93 | {
94 | /* temporary since we dont have relational creation yet */
95 | return (new User())->create([
96 | 'name' => 'admin',
97 | 'email' => 'admin@localhost.com',
98 | 'password' => bcrypt('password'),
99 | ])->profiles()->saveMany([
100 | new Profiles(['phone' => '123']),
101 | ]);
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/tests/IntegrationTestCase.php:
--------------------------------------------------------------------------------
1 | createUserRaw();
13 |
14 | *
15 | * need some way to pass form/input data
16 | * $results = $this->api()->index();
17 | */
18 |
19 | // ...
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/tests/IntegrationTests/SearchTest.php:
--------------------------------------------------------------------------------
1 | search('test')->with('profiles')->get();
15 | $this->assertInstanceOf('Illuminate\Database\Eloquent\Collection', $search);
16 | }
17 |
18 | public function testSearchGroupBy()
19 | {
20 | $search = (new User())->search('test')->with('profiles')->get();
21 | $this->assertInstanceOf('Illuminate\Database\Eloquent\Collection', $search);
22 | }
23 |
24 | public function testSearchNoColumnsDefined()
25 | {
26 | $search = (new Profiles())->search('test');
27 | $this->assertInstanceOf('Illuminate\Database\Eloquent\Builder', $search);
28 | }
29 |
30 | public function testSearchAltGroupBy()
31 | {
32 | $search = (new UserAlt())->search('test')->with('profiles')->get();
33 | $this->assertInstanceOf('Illuminate\Database\Eloquent\Collection', $search);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/tests/Main.json.postman_collection:
--------------------------------------------------------------------------------
1 | {
2 | "id": "e34ec7c9-8419-ac7e-d344-2eac7e6cbe71",
3 | "name": "Main",
4 | "description": "",
5 | "order": [
6 | "a2751967-0b45-0868-cc4d-370658e70470",
7 | "d37258a7-8074-a30d-9926-84363d7578ea",
8 | "8ce49bc4-4b3d-4da2-0a38-624bbdb2a844",
9 | "b15ccc96-82d6-c838-ea73-034836395500",
10 | "3d5535b8-f059-8237-36f3-aca6f40d83e3",
11 | "46050c55-b832-69b3-2c60-93ebfd2f9b48",
12 | "e8755e27-4396-cf48-5d77-4e20d6b4906e",
13 | "847e81b7-2f7e-7196-598b-c7079b94e06d",
14 | "76a6a3d5-2697-367c-febe-f0c517857ff6"
15 | ],
16 | "folders": [
17 | {
18 | "id": "6cafb725-c73c-ca4c-47af-c12ac1250db0",
19 | "name": "Errors",
20 | "description": "",
21 | "order": [
22 | "7b8f4299-fb53-992e-b94d-1cd1c05f4528",
23 | "f04d11a1-3e9b-0213-ed77-6d8f7db91d38",
24 | "0db676e2-f8a8-3309-c305-9b386ca3c5ef"
25 | ],
26 | "owner": "391915",
27 | "collectionId": "e34ec7c9-8419-ac7e-d344-2eac7e6cbe71"
28 | }
29 | ],
30 | "timestamp": 1457906270302,
31 | "owner": "391915",
32 | "remoteLink": "https://www.getpostman.com/collections/f67615f76d74506553d6",
33 | "public": false,
34 | "requests": [
35 | {
36 | "id": "0db676e2-f8a8-3309-c305-9b386ca3c5ef",
37 | "headers": "Content-Type: application/vnd.api+json\nAccept: application/vnd.api+json\n",
38 | "url": "http://localhost:8000/api/admin/user?page[limiit]=1&page[number]=2",
39 | "preRequestScript": null,
40 | "pathVariables": {},
41 | "method": "GET",
42 | "data": null,
43 | "dataMode": "params",
44 | "tests": "tests[\"Body Content-Type\"] = \"application/vnd.api+json\"\n\n\ntests[\"Status code is 400\"] = responseCode.code === 400;",
45 | "currentHelper": "normal",
46 | "helperAttributes": {},
47 | "time": 1458236437329,
48 | "name": "List Pagination Bad Page Variable",
49 | "description": " ",
50 | "collectionId": "e34ec7c9-8419-ac7e-d344-2eac7e6cbe71",
51 | "responses": [],
52 | "folder": "6cafb725-c73c-ca4c-47af-c12ac1250db0"
53 | },
54 | {
55 | "id": "3d5535b8-f059-8237-36f3-aca6f40d83e3",
56 | "headers": "Content-Type: application/vnd.api+json\nAccept: application/vnd.api+json\n",
57 | "url": "http://localhost:8000/api/admin/user?include=profiles",
58 | "preRequestScript": "",
59 | "pathVariables": {},
60 | "method": "GET",
61 | "data": [],
62 | "dataMode": "params",
63 | "tests": "tests[\"Body Content-Type\"] = \"application/vnd.api+json\"\n\ntests[\"Status code is 200\"] = responseCode.code === 200;\n\n",
64 | "currentHelper": "normal",
65 | "helperAttributes": {},
66 | "time": 1457906683361,
67 | "name": "List",
68 | "description": "",
69 | "collectionId": "e34ec7c9-8419-ac7e-d344-2eac7e6cbe71",
70 | "responses": []
71 | },
72 | {
73 | "id": "46050c55-b832-69b3-2c60-93ebfd2f9b48",
74 | "headers": "Content-Type: application/vnd.api+json\nAccept: application/vnd.api+json\n",
75 | "url": "http://localhost:8000/api/admin/user?search=hamster",
76 | "preRequestScript": null,
77 | "pathVariables": {},
78 | "method": "GET",
79 | "data": null,
80 | "dataMode": "params",
81 | "tests": "tests[\"Body Content-Type\"] = \"application/vnd.api+json\"\n\n\ntests[\"Status code is 200\"] = responseCode.code === 200;",
82 | "currentHelper": "normal",
83 | "helperAttributes": {},
84 | "time": 1458450215423,
85 | "name": "List Search",
86 | "description": " ",
87 | "collectionId": "e34ec7c9-8419-ac7e-d344-2eac7e6cbe71",
88 | "responses": []
89 | },
90 | {
91 | "id": "76a6a3d5-2697-367c-febe-f0c517857ff6",
92 | "headers": "Content-Type: application/vnd.api+json\nAccept: application/vnd.api+json\n",
93 | "url": "http://localhost:8000/api/admin/user/1",
94 | "preRequestScript": null,
95 | "pathVariables": {},
96 | "method": "DELETE",
97 | "data": null,
98 | "dataMode": "params",
99 | "version": 2,
100 | "tests": "tests[\"Content-Type is application/vnd.api+json\"] = postman.getResponseHeader(\"Content-Type\");",
101 | "currentHelper": "normal",
102 | "helperAttributes": {},
103 | "time": 1458450685116,
104 | "name": "Delete User",
105 | "description": "",
106 | "collectionId": "e34ec7c9-8419-ac7e-d344-2eac7e6cbe71",
107 | "responses": []
108 | },
109 | {
110 | "id": "7b8f4299-fb53-992e-b94d-1cd1c05f4528",
111 | "headers": "Content-Type: application/vnd.api+json\nAccept: application/vnd.api+json\n",
112 | "url": "http://localhost:8000/api/admin/user?fields[profiles]=id&include=profiles&sort=-id&adf",
113 | "preRequestScript": null,
114 | "pathVariables": {},
115 | "method": "GET",
116 | "data": null,
117 | "dataMode": "params",
118 | "tests": "\nvar jsonData = JSON.parse(responseBody);\ntests[\"Your test name\"] = jsonData.errors.title = \"Invalid Query Parameter\"\n\ntests[\"Status code is 400\"] = responseCode.code === 400;",
119 | "currentHelper": "normal",
120 | "helperAttributes": {},
121 | "time": 1458451657752,
122 | "name": "Bad Query Param",
123 | "description": "",
124 | "collectionId": "e34ec7c9-8419-ac7e-d344-2eac7e6cbe71",
125 | "responses": [],
126 | "folder": "6cafb725-c73c-ca4c-47af-c12ac1250db0"
127 | },
128 | {
129 | "id": "847e81b7-2f7e-7196-598b-c7079b94e06d",
130 | "headers": "Content-Type: application/vnd.api+json\nAccept: application/vnd.api+json\n",
131 | "url": "http://localhost:8000/api/admin/user?include=profiles",
132 | "preRequestScript": "",
133 | "pathVariables": {},
134 | "method": "GET",
135 | "data": [],
136 | "dataMode": "params",
137 | "tests": "tests[\"Body Content-Type\"] = \"application/vnd.api+json\"\n\ntests[\"Status code is 200\"] = responseCode.code === 200;\n\n",
138 | "currentHelper": "normal",
139 | "helperAttributes": {},
140 | "time": 1457906683361,
141 | "name": "List Pagination Sort",
142 | "description": "",
143 | "collectionId": "e34ec7c9-8419-ac7e-d344-2eac7e6cbe71",
144 | "responses": []
145 | },
146 | {
147 | "id": "8ce49bc4-4b3d-4da2-0a38-624bbdb2a844",
148 | "headers": "Content-Type: application/vnd.api+json\nAccept: application/vnd.api+json\n",
149 | "url": "http://localhost:8000/api/admin/user/1",
150 | "preRequestScript": "",
151 | "pathVariables": {},
152 | "method": "GET",
153 | "data": [],
154 | "dataMode": "params",
155 | "tests": "tests[\"Body Content-Type\"] = \"application/vnd.api+json\"\n\ntests[\"Status code is 200\"] = responseCode.code === 200;\n\n",
156 | "currentHelper": "normal",
157 | "helperAttributes": {},
158 | "time": 1458451647615,
159 | "name": "Get User",
160 | "description": "",
161 | "collectionId": "e34ec7c9-8419-ac7e-d344-2eac7e6cbe71",
162 | "responses": []
163 | },
164 | {
165 | "id": "a2751967-0b45-0868-cc4d-370658e70470",
166 | "headers": "Accept: application/vnd.api+json\nContent-Type: application/vnd.api+json\n",
167 | "url": "http://localhost:8000/api/admin/user",
168 | "preRequestScript": null,
169 | "pathVariables": {},
170 | "method": "POST",
171 | "data": [],
172 | "dataMode": "raw",
173 | "tests": null,
174 | "currentHelper": "normal",
175 | "helperAttributes": {},
176 | "time": 1458409064024,
177 | "name": "Create User",
178 | "description": "",
179 | "collectionId": "e34ec7c9-8419-ac7e-d344-2eac7e6cbe71",
180 | "responses": [],
181 | "rawModeData": "{\r\n \"data\": {\r\n \"type\": \"users\",\r\n \"attributes\": {\r\n \"name\": \"Ember Hamster\",\r\n \"email\": \"test@test.com\"\r\n }\r\n }\r\n}"
182 | },
183 | {
184 | "id": "b15ccc96-82d6-c838-ea73-034836395500",
185 | "headers": "Content-Type: application/vnd.api+json\nAccept: application/vnd.api+json\n",
186 | "url": "http://localhost:8000/api/admin/user?fields[profiles]=id&include=profiles&sort=-id",
187 | "pathVariables": {},
188 | "preRequestScript": null,
189 | "method": "GET",
190 | "collectionId": "e34ec7c9-8419-ac7e-d344-2eac7e6cbe71",
191 | "data": null,
192 | "dataMode": "params",
193 | "name": "Includes & Profiles",
194 | "description": "",
195 | "descriptionFormat": "html",
196 | "time": 1457907027561,
197 | "version": 2,
198 | "responses": [],
199 | "tests": "tests[\"Status code is 200\"] = responseCode.code === 200;\n\n",
200 | "currentHelper": "normal",
201 | "helperAttributes": {}
202 | },
203 | {
204 | "id": "d37258a7-8074-a30d-9926-84363d7578ea",
205 | "headers": "Accept: application/vnd.api+json\nContent-Type: application/vnd.api+json\n",
206 | "url": "http://localhost:8000/api/admin/user/1",
207 | "preRequestScript": null,
208 | "pathVariables": {},
209 | "method": "PATCH",
210 | "data": [],
211 | "dataMode": "raw",
212 | "tests": null,
213 | "currentHelper": "normal",
214 | "helperAttributes": {},
215 | "time": 1458450227925,
216 | "name": "Edit User",
217 | "description": "",
218 | "collectionId": "e34ec7c9-8419-ac7e-d344-2eac7e6cbe71",
219 | "responses": [],
220 | "rawModeData": "{\r\n \"data\": {\r\n \"type\": \"users\",\r\n \"attributes\": {\r\n \"name\": \"Blue Hamster\"\r\n }\r\n }\r\n}"
221 | },
222 | {
223 | "id": "e8755e27-4396-cf48-5d77-4e20d6b4906e",
224 | "headers": "Content-Type: application/vnd.api+json\nAccept: application/vnd.api+json\n",
225 | "url": "http://localhost:8000/api/admin/user?page[size]=1&page[number]=1",
226 | "preRequestScript": null,
227 | "pathVariables": {},
228 | "method": "GET",
229 | "data": null,
230 | "dataMode": "params",
231 | "tests": "tests[\"Body Content-Type\"] = \"application/vnd.api+json\"\n\n\ntests[\"Status code is 200\"] = responseCode.code === 200;",
232 | "currentHelper": "normal",
233 | "helperAttributes": {},
234 | "time": 1458489164991,
235 | "name": "List Pagination",
236 | "description": " ",
237 | "collectionId": "e34ec7c9-8419-ac7e-d344-2eac7e6cbe71",
238 | "responses": []
239 | },
240 | {
241 | "id": "f04d11a1-3e9b-0213-ed77-6d8f7db91d38",
242 | "headers": "Content-Type: application/vnd.api+json\nAccept: application/vnd.api+json\n",
243 | "url": "http://localhost:8000/api/admin/user/404",
244 | "preRequestScript": null,
245 | "pathVariables": {},
246 | "method": "DELETE",
247 | "data": null,
248 | "dataMode": "params",
249 | "version": 2,
250 | "tests": "tests[\"Content-Type is application/vnd.api+json\"] = postman.getResponseHeader(\"Content-Type\");",
251 | "currentHelper": "normal",
252 | "helperAttributes": {},
253 | "time": 1458450672722,
254 | "name": "Delete Bad User",
255 | "description": "",
256 | "collectionId": "e34ec7c9-8419-ac7e-d344-2eac7e6cbe71",
257 | "responses": [],
258 | "folder": "6cafb725-c73c-ca4c-47af-c12ac1250db0"
259 | }
260 | ]
261 | }
--------------------------------------------------------------------------------
/tests/SeeOrSaveJsonStructure.php:
--------------------------------------------------------------------------------
1 | setup();
17 |
18 | $file = rtrim(env('RESPONSE_FOLDER'), '\\/').DIRECTORY_SEPARATOR.class_basename(debug_backtrace()[1]['class']).'-'.debug_backtrace()[1]['function'].'.json';
19 |
20 | if (! env('SAVE_RESPONSES', false) && file_exists($file)) {
21 | $this->seeJsonStructure(json_decode(file_get_contents($file), true));
22 |
23 | return $this;
24 | }
25 |
26 | file_put_contents($file, json_encode($this->getKeys($this->response->getContent())));
27 |
28 | return $this;
29 | }
30 |
31 | /**
32 | * Create the response folder.
33 | *
34 | * @return void
35 | */
36 | private function setup()
37 | {
38 | if (! is_dir(env('RESPONSE_FOLDER'))) {
39 | mkdir(env('RESPONSE_FOLDER'), 0600, true);
40 | }
41 | }
42 |
43 | /**
44 | * Return a array of keys.
45 | *
46 | * @param array $array
47 | *
48 | * @return array
49 | */
50 | private function arrayKeys($array)
51 | {
52 | $results = [];
53 | foreach ($array as $key => $value) {
54 | if (is_array($value)) {
55 | $results = array_merge($results, [$key => array_merge(array_keys($value), $this->arrayKeys($value))]);
56 | }
57 | }
58 |
59 | return $results;
60 | }
61 |
62 | /**
63 | * Get keys from a json array.
64 | *
65 | * @param string $content
66 | *
67 | * @return arrayKeys
68 | */
69 | private function getKeys($content)
70 | {
71 | return $this->arrayKeys(json_decode($content, true));
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/tests/UnitTestCase.php:
--------------------------------------------------------------------------------
1 | createUserRaw();
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/tests/UnitTests/ExceptionTest.php:
--------------------------------------------------------------------------------
1 | setExpectedException(\Askedio\Laravel5ApiController\Exceptions\BadRequestException::class);
12 | throw new \Askedio\Laravel5ApiController\Exceptions\BadRequestException();
13 | }
14 |
15 | public function testNoTemplateException()
16 | {
17 | $this->setExpectedException(\Askedio\Laravel5ApiController\Exceptions\BadRequestException::class);
18 | throw new \Askedio\Laravel5ApiController\Exceptions\BadRequestException('badtemplate');
19 | }
20 | }
21 |
--------------------------------------------------------------------------------