├── .github └── workflows │ └── run-tests.yml ├── .gitignore ├── README.md ├── composer.json ├── config └── jory.php ├── phpunit.xml ├── src ├── Attributes │ └── Attribute.php ├── Config │ ├── Config.php │ ├── Field.php │ ├── Filter.php │ ├── Relation.php │ ├── Sort.php │ └── Validator.php ├── Console │ ├── JoryResourceGenerateCommand.php │ └── stubs │ │ └── jory-resource.stub ├── Exceptions │ ├── JoryException.php │ ├── LaravelJoryCallException.php │ ├── LaravelJoryException.php │ ├── RegistrationNotFoundException.php │ └── ResourceNotFoundException.php ├── Facades │ └── Jory.php ├── Helpers │ ├── Base64Validator.php │ ├── CaseManager.php │ ├── FilterHelper.php │ ├── ResourceNameHelper.php │ └── SimilarTextFinder.php ├── Http │ ├── Controllers │ │ └── JoryController.php │ ├── Middleware │ │ └── SetJoryHandler.php │ └── routes.php ├── JoryBuilder.php ├── JoryManager.php ├── JoryResource.php ├── JoryServiceProvider.php ├── Meta │ ├── Metadata.php │ ├── QueryCount.php │ ├── Time.php │ ├── Total.php │ └── User.php ├── Parsers │ └── RequestParser.php ├── Register │ ├── AutoRegistrar.php │ ├── JoryResourcesRegister.php │ └── RegistersJoryResources.php ├── Responses │ ├── JoryMultipleResponse.php │ └── JoryResponse.php ├── Scopes │ ├── CallbackFilterScope.php │ ├── CallbackSortScope.php │ ├── FilterScope.php │ └── SortScope.php └── Traits │ ├── AppliesConfigToJory.php │ ├── ConvertsConfigToArray.php │ ├── ConvertsModelToArray.php │ ├── HandlesJoryFilters.php │ ├── HandlesJorySelects.php │ ├── HandlesJorySorts.php │ ├── LoadsJoryRelations.php │ └── ProcessesMetadata.php └── tests ├── Attributes └── SongDescription.php ├── AuthorizeTest.php ├── Base64Test.php ├── BaseTest.php ├── CamelCaseTest.php ├── ConfigTest.php ├── ConsoleOutput ├── Generated │ └── .gitignore └── Original │ ├── AlbumJoryResource.php │ ├── AlternateBandJoryResource.php │ ├── BandJoryResource.php │ ├── EmptyJoryResource.php │ ├── ImageJoryResource.php │ ├── PersonJoryResource.php │ └── UserJoryResource.php ├── ConsoleTest.php ├── ControllerUsageTest.php ├── Controllers ├── BandController.php └── SongWithConfigController.php ├── CustomAttributeTest.php ├── ExistsTest.php ├── ExplicitSelectTest.php ├── FacadeTest.php ├── FieldsTest.php ├── FilterTest.php ├── FirstTest.php ├── JoryRegisterTest.php ├── JoryResources ├── AutoRegistered │ ├── AlbumCoverJoryResource.php │ ├── AlbumJoryResource.php │ ├── BandJoryResource.php │ ├── ImageJoryResource.php │ ├── InstrumentJoryResource.php │ ├── PersonJoryResource.php │ ├── SongJoryResource.php │ ├── TagJoryResource.php │ └── UnrelevantFileForAutoRegistrarTest.txt └── Unregistered │ ├── AlbumCoverJoryResourceWithExplicitSelect.php │ ├── AlbumCoverJoryResourceWithoutRoutes.php │ ├── AlbumJoryResourceWithExplicitSelect.php │ ├── BandJoryResourceWithExplicitSelect.php │ ├── CustomSongJoryResource.php │ ├── CustomSongJoryResource2.php │ ├── ImageJoryResourceWithExplicitSelect.php │ ├── InstrumentJoryResourceWithExplicitSelect.php │ ├── PersonJoryResourceWithCallables.php │ ├── PersonJoryResourceWithExplicitSelect.php │ ├── PersonJoryResourceWithScopes.php │ ├── SongJoryResourceWithAlternateUri.php │ ├── SongJoryResourceWithConfig.php │ ├── SongJoryResourceWithConfigThree.php │ ├── SongJoryResourceWithConfigTwo.php │ ├── SongJoryResourceWithExplicitSelect.php │ └── TagJoryResourceWithExplicitSelect.php ├── JoryRoutesTest.php ├── MetadataTest.php ├── Models ├── AlbumCover.php ├── Band.php ├── Groupie.php ├── Image.php ├── Instrument.php ├── Model.php ├── ModelWithoutJoryResource.php ├── Person.php ├── Song.php ├── SongWithCustomJoryResource.php ├── SubFolder │ ├── Album.php │ └── NonModelClass.php ├── Tag.php └── User.php ├── MultipleResponseTest.php ├── OffsetLimitTest.php ├── Parsers └── RequestParserTest.php ├── RegisterTest.php ├── RelationTest.php ├── ResponseTest.php ├── Scopes ├── AlbumCoverAlbumNameSort.php ├── AlbumNameFilter.php ├── AlphabeticNameSort.php ├── BandNameSort.php ├── CustomFilterFieldFilter.php ├── CustomSortFieldSort.php ├── FirstNameSort.php ├── FullNameFilter.php ├── HasAlbumWithNameFilter.php ├── HasSmallIdFilter.php ├── HasSongWithTitleFilter.php ├── NameFilter.php ├── NumberOfAlbumsInYearFilter.php ├── NumberOfSongsFilter.php ├── NumberOfSongsSort.php ├── SongAlbumNameSort.php └── SpecialFirstNameFilter.php ├── SnakeCaseTest.php ├── SortTest.php ├── TestCase.php └── WithConfigTest.php /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: "Run Tests" 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | fail-fast: true 12 | matrix: 13 | php: [8.1, 8.3] 14 | dependency-version: [prefer-lowest, prefer-stable] 15 | 16 | name: PHP${{ matrix.php }} ${{ matrix.dependency-version }} 17 | 18 | steps: 19 | - name: Setup PHP 20 | uses: shivammathur/setup-php@v2 21 | with: 22 | php-version: ${{ matrix.php }} 23 | coverage: none 24 | - name: Checkout code 25 | uses: actions/checkout@v2 26 | - name: Install Composer Dependencies 27 | run: composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction --no-suggest 28 | - name: Execute tests 29 | run: vendor/bin/phpunit 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | .DS_Store 3 | .idea/ 4 | composer.lock 5 | .phpunit.result.cache -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Run Tests](https://github.com/joskolenberg/laravel-jory/workflows/Run%20Tests/badge.svg) 2 | [![Total Downloads](https://poser.pugx.org/joskolenberg/laravel-jory/downloads)](https://packagist.org/packages/joskolenberg/laravel-jory) 3 | [![Latest Stable Version](https://poser.pugx.org/joskolenberg/laravel-jory/v/stable)](https://packagist.org/packages/joskolenberg/laravel-jory) 4 | [![License](https://poser.pugx.org/joskolenberg/laravel-jory/license)](https://packagist.org/packages/joskolenberg/laravel-jory) 5 | 6 | # Laravel-Jory: Flexible Eloquent API Resources 7 | [Complete documentation](https://laravel-jory.kolenberg.net/docs) 8 | 9 | 10 | ## Concept Overview 11 | Laravel Jory creates a dynamic API for your Laravel application to serve the data from your Eloquent models. 12 | JoryResources are comparable to Laravel's built-in Resource classes but you only write (or [generate](https://laravel-jory.kolenberg.net/docs/3.0/generator)) a JoryResource once for each model. Next, your data can be queried in a flexible way by passing a [Jory Query](https://laravel-jory.kolenberg.net/docs/3.0/fetching_introduction) to the [Jory Endpoints](https://laravel-jory.kolenberg.net/docs/3.0/endpoints). 13 | 14 | 15 | Jory is designed to be simple enough to master within minutes but flexible enough to fit 95% of your data-fetching use-cases. It brings Eloquent Query Builder's most-used features directly to your frontend. 16 | 17 | 18 | 19 | ## Supported Functions 20 | ### Querying 21 | - [Selecting fields](https://laravel-jory.kolenberg.net/docs/3.0/query_fields) (database fields & custom attributes) 22 | - [Filtering](https://laravel-jory.kolenberg.net/docs/3.0/query_filters) (including nested ```and``` and ```or``` clauses and custom filters) 23 | - [Sorting](https://laravel-jory.kolenberg.net/docs/3.0/query_sorts) (including custom sorts) 24 | - [Relations](https://laravel-jory.kolenberg.net/docs/3.0/query_relations) 25 | - [Offset & Limit](https://laravel-jory.kolenberg.net/docs/3.0/query_offset_and_limit) 26 | 27 | ### Endpoints 28 | - Fetch a [single record](https://laravel-jory.kolenberg.net/docs/3.0/endpoints#first) (like Laravel's ```first()```) 29 | - Fetch a [single record by id](https://laravel-jory.kolenberg.net/docs/3.0/endpoints#find) (like Laravel's ```find()```) 30 | - Fetch [multiple records](https://laravel-jory.kolenberg.net/docs/3.0/endpoints#get) (like Laravel's ```get()```) 31 | - Fetch [multiple resources at once](https://laravel-jory.kolenberg.net/docs/3.0/endpoints#multiple) 32 | 33 | ### Aggregates 34 | - [Count](https://laravel-jory.kolenberg.net/docs/3.0/endpoints#aggregates) 35 | - [Exists](https://laravel-jory.kolenberg.net/docs/3.0/endpoints#aggregates) 36 | 37 | ### Metadata 38 | - [Total records](https://laravel-jory.kolenberg.net/docs/3.0/metadata#total) (for pagination) 39 | - [Query count](https://laravel-jory.kolenberg.net/docs/3.0/metadata#query-count) 40 | 41 | 42 | For more information take a look at the [docs](https://laravel-jory.kolenberg.net/docs). 43 | 44 | 45 | Happy coding! 46 | 47 | Jos Kolenberg -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "joskolenberg/laravel-jory", 3 | "description": "Create a flexible API for your Laravel application using json based queries.", 4 | "license": "MIT", 5 | "keywords": ["query", "laravel", "api", "jory", "json", "filter", "sort", "relation", "model", "resource"], 6 | "authors": [ 7 | { 8 | "name": "Jos Kolenberg", 9 | "email": "jos@kolenbergsoftwareontwikkeling.nl" 10 | } 11 | ], 12 | "minimum-stability": "stable", 13 | "require": { 14 | "laravel/framework": "^10.0|^11.0|^12.0", 15 | "joskolenberg/jory": "^2.0", 16 | "joskolenberg/eloquent-reflector": "^2.1" 17 | }, 18 | "autoload": { 19 | "psr-4": { 20 | "JosKolenberg\\LaravelJory\\": "src/" 21 | } 22 | }, 23 | "autoload-dev": { 24 | "psr-4": { 25 | "JosKolenberg\\LaravelJory\\Tests\\": "tests/" 26 | } 27 | }, 28 | "require-dev": { 29 | "orchestra/testbench": "^8.0" 30 | }, 31 | "extra": { 32 | "laravel": { 33 | "providers": [ 34 | "JosKolenberg\\LaravelJory\\JoryServiceProvider" 35 | ] 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | tests 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/Attributes/Attribute.php: -------------------------------------------------------------------------------- 1 | field = $field; 64 | $this->getter = $getter; 65 | 66 | $this->case = app(CaseManager::class); 67 | } 68 | 69 | /** 70 | * Set the field to be hidden by default. 71 | * 72 | * @return $this 73 | */ 74 | public function hideByDefault(): Field 75 | { 76 | $this->showByDefault = false; 77 | 78 | return $this; 79 | } 80 | 81 | /** 82 | * Set the fields to be selected in the query. 83 | * 84 | * @param string|array $fields 85 | * @return Field 86 | */ 87 | public function select(...$fields): Field 88 | { 89 | $this->select = is_array($fields[0]) ? $fields[0] : $fields; 90 | 91 | return $this; 92 | } 93 | 94 | /** 95 | * Tell the query to select no fields in the query when this field is requested. 96 | * 97 | * @return Field 98 | */ 99 | public function noSelect(): Field 100 | { 101 | $this->select([]); 102 | 103 | return $this; 104 | } 105 | 106 | /** 107 | * Set the relations to be loaded for this field. 108 | * 109 | * @param mixed $relations 110 | * @return Field 111 | */ 112 | public function load($relations): Field 113 | { 114 | if (is_string($relations)) { 115 | $relations = func_get_args(); 116 | } 117 | 118 | $this->load = $relations; 119 | 120 | return $this; 121 | } 122 | 123 | /** 124 | * Get the field (name) in the current case. 125 | * 126 | * @return string 127 | */ 128 | public function getField(): string 129 | { 130 | return $this->case->toCurrent($this->field); 131 | } 132 | 133 | /** 134 | * Get the field (name) as configured. 135 | * 136 | * @return string 137 | */ 138 | public function getOriginalField(): string 139 | { 140 | return $this->field; 141 | } 142 | 143 | /** 144 | * Get the fields to be selected in the query. 145 | * 146 | * @return null|array 147 | */ 148 | public function getSelect():? array 149 | { 150 | return $this->select; 151 | } 152 | 153 | /** 154 | * Get the relations to be loaded for this field. 155 | * 156 | * @return null|array 157 | */ 158 | public function getEagerLoads():? array 159 | { 160 | return $this->load; 161 | } 162 | 163 | /** 164 | * Get the optional custom getter instance. 165 | * 166 | * @return null|Attribute 167 | */ 168 | public function getGetter():? Attribute 169 | { 170 | return $this->getter; 171 | } 172 | 173 | /** 174 | * Tell if this field should be shown by default. 175 | * 176 | * @return bool 177 | */ 178 | public function isShownByDefault(): bool 179 | { 180 | return $this->showByDefault; 181 | } 182 | 183 | /** 184 | * Mark this field to be filterable. 185 | * 186 | * @param callable|null $callback 187 | * @return $this 188 | */ 189 | public function filterable($callback = null): Field 190 | { 191 | $this->filter = new Filter($this->field); 192 | 193 | if (is_callable($callback)) { 194 | call_user_func($callback, $this->filter); 195 | } 196 | 197 | return $this; 198 | } 199 | 200 | /** 201 | * Get the filter. 202 | * 203 | * @return Filter|null 204 | */ 205 | public function getFilter(): ?Filter 206 | { 207 | return $this->filter; 208 | } 209 | 210 | /** 211 | * Mark this field to be sortable. 212 | * 213 | * @param callable|null $callback 214 | * @return $this 215 | */ 216 | public function sortable($callback = null): Field 217 | { 218 | $this->sort = new Sort($this->field); 219 | 220 | if (is_callable($callback)) { 221 | call_user_func($callback, $this->sort); 222 | } 223 | 224 | return $this; 225 | } 226 | 227 | /** 228 | * Get the sort. 229 | * 230 | * @return Sort|null 231 | */ 232 | public function getSort(): ?Sort 233 | { 234 | return $this->sort; 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/Config/Filter.php: -------------------------------------------------------------------------------- 1 | name = $name; 45 | $this->scope = $scope; 46 | $this->operators = config('jory.filters.operators'); 47 | 48 | $this->case = app(CaseManager::class); 49 | } 50 | 51 | /** 52 | * Set the filter's available operators. 53 | * 54 | * @param array $operators 55 | * @return $this 56 | */ 57 | public function operators(array $operators): Filter 58 | { 59 | $this->operators = $operators; 60 | 61 | return $this; 62 | } 63 | 64 | /** 65 | * Set the filter's scope class. 66 | * 67 | * @param FilterScope|callable $scope 68 | * @return $this 69 | */ 70 | public function scope(FilterScope|callable $scope = null): Filter 71 | { 72 | if(is_callable($scope)){ 73 | $scope = new CallbackFilterScope($scope); 74 | } 75 | 76 | $this->scope = $scope; 77 | 78 | return $this; 79 | } 80 | 81 | /** 82 | * Get the filter's name in the current case. 83 | * 84 | * @return string 85 | */ 86 | public function getName(): string 87 | { 88 | return $this->case->toCurrent($this->name); 89 | } 90 | 91 | /** 92 | * Get the field to filter on. 93 | * 94 | * This is always the name of the configured filter 95 | * unless a custom FilterScope is applied. 96 | * 97 | * @return string 98 | */ 99 | public function getField(): string 100 | { 101 | return $this->name; 102 | } 103 | 104 | /** 105 | * Get the filter's optional scope class. 106 | * 107 | * @return FilterScope|null 108 | */ 109 | public function getScope():? FilterScope 110 | { 111 | return $this->scope; 112 | } 113 | 114 | /** 115 | * Get the available operators. 116 | * 117 | * @return array 118 | */ 119 | public function getOperators(): array 120 | { 121 | return $this->operators; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/Config/Relation.php: -------------------------------------------------------------------------------- 1 | name = $name; 47 | $this->parentClass = $parentClass; 48 | $this->joryResource = $joryResource; 49 | 50 | $this->case = app(CaseManager::class); 51 | } 52 | 53 | /** 54 | * Get the relation's name in the current case. 55 | * 56 | * @return string 57 | */ 58 | public function getName(): string 59 | { 60 | return $this->case->toCurrent($this->name); 61 | } 62 | 63 | /** 64 | * Get the relation name as configured (which should be the actual relation name) 65 | * 66 | * @return string 67 | */ 68 | public function getOriginalName(): string 69 | { 70 | return $this->name; 71 | } 72 | 73 | /** 74 | * Get the related joryResource. 75 | * 76 | * @return JoryResource 77 | */ 78 | public function getJoryResource(): JoryResource 79 | { 80 | if (!$this->joryResource) { 81 | /** 82 | * When no explicit joryResource is given, 83 | * we will search for the joryResource for the related model. 84 | */ 85 | $relationMethod = Str::camel($this->name); 86 | 87 | $relatedClass = get_class((new $this->parentClass)->{$relationMethod}()->getRelated()); 88 | 89 | $this->joryResource = app()->make(JoryResourcesRegister::class)->getByModelClass($relatedClass); 90 | } 91 | 92 | return $this->joryResource; 93 | } 94 | 95 | /** 96 | * Get the related model type. 97 | * 98 | * @return null|string 99 | */ 100 | public function getType(): ?string 101 | { 102 | return $this->getJoryResource()->getUri(); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Config/Sort.php: -------------------------------------------------------------------------------- 1 | name = $name; 50 | $this->scope = $scope; 51 | 52 | $this->case = app(CaseManager::class); 53 | } 54 | 55 | /** 56 | * Set the sort's scope class. 57 | * 58 | * @param SortScope|callable $scope 59 | * @return $this 60 | */ 61 | public function scope(SortScope|callable $scope = null): Sort 62 | { 63 | if(is_callable($scope)){ 64 | $scope = new CallbackSortScope($scope); 65 | } 66 | 67 | $this->scope = $scope; 68 | 69 | return $this; 70 | } 71 | 72 | /** 73 | * Get the fields name in the current case. 74 | * 75 | * @return string 76 | */ 77 | public function getName(): string 78 | { 79 | return $this->case->toCurrent($this->name); 80 | } 81 | 82 | /** 83 | * Get the field to sort on. 84 | * 85 | * This is always the name of the configured sort 86 | * unless a custom SortScope is applied. 87 | * 88 | * @return string 89 | */ 90 | public function getField(): string 91 | { 92 | return $this->name; 93 | } 94 | 95 | /** 96 | * Get the sort's optional scope class. 97 | * 98 | * @return SortScope|null 99 | */ 100 | public function getScope():? SortScope 101 | { 102 | return $this->scope; 103 | } 104 | 105 | /** 106 | * Mark this sort to be applied by default. 107 | * 108 | * @param int $index 109 | * @param string $order 110 | * @return $this 111 | */ 112 | public function default(int $index = 0, string $order = 'asc'): Sort 113 | { 114 | $this->defaultIndex = $index; 115 | $this->defaultOrder = $order; 116 | 117 | return $this; 118 | } 119 | 120 | /** 121 | * Get the index for default sorting. 122 | * Null means there won't be sorted on this field by default. 123 | * 124 | * @return int|null 125 | */ 126 | public function getDefaultIndex(): ? int 127 | { 128 | return $this->defaultIndex; 129 | } 130 | 131 | /** 132 | * Get the sort order ('asc' or 'desc') if this sort needs to be applied by default. 133 | * 134 | * @return string|null 135 | */ 136 | public function getDefaultOrder(): string 137 | { 138 | return $this->defaultOrder; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/Console/stubs/jory-resource.stub: -------------------------------------------------------------------------------- 1 | original = $original; 26 | } 27 | 28 | public function render(Request $request): Response 29 | { 30 | return response([ 31 | config('jory.response.errors-key') => [ 32 | $this->original->getMessage(), 33 | ], 34 | ], 422); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Exceptions/LaravelJoryCallException.php: -------------------------------------------------------------------------------- 1 | errors = $errors; 24 | 25 | parent::__construct(implode(', ', $errors)); 26 | } 27 | 28 | /** 29 | * @return array 30 | */ 31 | public function getErrors(): array 32 | { 33 | return $this->errors; 34 | } 35 | 36 | public function render(Request $request): Response 37 | { 38 | $responseKey = config('jory.response.errors-key'); 39 | $response = $responseKey === null ? $this->getErrors() : [$responseKey => $this->getErrors()]; 40 | 41 | return response($response, 422); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Exceptions/LaravelJoryException.php: -------------------------------------------------------------------------------- 1 | make(JoryResourcesRegister::class); 23 | 24 | $message = 'Resource ' . $resource . ' not found, ' . $this->getSuggestion($register->getUrisArray(), $resource); 25 | 26 | parent::__construct($message); 27 | } 28 | 29 | /** 30 | * Get the 'Did you mean?' line for the best match in an array of strings. 31 | * 32 | * @param array $array 33 | * @param string $value 34 | * @return string 35 | */ 36 | protected function getSuggestion(array $array, string $value): string 37 | { 38 | $bestMatch = (new SimilarTextFinder($value, $array))->threshold(4)->first(); 39 | 40 | return $bestMatch ? 'did you mean "' . $bestMatch . '"?' : 'no suggestions found.'; 41 | } 42 | 43 | public function render(Request $request): Response 44 | { 45 | return response([ 46 | config('jory.response.errors-key') => [ 47 | $this->getMessage(), 48 | ], 49 | ], 404); 50 | } 51 | } -------------------------------------------------------------------------------- /src/Facades/Jory.php: -------------------------------------------------------------------------------- 1 | case = config('jory.case'); 29 | 30 | $inputCase = $request->input(config('jory.request.case-key')); 31 | if (in_array($inputCase, ['default', 'snake', 'camel'])) { 32 | $this->case = $inputCase; 33 | } 34 | } 35 | 36 | /** 37 | * Update a string to the current case mode. 38 | * 39 | * @param $string 40 | * @return string 41 | */ 42 | public function toCurrent($string): string 43 | { 44 | if ($this->case === 'camel') { 45 | return Str::camel($string); 46 | } 47 | 48 | if ($this->case === 'snake') { 49 | return Str::snake($string); 50 | } 51 | 52 | return $string; 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/Helpers/FilterHelper.php: -------------------------------------------------------------------------------- 1 | whereNull($field); 23 | 24 | return; 25 | case 'not_null': 26 | $builder->whereNotNull($field); 27 | 28 | return; 29 | case 'in': 30 | $builder->whereIn($field, $data); 31 | 32 | return; 33 | case 'not_in': 34 | $builder->whereNotIn($field, $data); 35 | 36 | return; 37 | case 'not_like': 38 | $builder->where($field, 'not like', $data); 39 | 40 | return; 41 | default: 42 | $builder->where($field, $operator ?: '=', $data); 43 | } 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /src/Helpers/ResourceNameHelper.php: -------------------------------------------------------------------------------- 1 | baseName = $baseName; 60 | $result->alias = $alias; 61 | $result->type = $type; 62 | $result->id = $id; 63 | 64 | return $result; 65 | } 66 | 67 | } -------------------------------------------------------------------------------- /src/Helpers/SimilarTextFinder.php: -------------------------------------------------------------------------------- 1 | needle = $needle; 44 | $this->haystack = $haystack; 45 | } 46 | 47 | /** 48 | * Sort Haystack. 49 | * 50 | * @return void 51 | */ 52 | protected function sortHaystack() 53 | { 54 | $sorted_haystack = []; 55 | foreach ($this->haystack as $string) { 56 | $sorted_haystack[$string] = $this->levenshteinUtf8($this->needle, $string); 57 | } 58 | 59 | // Apply threshold when set. 60 | if(!is_null($this->threshold)){ 61 | $sorted_haystack = array_filter($sorted_haystack, function ($score){ 62 | return $score <= $this->threshold; 63 | }); 64 | } 65 | 66 | asort($sorted_haystack); 67 | 68 | $this->sorted_haystack = $sorted_haystack; 69 | } 70 | 71 | /** 72 | * Apply threshold to filter only relevant results. The higher 73 | * the threshold the more results there will be returned. 74 | * 75 | * @param int|null $threshold 76 | * @return Finder 77 | */ 78 | public function threshold($threshold = null) 79 | { 80 | $this->threshold = $threshold; 81 | 82 | return $this; 83 | } 84 | 85 | /** 86 | * Return the highest match. 87 | * 88 | * @return mixed 89 | */ 90 | public function first() 91 | { 92 | $this->sortHaystack(); 93 | reset($this->sorted_haystack); 94 | return key($this->sorted_haystack); 95 | } 96 | 97 | /** 98 | * Return all strings in sorted match order. 99 | * 100 | * @return array 101 | */ 102 | public function all() 103 | { 104 | $this->sortHaystack(); 105 | return array_keys($this->sorted_haystack); 106 | } 107 | 108 | /** 109 | * Return whether there is an exact match. 110 | * 111 | * @return bool 112 | */ 113 | public function hasExactMatch() 114 | { 115 | return in_array($this->needle, $this->haystack); 116 | } 117 | 118 | /** 119 | * Ensure a string only uses ascii characters. 120 | * 121 | * @param string $str 122 | * @param array $map 123 | * @return string 124 | */ 125 | protected function utf8ToExtendedAscii($str, &$map) 126 | { 127 | // Find all multi-byte characters (cf. utf-8 encoding specs). 128 | $matches = array(); 129 | if (!preg_match_all('/[\xC0-\xF7][\x80-\xBF]+/', $str, $matches)) { 130 | return $str; // plain ascii string 131 | } 132 | 133 | // Update the encoding map with the characters not already met. 134 | foreach ($matches[0] as $mbc) { 135 | if (!isset($map[$mbc])) { 136 | $map[$mbc] = chr(128 + count($map)); 137 | } 138 | } 139 | 140 | // Finally remap non-ascii characters. 141 | return strtr($str, $map); 142 | } 143 | 144 | /** 145 | * Calculate the levenshtein distance between two strings. 146 | * 147 | * @param string $string1 148 | * @param string $string2 149 | * @return int 150 | */ 151 | protected function levenshteinUtf8($string1, $string2) 152 | { 153 | $charMap = array(); 154 | $string1 = $this->utf8ToExtendedAscii($string1, $charMap); 155 | $string2 = $this->utf8ToExtendedAscii($string2, $charMap); 156 | 157 | return levenshtein($string1, $string2); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/Http/Controllers/JoryController.php: -------------------------------------------------------------------------------- 1 | explicit(false); 20 | } 21 | 22 | /** 23 | * Count the number of items in a resource. 24 | * 25 | * @param string $resource 26 | * @return JoryResponse 27 | */ 28 | public function count(string $resource) 29 | { 30 | return Jory::byUri($resource)->explicit(false)->count(); 31 | } 32 | 33 | /** 34 | * Tell if a record exists. 35 | * 36 | * @param string $resource 37 | * @return JoryResponse 38 | */ 39 | public function exists(string $resource) 40 | { 41 | return Jory::byUri($resource)->explicit(false)->exists(); 42 | } 43 | 44 | /** 45 | * Give a single record by id. 46 | * 47 | * @param string $resource 48 | * @param $id 49 | * @return JoryResponse 50 | */ 51 | public function find(string $resource, $id) 52 | { 53 | return Jory::byUri($resource)->explicit(false)->find($id); 54 | } 55 | 56 | /** 57 | * Give the first record by filter and sort parameters. 58 | * 59 | * @param string $resource 60 | * @return JoryResponse 61 | */ 62 | public function first(string $resource) 63 | { 64 | return Jory::byUri($resource)->explicit(false)->first(); 65 | } 66 | 67 | /** 68 | * Load multiple resources at once. 69 | * 70 | */ 71 | public function multiple() 72 | { 73 | return Jory::multiple(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Http/Middleware/SetJoryHandler.php: -------------------------------------------------------------------------------- 1 | name('jory.multiple'); 8 | 9 | // Routes by resource 10 | Route::get('/{resource}/count', [JoryController::class, 'count'])->name('jory.count'); 11 | Route::get('/{resource}/exists', [JoryController::class, 'exists'])->name('jory.exists'); 12 | Route::get('/{resource}/first', [JoryController::class, 'first'])->name('jory.first'); 13 | Route::get('/{resource}/{id}', [JoryController::class, 'find'])->name('jory.find'); 14 | Route::get('/{resource}', [JoryController::class, 'get'])->name('jory.get'); 15 | -------------------------------------------------------------------------------- /src/JoryBuilder.php: -------------------------------------------------------------------------------- 1 | joryResource = $joryResource; 38 | } 39 | 40 | /** 41 | * Set a builder instance to build the query upon. 42 | * 43 | * @param Builder $builder 44 | * 45 | * @return JoryBuilder 46 | */ 47 | public function onQuery(Builder $builder): self 48 | { 49 | $this->builder = $builder; 50 | 51 | return $this; 52 | } 53 | 54 | /** 55 | * Get a collection of Models based on the baseQuery and Jory query. 56 | * 57 | * @return Collection 58 | */ 59 | public function get(): Collection 60 | { 61 | $collection = $this->buildQuery()->get(); 62 | 63 | $this->loadRelations($collection, $this->joryResource); 64 | 65 | return $collection; 66 | } 67 | 68 | /** 69 | * Get the first Model based on the baseQuery and Jory data. 70 | * 71 | * @return Model|null 72 | */ 73 | public function getFirst(): ?Model 74 | { 75 | $model = $this->buildQuery()->first(); 76 | 77 | if (!$model) { 78 | return null; 79 | } 80 | 81 | $this->loadRelations(new Collection([$model]), $this->joryResource); 82 | 83 | return $model; 84 | } 85 | 86 | /** 87 | * Count the records based on the filters in the Jory object. 88 | * 89 | * @return int 90 | */ 91 | public function getCount(): int 92 | { 93 | $builder = $this->builder; 94 | 95 | $this->applyOnCountQuery($builder); 96 | 97 | return $builder->count(); 98 | } 99 | 100 | /** 101 | * Tell if any record exists based on the filters in the Jory object. 102 | * 103 | * @return bool 104 | */ 105 | public function getExists(): bool 106 | { 107 | $builder = $this->builder; 108 | 109 | $this->applyOnCountQuery($builder); 110 | 111 | return $builder->exists(); 112 | } 113 | 114 | /** 115 | * Build a new query based on the baseQuery and Jory data. 116 | * 117 | * @return Builder 118 | */ 119 | public function buildQuery(): Builder 120 | { 121 | $builder = $this->builder; 122 | 123 | $this->applyOnQuery($builder); 124 | 125 | return $builder; 126 | } 127 | 128 | /** 129 | * Apply the jory data on an existing query. 130 | * 131 | * @param $builder 132 | * @return mixed 133 | */ 134 | public function applyOnQuery($builder) 135 | { 136 | $jory = $this->joryResource->getJory(); 137 | 138 | $this->applySelects($builder, $this->joryResource); 139 | 140 | // Apply filters if there are any 141 | if ($jory->getFilter()) { 142 | $this->applyFilter($builder, $this->joryResource); 143 | } 144 | 145 | $this->authorizeQuery($builder); 146 | 147 | $this->applySorts($builder, $this->joryResource); 148 | $this->applyOffsetAndLimit($builder, $jory->getOffset(), $jory->getLimit()); 149 | 150 | return $builder; 151 | } 152 | 153 | /** 154 | * Apply the jory data on an existing query. 155 | * 156 | * @param $builder 157 | * @return mixed 158 | */ 159 | public function applyOnCountQuery($builder) 160 | { 161 | // Apply filters if there are any 162 | if ($this->joryResource->getJory()->getFilter()) { 163 | $this->applyFilter($builder, $this->joryResource); 164 | } 165 | 166 | $this->authorizeQuery($builder); 167 | 168 | return $builder; 169 | } 170 | 171 | /** 172 | * Apply an offset and limit on the query. 173 | * 174 | * @param $builder 175 | * @param int|null $offset 176 | * @param int|null $limit 177 | */ 178 | protected function applyOffsetAndLimit($builder, int $offset = null, int $limit = null): void 179 | { 180 | if ($offset !== null) { 181 | // Check on null, so even 0 will be applied. 182 | // this can be overruled by the request this way. 183 | $builder->offset($offset); 184 | } 185 | if ($limit !== null) { 186 | $builder->limit($limit); 187 | } 188 | } 189 | 190 | /** 191 | * @param $builder 192 | * @return void 193 | */ 194 | protected function authorizeQuery($builder): void 195 | { 196 | $builder->where(function($builder) { 197 | $this->joryResource->authorize($builder, Auth::user()); 198 | }); 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/JoryManager.php: -------------------------------------------------------------------------------- 1 | make('request'), app()->make(JoryResourcesRegister::class)); 30 | } 31 | 32 | /** 33 | * Register a JoryResource using the facade. 34 | * 35 | * @param string|\JosKolenberg\LaravelJory\JoryResource $joryResource 36 | * @return JoryResourcesRegister 37 | */ 38 | public function register($joryResource): JoryResourcesRegister 39 | { 40 | if(is_string($joryResource)){ 41 | $joryResource = new $joryResource(); 42 | } 43 | 44 | return app()->make(JoryResourcesRegister::class)->add($joryResource); 45 | } 46 | 47 | /** 48 | * Create a new response based on the public uri. 49 | * 50 | * @param string $uri 51 | * @return JoryResponse 52 | */ 53 | public function byUri(string $uri): JoryResponse 54 | { 55 | return $this->getJoryResponse()->byUri($uri); 56 | } 57 | 58 | 59 | /** 60 | * Helper method to create a new response based on 61 | * a model instance, a model's class name or existing query. 62 | * 63 | * @param mixed $resource 64 | * @return JoryResponse 65 | */ 66 | public function on($resource): JoryResponse 67 | { 68 | $response = $this->getJoryResponse(); 69 | if($resource instanceof Model){ 70 | return $response->onModel($resource); 71 | } 72 | 73 | if($resource instanceof Builder){ 74 | return $response->onQuery($resource); 75 | } 76 | 77 | if(!is_string($resource)){ 78 | throw new LaravelJoryException('Unexpected type given. Please provide a model instance, Eloquent builder instance or a model\'s class name.'); 79 | } 80 | 81 | return $response->onModelClass($resource); 82 | } 83 | 84 | /** 85 | * Create a new response based on a model's class name. 86 | * 87 | * @param string $modelClass 88 | * @return JoryResponse 89 | */ 90 | public function onModelClass(string $modelClass): JoryResponse 91 | { 92 | return $this->getJoryResponse()->onModelClass($modelClass); 93 | } 94 | 95 | /** 96 | * Create a new response based on a model instance. 97 | * 98 | * @param Model $model 99 | * @return JoryResponse 100 | */ 101 | public function onModel(Model $model): JoryResponse 102 | { 103 | return $this->getJoryResponse()->onModel($model); 104 | } 105 | 106 | /** 107 | * Create a new response based on an existing query. 108 | * 109 | * @param Builder $builder 110 | * @return JoryResponse 111 | */ 112 | public function onQuery(Builder $builder): JoryResponse 113 | { 114 | return $this->getJoryResponse()->onQuery($builder); 115 | } 116 | 117 | /** 118 | * Get a fresh JoryResponse. 119 | * 120 | * @return JoryResponse 121 | */ 122 | protected function getJoryResponse(): JoryResponse 123 | { 124 | return new JoryResponse(app()->make('request'), app()->make(JoryResourcesRegister::class)); 125 | } 126 | 127 | } 128 | -------------------------------------------------------------------------------- /src/JoryServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 20 | __DIR__.'/../config/jory.php' => config_path('jory.php'), 21 | ]); 22 | 23 | if ($this->app->runningInConsole()) { 24 | $this->commands([ 25 | JoryResourceGenerateCommand::class, 26 | ]); 27 | } 28 | 29 | $this->registerRoutes(); 30 | } 31 | 32 | public function register() 33 | { 34 | $this->mergeConfigFrom(__DIR__.'/../config/jory.php', 'jory'); 35 | 36 | $this->app->singleton(JoryResourcesRegister::class, function () { 37 | $register = new JoryResourcesRegister(); 38 | 39 | foreach (config('jory.registrars') as $registrar){ 40 | $register->addRegistrar(new $registrar()); 41 | } 42 | 43 | return $register; 44 | }); 45 | 46 | $this->app->singleton(CaseManager::class, function ($app) { 47 | return new CaseManager($app->make('request')); 48 | }); 49 | 50 | $this->app->singleton('jory', function ($app) { 51 | return new JoryManager(); 52 | }); 53 | 54 | $this->app->bind(JoryBuilder::class, function ($app, $params){ 55 | return new JoryBuilder($params['joryResource']); 56 | }); 57 | 58 | $this->app->bind(Validator::class, function ($app, $params){ 59 | return new Validator($params['config'], $params['jory']); 60 | }); 61 | 62 | $this->app->bind(Config::class, function ($app, $params){ 63 | return new Config($params['modelClass']); 64 | }); 65 | } 66 | 67 | /** 68 | * Register the package routes. 69 | * 70 | * @return void 71 | */ 72 | private function registerRoutes(): void 73 | { 74 | if(config('jory.routes.enabled')){ 75 | Route::group($this->routeConfiguration(), function () { 76 | $this->loadRoutesFrom(__DIR__.'/Http/routes.php'); 77 | }); 78 | } 79 | } 80 | 81 | /** 82 | * Get the Jory route group configuration array. 83 | * 84 | * @return array 85 | */ 86 | private function routeConfiguration(): array 87 | { 88 | return [ 89 | 'prefix' => config('jory.routes.path'), 90 | 'middleware' => 'jory', 91 | ]; 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /src/Meta/Metadata.php: -------------------------------------------------------------------------------- 1 | request = $request; 22 | } 23 | 24 | /** 25 | * Get the return value for the metadata. 26 | * Called at the end of the request. 27 | * 28 | * @return mixed 29 | */ 30 | abstract public function get(); 31 | 32 | } -------------------------------------------------------------------------------- /src/Meta/QueryCount.php: -------------------------------------------------------------------------------- 1 | startTime = microtime(true); 26 | } 27 | 28 | /** 29 | * Get the return value for the metadata. 30 | * Called at the end of the request. 31 | * 32 | * @return mixed 33 | */ 34 | public function get() 35 | { 36 | return number_format(microtime(true) - $this->startTime, 4); 37 | } 38 | } -------------------------------------------------------------------------------- /src/Meta/Total.php: -------------------------------------------------------------------------------- 1 | getIndexCount(); 26 | } 27 | 28 | if($route === 'jory.multiple'){ 29 | return $this->getMultipleCount(); 30 | } 31 | 32 | return null; 33 | } 34 | 35 | /** 36 | * Get the total record count (including filtering) for an index request. 37 | * 38 | * @return mixed 39 | */ 40 | protected function getIndexCount(): int 41 | { 42 | $resource = $this->request->route('resource'); 43 | 44 | return Jory::byUri($resource)->count()->toArray(); 45 | } 46 | 47 | /** 48 | * Get the total record count (including filtering) for 49 | * all items in a request for multiple resources. 50 | * 51 | * @return array 52 | */ 53 | protected function getMultipleCount(): array 54 | { 55 | $data = $this->request->input(config('jory.request.key'), '{}'); 56 | 57 | $result = []; 58 | 59 | foreach ($data as $resourceName => $jory) { 60 | $resource = ResourceNameHelper::explode($resourceName); 61 | 62 | if($resource->type === 'multiple'){ 63 | $result[$resource->alias] = Jory::byUri($resource->baseName)->applyArray($jory)->count()->toArray(); 64 | } 65 | } 66 | 67 | return $result; 68 | } 69 | } -------------------------------------------------------------------------------- /src/Meta/User.php: -------------------------------------------------------------------------------- 1 | email; 21 | } 22 | } -------------------------------------------------------------------------------- /src/Parsers/RequestParser.php: -------------------------------------------------------------------------------- 1 | request = $request; 33 | } 34 | 35 | /** 36 | * Get the jory query object based on the given Request. 37 | * 38 | * @return Jory 39 | */ 40 | public function getJory(): Jory 41 | { 42 | $data = $this->request->input(config('jory.request.key'), '{}'); 43 | 44 | if (is_array($data)) { 45 | return (new ArrayParser($data))->getJory(); 46 | } 47 | 48 | if (Base64Validator::check($data)) { 49 | $data = base64_decode($data); 50 | } 51 | 52 | return (new JsonParser($data))->getJory(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Register/AutoRegistrar.php: -------------------------------------------------------------------------------- 1 | JoryResource naming convention 17 | */ 18 | class AutoRegistrar implements RegistersJoryResources 19 | { 20 | 21 | /** 22 | * The discovered JoryResources 23 | * 24 | * @var Collection 25 | */ 26 | protected $joryResources; 27 | 28 | /** 29 | * AutoRegistrar constructor. 30 | */ 31 | public function __construct() 32 | { 33 | $this->joryResources = new Collection(); 34 | 35 | $this->discoverJoryResources(); 36 | } 37 | 38 | /** 39 | * Get all registered registrations. 40 | * 41 | * @return Collection 42 | */ 43 | public function getJoryResources(): Collection 44 | { 45 | return $this->joryResources; 46 | } 47 | 48 | /** 49 | * Try to find the and register all the JoryResources 50 | */ 51 | protected function discoverJoryResources(): void 52 | { 53 | if(!file_exists(config('jory.auto-registrar.path'))){ 54 | return; 55 | } 56 | 57 | $files = (new Finder())->files()->in(config('jory.auto-registrar.path'))->depth('== 0'); 58 | 59 | foreach ($files as $file) { 60 | if ($file->getExtension() !== 'php') { 61 | continue; 62 | } 63 | 64 | $this->discoverJoryResourceForFile($file); 65 | } 66 | } 67 | 68 | /** 69 | * If we can find a JoryResource for the given file, we'll register it. 70 | * 71 | * @param SplFileInfo $file 72 | */ 73 | protected function discoverJoryResourceForFile(SplFileInfo $file): void 74 | { 75 | $className = $this->getClassNameFromFilePath($file->getRealPath()); 76 | 77 | $reflector = new \ReflectionClass($className); 78 | 79 | if($reflector->isSubclassOf(JoryResource::class)){ 80 | $this->joryResources->push(new $className()); 81 | } 82 | } 83 | 84 | /** 85 | * Convert the path name for a file to it's classname. 86 | * 87 | * @param $path 88 | * @return string 89 | */ 90 | protected function getClassNameFromFilePath($path): string 91 | { 92 | // Example; $path = /home/vagrant/code/project/app/Http/JoryResources/UserJoryResource.php 93 | 94 | $rootPath = config('jory.auto-registrar.path'); 95 | $rootNameSpace = config('jory.auto-registrar.namespace'); 96 | 97 | // Get filename relative to rootPath without extension, e.g. /Http/JoryResources/UserJoryResource 98 | $className = str_replace($rootPath, '', substr($path, 0, -4)); 99 | 100 | // Convert to backslashes and make all namespaces StudlyCased, e.g. \Http\JoryResources\UserJoryResource 101 | $className = collect(explode(DIRECTORY_SEPARATOR, $className)) 102 | ->map(function($namespace){ 103 | return Str::studly($namespace); 104 | }) 105 | ->implode('\\'); 106 | 107 | // Return the classname prefixed with the rootPath's namespace, e.g. \App\Http\JoryResources\UserJoryResource 108 | return $rootNameSpace . $className; 109 | } 110 | } -------------------------------------------------------------------------------- /src/Register/JoryResourcesRegister.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | protected $registrars = []; 26 | 27 | /** 28 | * JoryResourcesRegister constructor. 29 | */ 30 | public function __construct() 31 | { 32 | $this->joryResources = new Collection(); 33 | } 34 | 35 | /** 36 | * Manually register a JoryResource. 37 | * 38 | * @param JoryResource $joryResource 39 | * @return JoryResourcesRegister 40 | */ 41 | public function add(JoryResource $joryResource): JoryResourcesRegister 42 | { 43 | /** 44 | * Every resource has got to have a unique uri. 45 | * To be sure we filter out any previous 46 | * joryResources with the same uri. 47 | */ 48 | $this->joryResources = $this->joryResources->filter(function ($existing) use ($joryResource) { 49 | return $existing->getUri() !== $joryResource->getUri(); 50 | }); 51 | 52 | /** 53 | * If we have multiple resources for the same related model, we want 54 | * the last one to be applied. This way any standard registered 55 | * resources can be overridden later. So prepend to the 56 | * front of the resource collection. 57 | */ 58 | $this->joryResources->prepend($joryResource); 59 | 60 | return $this; 61 | } 62 | 63 | /** 64 | * Add a registrar for delivering registrations 65 | * 66 | * @param RegistersJoryResources $registrar 67 | */ 68 | public function addRegistrar(RegistersJoryResources $registrar): void 69 | { 70 | $this->registrars[] = $registrar; 71 | } 72 | 73 | /** 74 | * Get a JoryResource by a model's class name. 75 | * 76 | * When multiple resources exist for the same model, 77 | * the last one registered will be used. 78 | * 79 | * @param string $modelClass 80 | * @return JoryResource 81 | */ 82 | public function getByModelClass(string $modelClass): JoryResource 83 | { 84 | foreach ($this->getAllJoryResources() as $joryResource) { 85 | if ($joryResource->getModelClass() === $modelClass) { 86 | return $joryResource; 87 | } 88 | } 89 | 90 | throw new RegistrationNotFoundException('No joryResource found for model ' . $modelClass . '. Does ' . $modelClass . ' have an associated JoryResource?'); 91 | } 92 | 93 | /** 94 | * @param string $uri 95 | * @return JoryResource 96 | */ 97 | public function getByUri(string $uri): JoryResource 98 | { 99 | foreach ($this->getAllJoryResources() as $joryResource) { 100 | if ($joryResource->getUri() === $uri && $joryResource->hasRoutes()) { 101 | return $joryResource; 102 | } 103 | } 104 | 105 | throw new ResourceNotFoundException($uri); 106 | } 107 | 108 | /** 109 | * Get an sorted array of all registered uri's. 110 | * 111 | * @return array 112 | */ 113 | public function getUrisArray(): array 114 | { 115 | $result = []; 116 | 117 | foreach ($this->getAllJoryResources()->filter(function(JoryResource $joryResource){ 118 | return $joryResource->hasRoutes(); 119 | })->sortBy(function(JoryResource $joryResource){ 120 | return $joryResource->getUri(); 121 | }) as $joryResource) { 122 | $result[] = $joryResource->getUri(); 123 | } 124 | 125 | return $result; 126 | } 127 | 128 | /** 129 | * Get all registrations registered by all the registrars. 130 | * 131 | * @return Collection 132 | */ 133 | public function getAllJoryResources(): Collection 134 | { 135 | // Manual registrations get precedence. 136 | $joryResources = $this->joryResources; 137 | 138 | foreach ($this->registrars as $registrar){ 139 | $this->mergeJoryResources($joryResources, $registrar->getJoryResources()); 140 | } 141 | 142 | return $joryResources; 143 | } 144 | 145 | /** 146 | * Merge the additional registration collection into the 147 | * subject collection and filter duplicates by uri. 148 | * 149 | * @param Collection $subject 150 | * @param Collection $additional 151 | * @return void 152 | */ 153 | protected function mergeJoryResources(Collection $subject, Collection $additional): void 154 | { 155 | foreach ($additional as $joryResource){ 156 | /** 157 | * If the existing collection already has a joryResource for 158 | * this uri, don't register it again. 159 | */ 160 | if($subject->contains(function ($existing) use ($joryResource) { 161 | return $existing->getUri() === $joryResource->getUri(); 162 | })){ 163 | continue; 164 | } 165 | 166 | $subject->push($joryResource); 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/Register/RegistersJoryResources.php: -------------------------------------------------------------------------------- 1 | request = $request; 54 | $this->register = $register; 55 | 56 | $this->initMetadata($request); 57 | } 58 | 59 | /** 60 | * Apply an array or Json string. 61 | * 62 | * @param $jory 63 | * @return $this 64 | */ 65 | public function apply($jory): JoryMultipleResponse 66 | { 67 | if (is_array($jory)) { 68 | return $this->applyArray($jory); 69 | } 70 | 71 | if (!is_string($jory)) { 72 | throw new LaravelJoryException('Unexpected type given. Please provide an array or Json string.'); 73 | } 74 | 75 | if (Base64Validator::check($jory)) { 76 | $jory = base64_decode($jory); 77 | } 78 | 79 | return $this->applyJson($jory); 80 | } 81 | 82 | /** 83 | * Apply a json string. 84 | * 85 | * @param string $jory 86 | * @return $this 87 | */ 88 | public function applyJson(string $jory): JoryMultipleResponse 89 | { 90 | $array = json_decode($jory, true); 91 | 92 | if (json_last_error() !== JSON_ERROR_NONE) { 93 | /** 94 | * No use for further processing when json is not valid, abort. 95 | */ 96 | throw new LaravelJoryCallException(['Jory string is no valid json.']); 97 | } 98 | 99 | $this->data = $array; 100 | 101 | return $this; 102 | } 103 | 104 | /** 105 | * Apply an array. 106 | * 107 | * @param array $jory 108 | * @return $this 109 | */ 110 | public function applyArray(array $jory): JoryMultipleResponse 111 | { 112 | $this->data = $jory; 113 | 114 | return $this; 115 | } 116 | 117 | /** 118 | * Create an HTTP response that represents the object. 119 | * 120 | * @param Request $request 121 | * @return Response 122 | */ 123 | public function toResponse($request) 124 | { 125 | $data = $this->toArray(); 126 | 127 | $responseKey = config('jory.response.data-key'); 128 | $response = $responseKey === null ? $data : [$responseKey => $data]; 129 | 130 | $meta = $this->getMetadata(); 131 | if($responseKey !== null && $meta !== null){ 132 | $response[$this->getMetaResponseKey()] = $meta; 133 | } 134 | 135 | return response($response); 136 | } 137 | 138 | /** 139 | * Collect all the data for the requested resources. 140 | * 141 | * @return array 142 | */ 143 | public function toArray(): array 144 | { 145 | // Convert the raw data into jory queries. 146 | if (empty($this->jories)) { 147 | $this->dataIntoJories(); 148 | } 149 | 150 | //Process all Jory queries. 151 | $results = []; 152 | $errors = []; 153 | foreach ($this->jories as $single) { 154 | try { 155 | $results[$single->alias] = $this->processSingle($single); 156 | } catch (LaravelJoryCallException $e) { 157 | foreach ($e->getErrors() as $error) { 158 | /** 159 | * When multiple requests result in errors, we'd like 160 | * to show all the errors that occurred to the user. 161 | * So collect them here and throw them all 162 | * at once later on. 163 | */ 164 | $errors[] = $single->name . ': ' . $error; 165 | } 166 | } 167 | } 168 | 169 | if (count($errors) > 0) { 170 | throw new LaravelJoryCallException($errors); 171 | } 172 | 173 | return $results; 174 | } 175 | 176 | /** 177 | * Process the raw request data into the jories array. 178 | */ 179 | protected function dataIntoJories(): void 180 | { 181 | if (!$this->data) { 182 | // If no explicit data is set, we default to the data in the request. 183 | $this->apply($this->request->input(config('jory.request.key'), '{}')); 184 | } 185 | 186 | /** 187 | * Each item in the array should hold a key as the resource name 188 | * and value with a jory query array or json string. 189 | * Add the individual requested resources to the jories array. 190 | */ 191 | $errors = []; 192 | foreach ($this->data as $name => $data) { 193 | try { 194 | $this->addJory($name, $data); 195 | } catch (ResourceNotFoundException $e) { 196 | /** 197 | * When multiple resources are not found, we'd like 198 | * to show all the not found errors to the user. 199 | * So collect them here and throw them all 200 | * at once later on. 201 | */ 202 | $errors[] = $e->getMessage(); 203 | continue; 204 | } 205 | } 206 | 207 | if (!empty($errors)) { 208 | throw new LaravelJoryCallException($errors); 209 | } 210 | } 211 | 212 | /** 213 | * Add a jory request to the array. 214 | * 215 | * @param string $name 216 | * @param array $data 217 | */ 218 | protected function addJory(string $name, array $data): void 219 | { 220 | $exploded = ResourceNameHelper::explode($name); 221 | 222 | $single = new stdClass(); 223 | $single->name = $name; 224 | $single->data = $data; 225 | $single->resource = $this->register->getByUri($exploded->baseName); 226 | $single->alias = $exploded->alias; 227 | $single->type = $exploded->type; 228 | $single->id = $exploded->id; 229 | 230 | $this->jories[] = $single; 231 | } 232 | 233 | /** 234 | * Process a single resource call. We'll just use a normal single 235 | * JoryResponse and use the toArray() method to collect the data. 236 | * 237 | * @param $single 238 | * @return mixed 239 | */ 240 | protected function processSingle($single) 241 | { 242 | $singleResponse = Jory::byUri($single->resource->getUri()); 243 | 244 | $singleResponse->apply($single->data); 245 | 246 | /** 247 | * Call appropriate methods for specific types. 248 | */ 249 | if ($single->type === 'count') { 250 | $singleResponse->count(); 251 | } 252 | if ($single->type === 'exists') { 253 | $singleResponse->exists(); 254 | } 255 | if ($single->type === 'find' || $single->type === 'first') { 256 | // Return a single item 257 | $singleResponse->find($single->id); 258 | } 259 | 260 | return $singleResponse->toArray(); 261 | } 262 | } -------------------------------------------------------------------------------- /src/Scopes/CallbackFilterScope.php: -------------------------------------------------------------------------------- 1 | callback = $callback; 12 | } 13 | 14 | public function apply($builder, string $operator = null, $data = null): void 15 | { 16 | call_user_func($this->callback, $builder, $operator, $data); 17 | } 18 | } -------------------------------------------------------------------------------- /src/Scopes/CallbackSortScope.php: -------------------------------------------------------------------------------- 1 | callback = $callback; 12 | } 13 | 14 | public function apply($builder, string $order = 'asc'): void 15 | { 16 | call_user_func($this->callback, $builder, $order); 17 | } 18 | } -------------------------------------------------------------------------------- /src/Scopes/FilterScope.php: -------------------------------------------------------------------------------- 1 | applyFieldsToJory($jory, $config->getFields()); 23 | $this->applySortsToJory($jory, $config->getSorts()); 24 | $this->applyOffsetAndLimitToJory($jory, $jory->getLimit(), $config->getLimitDefault()); 25 | 26 | return $jory; 27 | } 28 | 29 | /** 30 | * Apply the field settings in this Config on the Jory query. 31 | * 32 | * When no fields are specified in the request, the default fields in will be set on the Jory query. 33 | * 34 | * @param Jory $jory 35 | * @param array $fields 36 | */ 37 | protected function applyFieldsToJory(Jory $jory, array $fields): void 38 | { 39 | if ($jory->getFields() === null) { 40 | // No fields set in the request, than we will update the fields 41 | // with the ones to be shown by default. 42 | $defaultFields = []; 43 | foreach ($fields as $field) { 44 | if ($field->isShownByDefault()) { 45 | $defaultFields[] = $field->getField(); 46 | } 47 | } 48 | $jory->setFields($defaultFields); 49 | } 50 | } 51 | 52 | /** 53 | * Apply the sort settings in this Config on the Jory query. 54 | * 55 | * @param Jory $jory 56 | * @param array $sorts 57 | */ 58 | protected function applySortsToJory(Jory $jory, array $sorts): void 59 | { 60 | /** 61 | * When default sorts are defined, add them to the Jory query. 62 | * When no sorts are requested, the default sorts in this Config will be applied. 63 | * When sorts are requested, the default sorts are applied after the requested ones. 64 | */ 65 | $defaultSorts = []; 66 | foreach ($sorts as $sort) { 67 | if ($sort->getDefaultIndex() !== null) { 68 | $defaultSorts[$sort->getDefaultIndex()] = new \JosKolenberg\Jory\Support\Sort($sort->getName(), 69 | $sort->getDefaultOrder()); 70 | } 71 | } 72 | ksort($defaultSorts); 73 | foreach ($defaultSorts as $sort) { 74 | $jory->addSort($sort); 75 | } 76 | } 77 | 78 | /** 79 | * Apply the offset and limit settings in this Config on the Jory query. 80 | * 81 | * When no offset or limit is set, the defaults will be used. 82 | * 83 | * @param Jory $jory 84 | * @param int|null $limit 85 | * @param int|null $limitDefault 86 | */ 87 | protected function applyOffsetAndLimitToJory(Jory $jory, int $limit = null, int $limitDefault = null): void 88 | { 89 | if (is_null($limit) && $limitDefault !== null) { 90 | $jory->setLimit($limitDefault); 91 | } 92 | } 93 | } -------------------------------------------------------------------------------- /src/Traits/ConvertsConfigToArray.php: -------------------------------------------------------------------------------- 1 | $this->fieldsToArray($config->getFields()), 23 | 'filters' => $this->filtersToArray($config->getFilters()), 24 | 'sorts' => $this->sortsToArray($config->getSorts()), 25 | 'limit' => [ 26 | 'default' => $config->getLimitDefault(), 27 | 'max' => $config->getLimitMax(), 28 | ], 29 | 'relations' => $this->relationsToArray($config->getRelations()), 30 | ]; 31 | } 32 | 33 | /** 34 | * Turn the fields part of the config into an array. 35 | * 36 | * @param array $fields 37 | * @return array 38 | */ 39 | protected function fieldsToArray(array $fields): array 40 | { 41 | $result = []; 42 | foreach ($fields as $field) { 43 | $result[] = [ 44 | 'field' => $field->getOriginalField(), 45 | 'default' => $field->isShownByDefault(), 46 | ]; 47 | } 48 | 49 | return $result; 50 | } 51 | 52 | /** 53 | * Turn the filters part of the config into an array. 54 | * 55 | * @param array $filters 56 | * @return array 57 | */ 58 | protected function filtersToArray(array $filters): array 59 | { 60 | $result = []; 61 | foreach ($filters as $filter) { 62 | $result[] = [ 63 | 'name' => $filter->getField(), 64 | 'operators' => $filter->getOperators(), 65 | ]; 66 | } 67 | 68 | return $result; 69 | } 70 | 71 | /** 72 | * Turn the sorts part of the config into an array. 73 | * 74 | * @param array $sorts 75 | * @return array 76 | */ 77 | protected function sortsToArray(array $sorts): array 78 | { 79 | $result = []; 80 | foreach ($sorts as $sort) { 81 | $result[] = [ 82 | 'name' => $sort->getField(), 83 | 'default' => ($sort->getDefaultIndex() === null ? false : [ 84 | 'index' => $sort->getDefaultIndex(), 85 | 'order' => $sort->getDefaultOrder(), 86 | ]), 87 | ]; 88 | } 89 | 90 | return $result; 91 | } 92 | 93 | /** 94 | * Turn the relations part of the config into an array. 95 | * 96 | * @param array $relations 97 | * @return array|string 98 | */ 99 | protected function relationsToArray(array $relations): array 100 | { 101 | $result = []; 102 | foreach ($relations as $relation) { 103 | try{ 104 | $type = $relation->getType(); 105 | }catch (RegistrationNotFoundException $e){ 106 | $type = null; 107 | } 108 | $result[] = [ 109 | 'relation' => $relation->getOriginalName(), 110 | 'type' => $type, 111 | ]; 112 | } 113 | 114 | return $result; 115 | } 116 | } -------------------------------------------------------------------------------- /src/Traits/ConvertsModelToArray.php: -------------------------------------------------------------------------------- 1 | createModelArray($model, $joryResource); 29 | 30 | // Add the relations to the result 31 | foreach ($joryResource->getRelatedJoryResources($joryResource) as $relationName => $relatedJoryResource) { 32 | $relationDetails = ResourceNameHelper::explode($relationName); 33 | $relationAlias = $relationDetails->alias; 34 | 35 | // Get the related records which were fetched earlier. These are stored in the model under the full relation's name including alias 36 | $related = $model->joryRelations[$relationName]; 37 | 38 | if (in_array($relationDetails->type, ['count', 'exists'])) { 39 | // A count query; just put the result here 40 | $result[$relationAlias] = $related; 41 | continue; 42 | } 43 | 44 | $result[$relationAlias] = $joryResource->turnRelationResultIntoArray($related, $relatedJoryResource); 45 | } 46 | 47 | return $result; 48 | } 49 | 50 | /** 51 | * Turn the result of a loaded relation into a result array. 52 | * 53 | * @param mixed $relatedData 54 | * @param JoryResource $relatedJoryResource 55 | * @return array|null 56 | */ 57 | protected function turnRelationResultIntoArray($relatedData, JoryResource $relatedJoryResource):? array 58 | { 59 | if ($relatedData === null) { 60 | return null; 61 | } 62 | 63 | if ($relatedData instanceof Model) { 64 | // A related model is found 65 | return $relatedJoryResource->modelToArray($relatedData); 66 | } 67 | 68 | // Must be a related collection 69 | $relationResult = []; 70 | foreach ($relatedData as $relatedModel) { 71 | $relationResult[] = $relatedJoryResource->modelToArray($relatedModel); 72 | } 73 | 74 | return $relationResult; 75 | } 76 | 77 | /** 78 | * Get an associative array of all relations requested in the Jory query. 79 | * 80 | * The key of the array holds the name of the relation (including any 81 | * aliases) The values of the array are JoryResource objects which 82 | * in turn hold the Jory query object for the relation. 83 | * 84 | * We build this array here once so we don't have to grab for a new 85 | * JoryResource for each record we want to convert to an array. 86 | * 87 | * @param JoryResource $joryResource 88 | * @return array 89 | */ 90 | protected function getRelatedJoryResources(JoryResource $joryResource): array 91 | { 92 | if (! $joryResource->relatedJoryResources) { 93 | $joryResource->relatedJoryResources = []; 94 | 95 | foreach ($joryResource->jory->getRelations() as $relation) { 96 | $relatedJoryResource = $joryResource->getConfig()->getRelation($relation)->getJoryResource()->fresh(); 97 | 98 | $relatedJoryResource->setJory($relation->getJory()); 99 | 100 | $joryResource->relatedJoryResources[$relation->getName()] = $relatedJoryResource; 101 | } 102 | } 103 | 104 | return $joryResource->relatedJoryResources; 105 | } 106 | 107 | /** 108 | * Export this model's attributes to an array. A custom attribute class 109 | * has precedence so we'll check on that first. Otherwise we will use 110 | * Eloquent's attributesToArray so we get the casting which is set 111 | * in the model's casts array. When the value is not present 112 | * (because it's not visible for the serialisation) 113 | * we will call for the property directly. 114 | * 115 | * @param Model $model 116 | * @param JoryResource $joryResource 117 | * @return array 118 | */ 119 | protected function createModelArray(Model $model, JoryResource $joryResource): array 120 | { 121 | $jory = $joryResource->getJory(); 122 | 123 | $result = []; 124 | 125 | $raw = $model->attributesToArray(); 126 | 127 | foreach ($jory->getFields() as $field) { 128 | $configuredField = $joryResource->getConfig()->getField($field); 129 | 130 | /** 131 | * Check if there's a custom attribute class configured. 132 | */ 133 | if($configuredField->getGetter() !== null){ 134 | $result[$field] = $configuredField->getGetter()->get($model); 135 | 136 | continue; 137 | } 138 | 139 | /** 140 | * No custom attribute class is present, get the attribute 141 | * from the casted array or directly from the model. 142 | */ 143 | $result[$field] = array_key_exists($configuredField->getOriginalField(), $raw) 144 | ? $raw[$configuredField->getOriginalField()] 145 | : $model->{$configuredField->getOriginalField()}; 146 | } 147 | 148 | return $result; 149 | } 150 | } -------------------------------------------------------------------------------- /src/Traits/HandlesJoryFilters.php: -------------------------------------------------------------------------------- 1 | doApplyFilter($builder, $joryResource->getJory()->getFilter(), $joryResource); 27 | } 28 | 29 | /** 30 | * Apply a filter (field, groupAnd or groupOr) on a query. 31 | * 32 | * Although it seems like we can retrieve the filter from the JoryResource 33 | * using joryResource->getJory()->getFilter(), this won't work. We 34 | * will be using the same joryResource for the subfilters as well 35 | * so we do have to supply them as two different parameters. 36 | * 37 | * @param mixed $builder 38 | * @param FilterInterface $filter 39 | * @param JoryResource $joryResource 40 | */ 41 | public function doApplyFilter($builder, FilterInterface $filter, JoryResource $joryResource): void 42 | { 43 | if ($filter instanceof Filter) { 44 | $this->applyFieldFilter($builder, $filter, $joryResource); 45 | } 46 | if ($filter instanceof GroupAndFilter) { 47 | $builder->where(function ($builder) use ($joryResource, $filter) { 48 | foreach ($filter as $subFilter) { 49 | $this->doApplyFilter($builder, $subFilter, $joryResource); 50 | } 51 | }); 52 | } 53 | if ($filter instanceof GroupOrFilter) { 54 | $builder->where(function ($builder) use ($joryResource, $filter) { 55 | foreach ($filter as $subFilter) { 56 | $builder->orWhere(function ($builder) use ($joryResource, $subFilter) { 57 | $this->doApplyFilter($builder, $subFilter, $joryResource); 58 | }); 59 | } 60 | }); 61 | } 62 | } 63 | 64 | /** 65 | * Apply a filter to a field. 66 | * Use custom filter method if available. 67 | * If not, run the default filter method.. 68 | * 69 | * @param mixed $builder 70 | * @param Filter $filter 71 | * @param JoryResource $joryResource 72 | */ 73 | public function applyFieldFilter($builder, Filter $filter, JoryResource $joryResource): void 74 | { 75 | $configuredFilter = $joryResource->getConfig()->getFilter($filter); 76 | 77 | /** 78 | * First check if there is a custom scope attached 79 | * to the filter. If so, apply that one. 80 | */ 81 | $scope = $configuredFilter->getScope(); 82 | if($scope){ 83 | // Wrap in a where closure to encapsulate any OR clauses in custom method 84 | // which could lead to unexpected results. 85 | $builder->where(function ($builder) use ($joryResource, $filter, $scope) { 86 | $scope->apply($builder, $filter->getOperator(), $filter->getData()); 87 | }); 88 | return; 89 | } 90 | 91 | /** 92 | * When the field contains dots, we want to query on a relation 93 | * with the last part of the string being the field to filter on. 94 | */ 95 | if(Str::contains($filter->getField(), '.')){ 96 | $this->applyRelationFilter($builder, $filter, $configuredFilter); 97 | 98 | return; 99 | } 100 | 101 | /** 102 | * Always apply the filter on the table of the model which 103 | * is being queried even if a join is applied (e.g. when filtering 104 | * a belongsToMany relation), so we prefix the field with the table name. 105 | */ 106 | $field = $builder->getModel()->getTable().'.'.$configuredFilter->getField(); 107 | FilterHelper::applyWhere($builder, $field, $filter->getOperator(), $filter->getData()); 108 | } 109 | 110 | /** 111 | * Apply a filter on a field in a relation 112 | * using relation1.relation2.etc.field notation. 113 | * 114 | * @param mixed $builder 115 | * @param Filter $filter 116 | * @return void 117 | */ 118 | public function applyRelationFilter($builder, Filter $filter, \JosKolenberg\LaravelJory\Config\Filter $configuredFilter): void 119 | { 120 | $relations = explode('.', $configuredFilter->getField()); 121 | 122 | $field = array_pop($relations); 123 | 124 | $relation = implode('.', $relations); 125 | 126 | $builder->whereHas($relation, function ($builder) use ($filter, $field) { 127 | FilterHelper::applyWhere($builder, $field, $filter->getOperator(), $filter->getData()); 128 | }); 129 | } 130 | 131 | } -------------------------------------------------------------------------------- /src/Traits/HandlesJorySorts.php: -------------------------------------------------------------------------------- 1 | getJory()->getSorts() as $sort) { 19 | $this->applySort($builder, $sort, $joryResource); 20 | } 21 | } 22 | 23 | /** 24 | * Apply a single sort on a query. 25 | * 26 | * @param $builder 27 | * @param Sort $sort 28 | * @param JoryResource $joryResource 29 | */ 30 | public function applySort($builder, Sort $sort, JoryResource $joryResource): void 31 | { 32 | $configuredSort = $joryResource->getConfig()->getSort($sort); 33 | 34 | /** 35 | * First check if there is a custom scope attached 36 | * to the sort. If so, apply that one. 37 | */ 38 | $scope = $configuredSort->getScope(); 39 | if($scope){ 40 | $scope->apply($builder, $sort->getOrder()); 41 | return; 42 | } 43 | 44 | // Always apply the sort on the table of the model which 45 | // is being queried even if a join is applied (e.g. when filtering 46 | // a belongsToMany relation), so we prefix the field with the table name. 47 | $field = $builder->getModel()->getTable().'.'.$configuredSort->getField(); 48 | $this->applyDefaultSort($builder, $field, $sort->getOrder()); 49 | } 50 | 51 | /** 52 | * Apply a sort to a field with default options. 53 | * 54 | * @param $builder 55 | * @param string $field 56 | * @param string $order 57 | */ 58 | public function applyDefaultSort($builder, string $field, string $order): void 59 | { 60 | $builder->orderBy($field, $order); 61 | } 62 | } -------------------------------------------------------------------------------- /src/Traits/ProcessesMetadata.php: -------------------------------------------------------------------------------- 1 | $metaClass) { 33 | $this->availableMeta[$caseManager->toCurrent($name)] = $metaClass; 34 | } 35 | 36 | $requestedMetaData = $request->input(config('jory.request.meta-key'), []); 37 | 38 | $this->validateRequestedMeta($requestedMetaData); 39 | 40 | foreach ($requestedMetaData as $metaName){ 41 | $this->meta[$metaName] = new $this->availableMeta[$metaName]($request); 42 | } 43 | } 44 | 45 | /** 46 | * @param array $metaTags 47 | * @throws LaravelJoryCallException 48 | */ 49 | protected function validateRequestedMeta(array $metaTags): void 50 | { 51 | if(!$metaTags){ 52 | return; 53 | } 54 | 55 | if(config('jory.response.data-key') === null){ 56 | throw new LaravelJoryCallException(['Meta tags are not supported when data is returned in the root.']); 57 | } 58 | 59 | $unknownMetas = []; 60 | foreach ($metaTags as $metaTag){ 61 | if(!array_key_exists($metaTag, $this->availableMeta)){ 62 | $unknownMetas[] = 'Meta tag ' . $metaTag . ' is not supported.'; 63 | } 64 | } 65 | 66 | if($unknownMetas){ 67 | throw new LaravelJoryCallException($unknownMetas); 68 | } 69 | } 70 | 71 | /** 72 | * Get the requested metadata. 73 | */ 74 | protected function getMetadata(): ?array 75 | { 76 | if(count($this->meta) === 0){ 77 | return null; 78 | } 79 | 80 | $result = []; 81 | 82 | foreach ($this->meta as $metaName => $meta) { 83 | $result[$metaName] = $meta->get(); 84 | } 85 | 86 | return $result; 87 | } 88 | 89 | /** 90 | * Get the key on which metadata should be returned. 91 | * 92 | * @return string 93 | */ 94 | protected function getMetaResponseKey(): string 95 | { 96 | return config('jory.response.meta-key'); 97 | } 98 | } -------------------------------------------------------------------------------- /tests/Attributes/SongDescription.php: -------------------------------------------------------------------------------- 1 | title . ' from the ' . $song->album->name . ' album.'; 22 | } 23 | } -------------------------------------------------------------------------------- /tests/AuthorizeTest.php: -------------------------------------------------------------------------------- 1 | json('GET', 'jory/band', [ 14 | 'jory' => '{"srt":["name"]}', 15 | ]); 16 | 17 | $expected = [ 18 | 'data' => [ 19 | [ 20 | 'id' => 3, 21 | 'name' => 'Beatles', 22 | 'year_start' => 1960, 23 | 'year_end' => 1970, 24 | ], 25 | [ 26 | 'id' => 4, 27 | 'name' => 'Jimi Hendrix Experience', 28 | 'year_start' => 1966, 29 | 'year_end' => 1970, 30 | ], 31 | [ 32 | 'id' => 2, 33 | 'name' => 'Led Zeppelin', 34 | 'year_start' => 1968, 35 | 'year_end' => 1980, 36 | ], 37 | [ 38 | 'id' => 1, 39 | 'name' => 'Rolling Stones', 40 | 'year_start' => 1962, 41 | 'year_end' => null, 42 | ], 43 | ], 44 | ]; 45 | $response->assertStatus(200)->assertExactJson($expected)->assertJson($expected); 46 | 47 | $this->assertQueryCount(1); 48 | } 49 | 50 | #[Test] 51 | public function it_can_modify_the_query_by_authorize_method_2() 52 | { 53 | $this->actingAs(User::where('name', 'mick')->first()); 54 | 55 | $response = $this->json('GET', 'jory/band', [ 56 | 'jory' => '{"srt":["name"]}', 57 | ]); 58 | 59 | $expected = [ 60 | 'data' => [ 61 | [ 62 | 'id' => 3, 63 | 'name' => 'Beatles', 64 | 'year_start' => 1960, 65 | 'year_end' => 1970, 66 | ], 67 | [ 68 | 'id' => 4, 69 | 'name' => 'Jimi Hendrix Experience', 70 | 'year_start' => 1966, 71 | 'year_end' => 1970, 72 | ], 73 | ], 74 | ]; 75 | $response->assertStatus(200)->assertExactJson($expected)->assertJson($expected); 76 | 77 | $this->assertQueryCount(2); 78 | } 79 | 80 | #[Test] 81 | public function it_can_modify_the_query_by_authorize_method_3() 82 | { 83 | $this->actingAs(User::where('name', 'ronnie')->first()); 84 | 85 | $response = $this->json('GET', 'jory/band', [ 86 | 'jory' => '{"srt":["name"]}', 87 | ]); 88 | 89 | $expected = [ 90 | 'data' => [ 91 | [ 92 | 'id' => 3, 93 | 'name' => 'Beatles', 94 | 'year_start' => 1960, 95 | 'year_end' => 1970, 96 | ], 97 | [ 98 | 'id' => 4, 99 | 'name' => 'Jimi Hendrix Experience', 100 | 'year_start' => 1966, 101 | 'year_end' => 1970, 102 | ], 103 | [ 104 | 'id' => 2, 105 | 'name' => 'Led Zeppelin', 106 | 'year_start' => 1968, 107 | 'year_end' => 1980, 108 | ], 109 | [ 110 | 'id' => 1, 111 | 'name' => 'Rolling Stones', 112 | 'year_start' => 1962, 113 | 'year_end' => null, 114 | ], 115 | ], 116 | ]; 117 | $response->assertStatus(200)->assertExactJson($expected)->assertJson($expected); 118 | 119 | $this->assertQueryCount(2); 120 | } 121 | 122 | #[Test] 123 | public function it_can_modify_the_query_by_authorize_method_in_relations() 124 | { 125 | $this->actingAs(User::where('name', 'mick')->first()); 126 | 127 | $response = $this->json('GET', 'jory/album', [ 128 | 'jory' => '{"flt":{"f":"id","o":"in","d":[2,9]},"fld":["name"],"rlt":{"band":{"fld":["name"]}}}', 129 | ]); 130 | 131 | $expected = [ 132 | 'data' => [ 133 | [ 134 | 'name' => 'Sticky Fingers', 135 | 'band' => null, 136 | ], 137 | [ 138 | 'name' => 'Let it be', 139 | 'band' => [ 140 | 'name' => 'Beatles', 141 | ], 142 | ], 143 | ], 144 | ]; 145 | $response->assertStatus(200)->assertExactJson($expected)->assertJson($expected); 146 | 147 | $this->assertQueryCount(3); 148 | } 149 | 150 | #[Test] 151 | public function the_authorize_method_is_scoped() 152 | { 153 | $this->actingAs(User::where('name', 'keith')->first()); 154 | 155 | $response = $this->json('GET', 'jory/band', [ 156 | 'jory' => [ 157 | 'flt' => [ 158 | 'f' => 'name', 159 | 'o' => 'like', 160 | 'd' => '%t%' 161 | ], 162 | 'fld' => 'name', 163 | 'srt' => 'name', 164 | ], 165 | ]); 166 | 167 | $expected = [ 168 | 'data' => [ 169 | [ 170 | 'name' => 'Rolling Stones', 171 | ], 172 | ], 173 | ]; 174 | $response->assertStatus(200)->assertExactJson($expected)->assertJson($expected); 175 | 176 | $this->assertQueryCount(2); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /tests/Base64Test.php: -------------------------------------------------------------------------------- 1 | json('GET', 'jory/band', [ 14 | 'jory' => base64_encode('{"filter":{"f":"name","o":"like","d":"%zep%"},"rlt":{"albums":{"flt":{"f":"name","o":"like","d":"%III%"}}},"fld":["id","name"]}'), 15 | ]); 16 | 17 | $response->assertStatus(200)->assertExactJson([ 18 | 'data' => [ 19 | [ 20 | 'id' => 2, 21 | 'name' => 'Led Zeppelin', 22 | 'albums' => [ 23 | [ 24 | 'id' => 6, 25 | 'band_id' => 2, 26 | 'name' => 'Led Zeppelin III', 27 | 'release_date' => '1970-10-05 00:00:00', 28 | ], 29 | ], 30 | ], 31 | ], 32 | ]); 33 | 34 | $this->assertQueryCount(2); 35 | } 36 | 37 | #[Test] 38 | public function it_can_process_a_base64_encoded_json_string_2() 39 | { 40 | $response = $this->json('GET', 'jory/band', [ 41 | 'jory' => 'eyJmaWx0ZXIiOnsiZiI6Im5hbWUiLCJvIjoibGlrZSIsImQiOiIlaW4lIn0sInJsdCI6eyJzb25ncyI6eyJmbGQiOlsiaWQiLCJ0aXRsZSJdLCJmbHQiOnsiZiI6InRpdGxlIiwibyI6Imxpa2UiLCJkIjoiJWxvdmUlIn0sInJsdCI6eyJhbGJ1bSI6e319fX19', 42 | ]); 43 | 44 | $response->assertStatus(200)->assertExactJson([ 45 | 'data' => [ 46 | [ 47 | 'id' => 1, 48 | 'name' => 'Rolling Stones', 49 | 'year_start' => 1962, 50 | 'year_end' => null, 51 | 'songs' => [ 52 | [ 53 | 'id' => 2, 54 | 'title' => 'Love In Vain (Robert Johnson)', 55 | 'album' => [ 56 | 'id' => 1, 57 | 'band_id' => 1, 58 | 'name' => 'Let it bleed', 59 | 'release_date' => '1969-12-05 00:00:00', 60 | ], 61 | ], 62 | ], 63 | ], 64 | [ 65 | 'id' => 2, 66 | 'name' => 'Led Zeppelin', 67 | 'year_start' => 1968, 68 | 'year_end' => 1980, 69 | 'songs' => [ 70 | [ 71 | 'id' => 47, 72 | 'title' => 'Whole Lotta Love', 73 | 'album' => [ 74 | 'id' => 5, 75 | 'band_id' => 2, 76 | 'name' => 'Led Zeppelin II', 77 | 'release_date' => '1969-10-22 00:00:00', 78 | ], 79 | ], 80 | ], 81 | ], 82 | ], 83 | ]); 84 | 85 | $this->assertQueryCount(3); 86 | } 87 | 88 | #[Test] 89 | public function it_can_process_a_base64_encoded_json_string_for_multiple_resources() 90 | { 91 | $response = $this->json('GET', 'jory', [ 92 | 'jory' => base64_encode('{"band:first as lz":{"filter":{"f":"name","o":"like","d":"%zep%"},"rlt":{"albums":{"flt":{"f":"name","o":"like","d":"%III%"}}},"fld":["id","name"]},"song as songs":{"filter":{"f":"title","o":"like","d":"%let%"},"fld":["title"],"srt":"title"}}'), 93 | ]); 94 | 95 | $response->assertStatus(200)->assertExactJson([ 96 | 'data' => [ 97 | 'lz' => [ 98 | 'id' => 2, 99 | 'name' => 'Led Zeppelin', 100 | 'albums' => [ 101 | [ 102 | 'id' => 6, 103 | 'band_id' => 2, 104 | 'name' => 'Led Zeppelin III', 105 | 'release_date' => '1970-10-05 00:00:00', 106 | ], 107 | ], 108 | ], 109 | 'songs' => [ 110 | [ 111 | 'title' => 'Let It Be', 112 | ], 113 | [ 114 | 'title' => 'Let It Bleed', 115 | ], 116 | [ 117 | 'title' => 'Let It Loose', 118 | ], 119 | ], 120 | ], 121 | ]); 122 | 123 | $this->assertQueryCount(3); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /tests/ConfigTest.php: -------------------------------------------------------------------------------- 1 | set('jory.response.data-key', null); 13 | $app['config']->set('jory.response.errors-key', null); 14 | } 15 | 16 | #[Test] 17 | public function it_can_return_data_in_the_root_when_data_key_is_configured_null() 18 | { 19 | $response = $this->json('GET', 'jory/band/3', [ 20 | 'jory' => '{"fld":["name"]}', 21 | ]); 22 | 23 | $expected = [ 24 | 'name' => 'Beatles', 25 | ]; 26 | $response->assertStatus(200)->assertExactJson($expected)->assertJson($expected); 27 | 28 | $this->assertQueryCount(1); 29 | } 30 | 31 | #[Test] 32 | public function it_can_return_data_in_the_root_when_data_key_is_configured_null_2() 33 | { 34 | $response = $this->json('GET', 'jory', [ 35 | 'jory' => '{"band:3 as beatles":{"fld":["name"]}}', 36 | ]); 37 | 38 | $expected = [ 39 | 'beatles' => [ 40 | 'name' => 'Beatles', 41 | ], 42 | ]; 43 | $response->assertStatus(200)->assertExactJson($expected)->assertJson($expected); 44 | 45 | $this->assertQueryCount(1); 46 | } 47 | 48 | #[Test] 49 | public function it_can_return_errors_in_the_root_when_data_key_is_configured_null() 50 | { 51 | $response = $this->json('GET', 'jory/band/3', [ 52 | 'jory' => '{"fld":["naame"]}', 53 | ]); 54 | 55 | $expected = [ 56 | 'Field "naame" is not available, did you mean "name"? (Location: fields.naame)', 57 | ]; 58 | $response->assertStatus(422)->assertExactJson($expected)->assertJson($expected); 59 | 60 | $this->assertQueryCount(0); 61 | } 62 | 63 | #[Test] 64 | public function it_can_return_errors_in_the_root_when_data_key_is_configured_null_2() 65 | { 66 | $response = $this->json('GET', 'jory', [ 67 | 'jory' => '{"band:3 as beatles":{"fld":["naame"]}}', 68 | ]); 69 | 70 | $expected = [ 71 | 'band:3 as beatles: Field "naame" is not available, did you mean "name"? (Location: fields.naame)', 72 | ]; 73 | $response->assertStatus(422)->assertExactJson($expected)->assertJson($expected); 74 | 75 | $this->assertQueryCount(0); 76 | } 77 | 78 | #[Test] 79 | public function a_relation_can_be_defined_with_a_custom_jory_resource_1() 80 | { 81 | $response = $this->json('GET', 'jory/album/5', [ 82 | 'jory' => [ 83 | 'fld' => ['name'], 84 | 'rlt' => [ 85 | 'customSongs2 as songs' => [ 86 | 'flt' => [ 87 | 'f' => 'title', 88 | 'o' => 'like', 89 | 'd' => '%love%', 90 | ], 91 | 'fld' => [ 92 | 'custom_field', 93 | ], 94 | ], 95 | ], 96 | ], 97 | ]); 98 | 99 | $expected = [ 100 | 'name' => 'Led Zeppelin II', 101 | 'songs' => [ 102 | [ 103 | 'custom_field' => 'custom_value', 104 | ], 105 | ], 106 | ]; 107 | $response->assertStatus(200)->assertExactJson($expected)->assertJson($expected); 108 | 109 | $this->assertQueryCount(2); 110 | } 111 | 112 | #[Test] 113 | public function a_relation_can_be_defined_with_a_custom_jory_resource_2() 114 | { 115 | $response = $this->json('GET', 'jory/album/5', [ 116 | 'jory' => [ 117 | 'fld' => ['name'], 118 | 'rlt' => [ 119 | 'customSongs3 as songs' => [ 120 | 'flt' => [ 121 | 'f' => 'title', 122 | 'o' => 'like', 123 | 'd' => '%love%', 124 | ], 125 | 'fld' => [ 126 | 'title', 127 | 'custom_field', 128 | ], 129 | ], 130 | ], 131 | ], 132 | ]); 133 | 134 | $expected = [ 135 | 'Field "custom_field" is not available, no suggestions found. (Location: customSongs3 as songs.fields.custom_field)', 136 | ]; 137 | $response->assertStatus(422)->assertExactJson($expected)->assertJson($expected); 138 | 139 | $this->assertQueryCount(0); 140 | } 141 | 142 | #[Test] 143 | public function a_relation_can_be_defined_with_a_custom_jory_resource_3() 144 | { 145 | $response = $this->json('GET', 'jory/album/5', [ 146 | 'jory' => [ 147 | 'fld' => ['name'], 148 | 'rlt' => [ 149 | 'customSongs3' => [ 150 | 'flt' => [ 151 | 'f' => 'title', 152 | 'o' => 'like', 153 | 'd' => '%love%', 154 | ], 155 | 'fld' => [ 156 | 'title', 157 | 'custom_field', 158 | ], 159 | ], 160 | ], 161 | ], 162 | ]); 163 | 164 | $expected = [ 165 | 'Field "custom_field" is not available, no suggestions found. (Location: customSongs3.fields.custom_field)', 166 | ]; 167 | $response->assertStatus(422)->assertExactJson($expected)->assertJson($expected); 168 | 169 | $this->assertQueryCount(0); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /tests/ConsoleOutput/Generated/.gitignore: -------------------------------------------------------------------------------- 1 | !.gitignore 2 | * -------------------------------------------------------------------------------- /tests/ConsoleOutput/Original/AlbumJoryResource.php: -------------------------------------------------------------------------------- 1 | field('id')->filterable()->sortable(); 21 | $this->field('name')->filterable()->sortable(); 22 | $this->field('band_id')->filterable()->sortable(); 23 | $this->field('release_date')->filterable()->sortable(); 24 | 25 | // Custom attributes 26 | $this->field('cover_image')->hideByDefault(); 27 | $this->field('tag_names_string')->hideByDefault(); 28 | $this->field('titles_string')->hideByDefault(); 29 | 30 | // Relations 31 | $this->relation('albumCover'); 32 | $this->relation('band'); 33 | $this->relation('cover'); 34 | $this->relation('songs'); 35 | $this->relation('tags'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/ConsoleOutput/Original/AlternateBandJoryResource.php: -------------------------------------------------------------------------------- 1 | field('id')->filterable()->sortable(); 21 | $this->field('name')->filterable()->sortable(); 22 | $this->field('year_start')->filterable()->sortable(); 23 | $this->field('year_end')->filterable()->sortable(); 24 | 25 | // Custom attributes 26 | $this->field('all_albums_string')->hideByDefault(); 27 | $this->field('first_title_string')->hideByDefault(); 28 | $this->field('image_urls_string')->hideByDefault(); 29 | $this->field('titles_string')->hideByDefault(); 30 | 31 | // Relations 32 | $this->relation('albums'); 33 | $this->relation('firstSong'); 34 | $this->relation('images'); 35 | $this->relation('people'); 36 | $this->relation('songs'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/ConsoleOutput/Original/BandJoryResource.php: -------------------------------------------------------------------------------- 1 | field('id')->filterable()->sortable(); 21 | $this->field('name')->filterable()->sortable(); 22 | $this->field('year_start')->filterable()->sortable(); 23 | $this->field('year_end')->filterable()->sortable(); 24 | 25 | // Custom attributes 26 | $this->field('all_albums_string')->hideByDefault(); 27 | $this->field('first_title_string')->hideByDefault(); 28 | $this->field('image_urls_string')->hideByDefault(); 29 | $this->field('titles_string')->hideByDefault(); 30 | 31 | // Relations 32 | $this->relation('albums'); 33 | $this->relation('firstSong'); 34 | $this->relation('images'); 35 | $this->relation('people'); 36 | $this->relation('songs'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/ConsoleOutput/Original/EmptyJoryResource.php: -------------------------------------------------------------------------------- 1 | field('id')->filterable()->sortable(); 21 | $this->field('url')->filterable()->sortable(); 22 | $this->field('imageable_id')->filterable()->sortable(); 23 | $this->field('imageable_type')->filterable()->sortable(); 24 | 25 | // Relations 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/ConsoleOutput/Original/PersonJoryResource.php: -------------------------------------------------------------------------------- 1 | field('id')->filterable()->sortable(); 21 | $this->field('first_name')->filterable()->sortable(); 22 | $this->field('last_name')->filterable()->sortable(); 23 | $this->field('date_of_birth')->filterable()->sortable(); 24 | 25 | // Custom attributes 26 | $this->field('first_image_url')->hideByDefault(); 27 | $this->field('full_name')->hideByDefault(); 28 | $this->field('instruments_string')->hideByDefault(); 29 | 30 | // Relations 31 | $this->relation('band'); 32 | $this->relation('firstImage'); 33 | $this->relation('groupies'); 34 | $this->relation('instruments'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/ConsoleOutput/Original/UserJoryResource.php: -------------------------------------------------------------------------------- 1 | field('id')->filterable()->sortable(); 21 | $this->field('name')->filterable()->sortable(); 22 | $this->field('email')->filterable()->sortable(); 23 | $this->field('created_at')->filterable()->sortable(); 24 | $this->field('updated_at')->filterable()->sortable(); 25 | 26 | // Relations 27 | $this->relation('notifications'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/ControllerUsageTest.php: -------------------------------------------------------------------------------- 1 | middleware('jory'); 16 | Route::get('band/first-by-filter', BandController::class.'@firstByFilter')->middleware('jory'); 17 | Route::get('band/count', BandController::class.'@count')->middleware('jory'); 18 | Route::get('band/{bandId}', BandController::class.'@show')->middleware('jory'); 19 | } 20 | 21 | #[Test] 22 | public function it_can_return_a_collection_based_on_request() 23 | { 24 | $response = $this->json('GET', 'band', [ 25 | 'jory' => '{"filter":{"f":"name","o":"like","d":"%zep%"}}', 26 | ]); 27 | 28 | $response->assertStatus(200)->assertExactJson([ 29 | 'data' => [ 30 | [ 31 | 'id' => 2, 32 | 'name' => 'Led Zeppelin', 33 | 'year_start' => 1968, 34 | 'year_end' => 1980, 35 | ], 36 | ], 37 | ]); 38 | 39 | $this->assertQueryCount(1); 40 | } 41 | 42 | #[Test] 43 | public function it_can_return_a_single_record_based_on_request() 44 | { 45 | $response = $this->json('GET', 'band/2', [ 46 | 'jory' => [] 47 | ]); 48 | 49 | $response->assertStatus(200)->assertExactJson([ 50 | 'data' => [ 51 | 'id' => 2, 52 | 'name' => 'Led Zeppelin', 53 | 'year_start' => 1968, 54 | 'year_end' => 1980, 55 | ], 56 | ]); 57 | 58 | $this->assertQueryCount(1); 59 | } 60 | 61 | #[Test] 62 | public function it_can_return_a_single_record_filtered_by_jory() 63 | { 64 | $response = $this->json('GET', 'band/first-by-filter', ['jory' => '{"flt":{"f":"name","d":"Beatles"}}']); 65 | 66 | $response->assertStatus(200)->assertExactJson([ 67 | 'data' => [ 68 | 'id' => 3, 69 | 'name' => 'Beatles', 70 | 'year_start' => 1960, 71 | 'year_end' => 1970, 72 | ], 73 | ]); 74 | 75 | $this->assertQueryCount(1); 76 | } 77 | 78 | #[Test] 79 | public function it_can_return_a_record_count_based_on_jory_filters() 80 | { 81 | $response = $this->json('GET', 'band/count', ['jory' => '{"flt":{"f":"name","o":"like","d":"%r%"}}']); 82 | 83 | $response->assertStatus(200)->assertExactJson([ 84 | 'data' => 2, 85 | ]); 86 | 87 | $this->assertQueryCount(1); 88 | } 89 | 90 | #[Test] 91 | public function it_does_not_execute_when_no_jory_parameter_is_given() 92 | { 93 | $response = $this->json('GET', 'band/2'); 94 | $response->assertStatus(200)->assertExactJson([]); 95 | 96 | $response = $this->json('GET', 'band/first-by-filter'); 97 | $response->assertStatus(200)->assertExactJson([]); 98 | 99 | $response = $this->json('GET', 'band'); 100 | $response->assertStatus(200)->assertExactJson([]); 101 | 102 | $response = $this->json('GET', 'band/count'); 103 | $response->assertStatus(200)->assertExactJson([]); 104 | 105 | $this->assertQueryCount(0); 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /tests/Controllers/BandController.php: -------------------------------------------------------------------------------- 1 | find($bandId); 19 | } 20 | 21 | public function firstByFilter() 22 | { 23 | return Jory::on(Band::query())->first(); 24 | } 25 | 26 | public function count() 27 | { 28 | return Jory::on(Band::query())->count(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/Controllers/SongWithConfigController.php: -------------------------------------------------------------------------------- 1 | getConfig()->toArray()); 39 | } 40 | 41 | public function optionsTwo() 42 | { 43 | Jory::register(SongJoryResourceWithConfigTwo::class); 44 | return response((new SongJoryResourceWithConfigTwo())->getConfig()->toArray()); 45 | } 46 | 47 | public function optionsThree() 48 | { 49 | Jory::register(SongJoryResourceWithConfigThree::class); 50 | return response((new SongJoryResourceWithConfigThree())->getConfig()->toArray()); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/CustomAttributeTest.php: -------------------------------------------------------------------------------- 1 | json('GET', 'jory/song/12', [ 17 | 'jory' => [ 18 | 'fld' => [ 19 | 'title', 20 | 'description' 21 | ] 22 | ], 23 | ]); 24 | 25 | $response->assertStatus(200)->assertExactJson([ 26 | 'data' => [ 27 | 'title' => 'Wild Horses', 28 | 'description' => 'Wild Horses from the Sticky Fingers album.', 29 | ], 30 | ]); 31 | 32 | $this->assertQueryCount(2); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/ExistsTest.php: -------------------------------------------------------------------------------- 1 | json('GET', 'jory/song/exists', [ 14 | 'jory' => [ 15 | 'flt' => [ 16 | 'f' => 'title', 17 | 'o' => 'like', 18 | 'd' => '%love%', 19 | ], 20 | ] 21 | ]); 22 | 23 | $expected = [ 24 | 'data' => true, 25 | ]; 26 | $response->assertStatus(200)->assertExactJson($expected)->assertJson($expected); 27 | 28 | $this->assertQueryCount(1); 29 | } 30 | 31 | #[Test] 32 | public function it_can_tell_if_an_item_exists_using_the_uri_2() 33 | { 34 | $response = $this->json('GET', 'jory/band/exists', [ 35 | 'jory' => '{"filter":{"f":"name","o":"like","d":"%zep%"},"rlt":{"albums":{"flt":{"f":"name","o":"like","d":"%III%"}}},"fld":["id","name"]}', 36 | ]); 37 | 38 | $response->assertStatus(200)->assertExactJson([ 39 | 'data' => true 40 | ]); 41 | 42 | $this->assertQueryCount(1); 43 | } 44 | 45 | #[Test] 46 | public function it_can_tell_if_an_item_exists_using_the_uri_3() 47 | { 48 | $response = $this->json('GET', 'jory/song/exists', [ 49 | 'jory' => [ 50 | 'flt' => [ 51 | 'f' => 'title', 52 | 'o' => 'like', 53 | 'd' => '%lovvve%', 54 | ], 55 | ] 56 | ]); 57 | 58 | $expected = [ 59 | 'data' => false, 60 | ]; 61 | $response->assertStatus(200)->assertExactJson($expected)->assertJson($expected); 62 | 63 | $this->assertQueryCount(1); 64 | } 65 | 66 | #[Test] 67 | public function it_can_tell_if_a_relation_exists_1() 68 | { 69 | $response = $this->json('GET', 'jory/band', [ 70 | 'jory' => '{"filter":{"f":"name","o":"like","d":"%es%"},"rlt":{"songs:exists":{"flt":{"f":"title","o":"like","d":"%gimme%"},"fld":["title"]}},"fld":["name"]}', 71 | ]); 72 | 73 | $response->assertStatus(200)->assertExactJson([ 74 | 'data' => [ 75 | [ 76 | 'name' => 'Rolling Stones', 77 | 'songs:exists' => true, 78 | ], 79 | [ 80 | 'name' => 'Beatles', 81 | 'songs:exists' => false, 82 | ], 83 | ], 84 | ]); 85 | 86 | $this->assertQueryCount(3); 87 | } 88 | 89 | #[Test] 90 | public function it_can_tell_if_a_relation_exists_2() 91 | { 92 | $response = $this->json('GET', 'jory/album/3', [ 93 | 'jory' => '{"rlt":{"songs:exists as song_exists":{"srt":["-id"],"fld":["title"]}},"fld":["name"]}', 94 | ]); 95 | 96 | $response->assertStatus(200)->assertExactJson([ 97 | 'data' => [ 98 | 'name' => 'Exile on main st.', 99 | 'song_exists' => true, 100 | ], 101 | ]); 102 | 103 | $this->assertQueryCount(2); 104 | } 105 | 106 | #[Test] 107 | public function it_doesnt_fail_when_requesting_exists_on_a_non_collection_relation() 108 | { 109 | $response = $this->json('GET', 'jory/song/first', [ 110 | 'jory' => '{"rlt":{"album:exists":{"fld":["name"]}},"fld":["title"]}', 111 | ]); 112 | 113 | $response->assertStatus(200)->assertExactJson([ 114 | 'data' => [ 115 | 'title' => 'Gimme Shelter', 116 | 'album:exists' => true, 117 | ], 118 | ]); 119 | 120 | $this->assertQueryCount(2); 121 | } 122 | 123 | #[Test] 124 | public function it_can_apply_exists_when_fetching_multiple_resources() 125 | { 126 | $response = $this->json('GET', 'jory', [ 127 | 'jory' => [ 128 | 'song:exists as song_exists' => [ 129 | 'srt' => ['-title'], 130 | 'flt' => [ 131 | 'f' => 'title', 132 | 'o' => 'like', 133 | 'd' => '%love%', 134 | ], 135 | 'fld' => ['title'], 136 | ], 137 | 'band:first' => [ 138 | 'srt' => ['id'], 139 | 'fld' => ['name'], 140 | ] 141 | ] 142 | ]); 143 | 144 | $expected = [ 145 | 'data' => [ 146 | 'song_exists' => true, 147 | 'band:first' => [ 148 | 'name' => 'Rolling Stones', 149 | ], 150 | ], 151 | ]; 152 | $response->assertStatus(200)->assertExactJson($expected)->assertJson($expected); 153 | 154 | $this->assertQueryCount(2); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /tests/FacadeTest.php: -------------------------------------------------------------------------------- 1 | applyJson('{"filter":{"f":"title","o":"like","d":"%love"},"fld":["title"]}') 23 | ->toArray(); 24 | 25 | $this->assertEquals([ 26 | ['title' => 'Whole Lotta Love'], 27 | ['title' => 'May This Be Love'], 28 | ['title' => 'Bold as Love'], 29 | ['title' => 'And the Gods Made Love'], 30 | ], $actual); 31 | 32 | $this->assertQueryCount(1); 33 | } 34 | 35 | #[Test] 36 | public function it_can_apply_on_a_query_using_on() 37 | { 38 | $actual = Jory::on(Song::query()->where('title', 'like', '%ol%')) 39 | ->applyJson('{"filter":{"f":"title","o":"like","d":"%love"},"fld":["title"]}') 40 | ->toArray(); 41 | 42 | $this->assertEquals([ 43 | ['title' => 'Whole Lotta Love'], 44 | ['title' => 'Bold as Love'], 45 | ], $actual); 46 | 47 | $this->assertQueryCount(1); 48 | } 49 | 50 | #[Test] 51 | public function it_can_apply_on_a_model_instance_using_on() 52 | { 53 | $actual = Jory::on(Song::find(47)) 54 | ->applyJson('{"fld":["title"],"rlt":{"album":{"fld":["name"]}}}') 55 | ->toArray(); 56 | 57 | $this->assertEquals([ 58 | 'title' => 'Whole Lotta Love', 59 | 'album' => [ 60 | 'name' => 'Led Zeppelin II', 61 | ] 62 | ], $actual); 63 | 64 | $this->assertQueryCount(3); 65 | } 66 | 67 | #[Test] 68 | public function it_throws_an_exception_when_no_valid_resource_is_given_1() 69 | { 70 | $this->expectException(RegistrationNotFoundException::class); 71 | $this->expectExceptionMessage('No joryResource found for model JosKolenberg\LaravelJory\Http\Controllers\JoryController. Does JosKolenberg\LaravelJory\Http\Controllers\JoryController have an associated JoryResource?'); 72 | Jory::on(JoryController::class); 73 | } 74 | 75 | #[Test] 76 | public function it_throws_an_exception_when_no_valid_resource_is_given_2() 77 | { 78 | $this->expectException(LaravelJoryException::class); 79 | $this->expectExceptionMessage('Unexpected type given. Please provide a model instance, Eloquent builder instance or a model\'s class name.'); 80 | Jory::on(new JoryController()); 81 | } 82 | 83 | #[Test] 84 | public function it_can_register_a_jory_resource_by_class_name() 85 | { 86 | $this->assertInstanceOf(TagJoryResource::class, app(JoryResourcesRegister::class)->getByUri('tag')); 87 | 88 | Jory::register(TagJoryResourceWithExplicitSelect::class); 89 | 90 | $this->assertInstanceOf(TagJoryResourceWithExplicitSelect::class, app(JoryResourcesRegister::class)->getByUri('tag')); 91 | } 92 | 93 | #[Test] 94 | public function it_can_register_a_jory_resource_by_instance() 95 | { 96 | $this->assertInstanceOf(TagJoryResource::class, app(JoryResourcesRegister::class)->getByUri('tag')); 97 | 98 | Jory::register(new TagJoryResourceWithExplicitSelect()); 99 | 100 | $this->assertInstanceOf(TagJoryResourceWithExplicitSelect::class, app(JoryResourcesRegister::class)->getByUri('tag')); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /tests/FirstTest.php: -------------------------------------------------------------------------------- 1 | json('GET', 'jory/song/first', [ 14 | 'jory' => [ 15 | 'srt' => ['-title'], 16 | 'flt' => [ 17 | 'f' => 'title', 18 | 'o' => 'like', 19 | 'd' => '%love%', 20 | ], 21 | 'fld' => ['title'], 22 | ] 23 | ]); 24 | 25 | $expected = [ 26 | 'data' => [ 27 | 'title' => 'Whole Lotta Love', 28 | ], 29 | ]; 30 | $response->assertStatus(200)->assertExactJson($expected)->assertJson($expected); 31 | 32 | $this->assertQueryCount(1); 33 | } 34 | 35 | #[Test] 36 | public function it_can_return_the_first_item_using_the_uri_2() 37 | { 38 | $response = $this->json('GET', 'jory/band/first', [ 39 | 'jory' => '{"filter":{"f":"name","o":"like","d":"%zep%"},"rlt":{"albums":{"flt":{"f":"name","o":"like","d":"%III%"}}},"fld":["id","name"]}', 40 | ]); 41 | 42 | $response->assertStatus(200)->assertExactJson([ 43 | 'data' => [ 44 | 'id' => 2, 45 | 'name' => 'Led Zeppelin', 46 | 'albums' => [ 47 | [ 48 | 'id' => 6, 49 | 'band_id' => 2, 50 | 'name' => 'Led Zeppelin III', 51 | 'release_date' => '1970-10-05 00:00:00', 52 | ], 53 | ], 54 | ], 55 | ]); 56 | 57 | $this->assertQueryCount(2); 58 | } 59 | 60 | #[Test] 61 | public function it_can_return_the_first_item_on_a_relation_1() 62 | { 63 | $response = $this->json('GET', 'jory/band', [ 64 | 'jory' => '{"filter":{"f":"name","o":"like","d":"%es%"},"rlt":{"songs:first":{"flt":{"f":"title","o":"like","d":"%love%"},"fld":["title"]}},"fld":["name"]}', 65 | ]); 66 | 67 | $response->assertStatus(200)->assertExactJson([ 68 | 'data' => [ 69 | [ 70 | 'name' => 'Rolling Stones', 71 | 'songs:first' => [ 72 | 'title' => 'Love In Vain (Robert Johnson)', 73 | ], 74 | ], 75 | [ 76 | 'name' => 'Beatles', 77 | 'songs:first' => [ 78 | 'title' => 'Lovely Rita', 79 | ], 80 | ], 81 | ], 82 | ]); 83 | 84 | $this->assertQueryCount(3); 85 | } 86 | 87 | #[Test] 88 | public function it_can_return_the_first_item_on_a_relation_2() 89 | { 90 | $response = $this->json('GET', 'jory/album/3', [ 91 | 'jory' => '{"rlt":{"songs:first as last_song":{"srt":["-id"],"fld":["title"]}},"fld":["name"]}', 92 | ]); 93 | 94 | $response->assertStatus(200)->assertExactJson([ 95 | 'data' => [ 96 | 'name' => 'Exile on main st.', 97 | 'last_song' => [ 98 | 'title' => 'Soul Survivor', 99 | ], 100 | ], 101 | ]); 102 | 103 | $this->assertQueryCount(2); 104 | } 105 | 106 | #[Test] 107 | public function it_doesnt_fail_when_requesting_the_first_item_on_a_non_collection_relation() 108 | { 109 | $response = $this->json('GET', 'jory/song/first', [ 110 | 'jory' => '{"rlt":{"album:first":{"fld":["name"]}},"fld":["title"]}', 111 | ]); 112 | 113 | $response->assertStatus(200)->assertExactJson([ 114 | 'data' => [ 115 | 'title' => 'Gimme Shelter', 116 | 'album:first' => [ 117 | 'name' => 'Let it bleed', 118 | ], 119 | ], 120 | ]); 121 | 122 | $this->assertQueryCount(2); 123 | } 124 | 125 | #[Test] 126 | public function it_can_return_the_first_item_when_fetching_multiple_resources() 127 | { 128 | $response = $this->json('GET', 'jory', [ 129 | 'jory' => [ 130 | 'song:first as first_song' => [ 131 | 'srt' => ['-title'], 132 | 'flt' => [ 133 | 'f' => 'title', 134 | 'o' => 'like', 135 | 'd' => '%love%', 136 | ], 137 | 'fld' => ['title'], 138 | ], 139 | 'band:first' => [ 140 | 'srt' => ['id'], 141 | 'fld' => ['name'], 142 | ] 143 | ] 144 | ]); 145 | 146 | $expected = [ 147 | 'data' => [ 148 | 'first_song' => [ 149 | 'title' => 'Whole Lotta Love', 150 | ], 151 | 'band:first' => [ 152 | 'name' => 'Rolling Stones', 153 | ], 154 | ], 155 | ]; 156 | $response->assertStatus(200)->assertExactJson($expected)->assertJson($expected); 157 | 158 | $this->assertQueryCount(2); 159 | } 160 | 161 | #[Test] 162 | public function it_returns_a_404_when_a_model_is_not_found_by_id() 163 | { 164 | $response = $this->json('GET', 'jory/band/1234', [ 165 | 'jory' => [ 166 | 'srt' => ['id'], 167 | 'fld' => ['name'], 168 | ] 169 | ]); 170 | 171 | $expected = [ 172 | 'message' => 'No query results for model [JosKolenberg\LaravelJory\Tests\Models\Band] 1234', 173 | ]; 174 | $response->assertStatus(404)->assertExactJson($expected)->assertJson($expected); 175 | 176 | $this->assertQueryCount(1); 177 | } 178 | 179 | #[Test] 180 | public function it_returns_a_404_when_a_model_is_not_found_by_first() 181 | { 182 | $response = $this->json('GET', 'jory/band/first', [ 183 | 'jory' => [ 184 | 'flt' => [ 185 | 'f' => 'name', 186 | 'd' => 'The Kinks' 187 | ], 188 | 'srt' => ['id'], 189 | 'fld' => ['name'], 190 | ] 191 | ]); 192 | 193 | $expected = [ 194 | 'message' => 'No query results for model [JosKolenberg\LaravelJory\Tests\Models\Band].', 195 | ]; 196 | $response->assertStatus(404)->assertExactJson($expected)->assertJson($expected); 197 | 198 | $this->assertQueryCount(1); 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /tests/JoryRegisterTest.php: -------------------------------------------------------------------------------- 1 | expectException(RegistrationNotFoundException::class); 17 | $this->expectExceptionMessage('No joryResource found for model JosKolenberg\LaravelJory\Tests\Models\ModelWithoutJoryResource. Does JosKolenberg\LaravelJory\Tests\Models\ModelWithoutJoryResource have an associated JoryResource?'); 18 | 19 | Jory::on(Song::find(1))->apply([ 20 | 'rlt' => [ 21 | 'testRelationWithoutJoryResource' => [] 22 | ] 23 | ])->toArray(); 24 | } 25 | #[Test] 26 | public function it_doesnt_throw_an_exception_when_no_associated_jory_resource_is_found_as_long_as_the_relation_isnt_requested() 27 | { 28 | $response = $this->json('GET', 'jory/song/1', [ 29 | 'jory' => [ 30 | 'fld' => ['title'], 31 | ] 32 | ]); 33 | 34 | $response->assertStatus(200); 35 | } 36 | 37 | #[Test] 38 | public function it_does_throw_an_exception_when_no_associated_jory_resource_is_found_when_the_relation_is_requested_1() 39 | { 40 | $response = $this->json('GET', 'jory/song/1', [ 41 | 'jory' => [ 42 | 'fld' => ['title'], 43 | 'rlt' => [ 44 | 'testRelationWithoutJoryResource' => [] 45 | ] 46 | ] 47 | ]); 48 | $response->assertStatus(500); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /tests/JoryResources/AutoRegistered/AlbumCoverJoryResource.php: -------------------------------------------------------------------------------- 1 | field('id')->filterable()->sortable(); 18 | $this->field('image')->filterable()->sortable(); 19 | $this->field('album_id')->filterable()->sortable(); 20 | 21 | // Custom sorts 22 | $this->sort('album_name', new AlbumCoverAlbumNameSort); 23 | 24 | // Relations 25 | $this->relation('album'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/JoryResources/AutoRegistered/AlbumJoryResource.php: -------------------------------------------------------------------------------- 1 | field('id')->filterable()->sortable(); 22 | $this->field('name')->filterable()->sortable(); 23 | $this->field('band_id')->filterable()->sortable(); 24 | $this->field('release_date')->filterable()->sortable(); 25 | $this->field('custom_field')->hideByDefault(); 26 | 27 | $this->field('cover_image')->load('cover')->hideByDefault(); 28 | $this->field('titles_string')->load('songs')->hideByDefault(); 29 | $this->field('tag_names_string')->load('tags')->hideByDefault(); 30 | 31 | $this->filter('number_of_songs', new NumberOfSongsFilter); 32 | $this->filter('has_song_with_title', new HasSongWithTitleFilter); 33 | $this->filter('albumCover.album_id'); 34 | $this->filter('has_small_id', new HasSmallIdFilter); 35 | 36 | $this->sort('number_of_songs', new NumberOfSongsSort); 37 | $this->sort('band_name', new BandNameSort); 38 | $this->sort('alphabetic_name', new AlphabeticNameSort); 39 | 40 | $this->relation('songs', SongJoryResource::class); 41 | $this->relation('band'); 42 | $this->relation('cover'); 43 | $this->relation('albumCover', AlbumCoverJoryResource::class); 44 | $this->relation('customSongs2', CustomSongJoryResource::class); 45 | $this->relation('customSongs3', SongJoryResource::class); 46 | $this->relation('tags'); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/JoryResources/AutoRegistered/BandJoryResource.php: -------------------------------------------------------------------------------- 1 | field('id')->filterable(function (Filter $filter) { 18 | $filter->operators(['=', '>', '<', '<=', '>=', '<>', '!=', 'in', 'not_in']); 19 | })->sortable(); 20 | 21 | $this->field('name')->filterable()->sortable(); 22 | 23 | $this->field('year_start')->filterable()->sortable(); 24 | 25 | $this->field('year_end')->filterable()->sortable(); 26 | 27 | $this->field('all_albums_string')->load('albums')->hideByDefault(); 28 | $this->field('titles_string')->load('songs')->hideByDefault(); 29 | $this->field('first_title_string')->load('firstSong')->hideByDefault(); 30 | $this->field('image_urls_string')->load('images')->hideByDefault(); 31 | 32 | $this->filter('has_album_with_name', new HasAlbumWithNameFilter); 33 | $this->filter('number_of_albums_in_year', new NumberOfAlbumsInYearFilter)->operators([ 34 | '=', 35 | '>', 36 | '<', 37 | '<=', 38 | '>=', 39 | '<>', 40 | '!=', 41 | ]); 42 | 43 | $this->limitDefault(30)->limitMax(120); 44 | 45 | $this->relation('albums'); 46 | $this->relation('people'); 47 | $this->relation('songs'); 48 | $this->relation('firstSong'); 49 | $this->relation('images'); 50 | } 51 | 52 | public function authorize($builder, $user = null): void 53 | { 54 | if($user && $user->id == 1){ 55 | $builder->where('id', '>=', 3); 56 | } 57 | 58 | if($user && $user->id == 2){ 59 | $builder->where('id', '<', 2) 60 | ->orWhere('id', '>', 3); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/JoryResources/AutoRegistered/ImageJoryResource.php: -------------------------------------------------------------------------------- 1 | field('id')->filterable()->sortable(); 21 | $this->field('url')->filterable()->sortable(); 22 | $this->field('imageable_id')->filterable()->sortable(); 23 | $this->field('imageable_type')->filterable()->sortable(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/JoryResources/AutoRegistered/InstrumentJoryResource.php: -------------------------------------------------------------------------------- 1 | field('id')->filterable()->sortable(); 18 | $this->field('name')->filterable(function (Filter $filter){ 19 | $filter->scope(new NameFilter); 20 | })->sortable(); 21 | $this->field('type_name')->filterable()->sortable()->hideByDefault(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/JoryResources/AutoRegistered/PersonJoryResource.php: -------------------------------------------------------------------------------- 1 | field('id')->filterable()->sortable(); 18 | $this->field('first_name')->filterable()->sortable(); 19 | $this->field('last_name')->filterable()->sortable(); 20 | $this->field('date_of_birth')->filterable()->sortable(); 21 | $this->field('full_name')->filterable(function (Filter $filter){ 22 | $filter->scope(new FullNameFilter); 23 | }); 24 | 25 | // Custom attributes 26 | $this->field('instruments_string')->load('instruments')->hideByDefault(); 27 | $this->field('first_image_url')->load('firstImage')->hideByDefault(); 28 | 29 | $this->filter('band.albums.songs.title'); 30 | $this->filter('instruments.name'); 31 | 32 | // Relations 33 | $this->relation('instruments'); 34 | $this->relation('firstImage'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/JoryResources/AutoRegistered/SongJoryResource.php: -------------------------------------------------------------------------------- 1 | field('id')->filterable()->sortable(); 17 | $this->field('title')->filterable()->sortable(); 18 | $this->field('album_id')->filterable()->sortable(); 19 | 20 | // Custom attributes 21 | $this->field('album_name')->load('album')->hideByDefault(); 22 | 23 | // Custom filters 24 | $this->filter('album_name', new AlbumNameFilter); 25 | 26 | // Relations 27 | $this->relation('album'); 28 | $this->relation('testRelationWithoutJoryResource'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/JoryResources/AutoRegistered/TagJoryResource.php: -------------------------------------------------------------------------------- 1 | field('id')->filterable()->sortable(); 21 | $this->field('name')->filterable()->sortable(); 22 | 23 | $this->field('song_titles_string')->load('songs')->hideByDefault(); 24 | 25 | // Relations 26 | $this->relation('albums'); 27 | $this->relation('songs'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/JoryResources/AutoRegistered/UnrelevantFileForAutoRegistrarTest.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joskolenberg/laravel-jory/5d6ce2ead9601a546c46065d1332e59cd65c00c7/tests/JoryResources/AutoRegistered/UnrelevantFileForAutoRegistrarTest.txt -------------------------------------------------------------------------------- /tests/JoryResources/Unregistered/AlbumCoverJoryResourceWithExplicitSelect.php: -------------------------------------------------------------------------------- 1 | explicitSelect(); 17 | 18 | // Fields 19 | $this->field('id')->filterable()->sortable(); 20 | $this->field('image')->filterable()->sortable(); 21 | $this->field('album_id')->filterable()->sortable(); 22 | 23 | // Custom sorts 24 | $this->sort('album_name', new AlbumCoverAlbumNameSort); 25 | 26 | // Relations 27 | $this->relation('album'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/JoryResources/Unregistered/AlbumCoverJoryResourceWithoutRoutes.php: -------------------------------------------------------------------------------- 1 | field('id')->filterable()->sortable(); 19 | $this->field('image')->filterable()->sortable(); 20 | $this->field('album_id')->filterable()->sortable(); 21 | 22 | // Custom sorts 23 | $this->sort('album_name', new AlbumCoverAlbumNameSort); 24 | 25 | // Relations 26 | $this->relation('album'); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/JoryResources/Unregistered/AlbumJoryResourceWithExplicitSelect.php: -------------------------------------------------------------------------------- 1 | explicitSelect(); 21 | 22 | $this->field('id')->filterable()->sortable(); 23 | $this->field('name')->filterable()->sortable(); 24 | $this->field('band_id')->filterable()->sortable(); 25 | $this->field('release_date')->filterable()->sortable(); 26 | $this->field('custom_field')->noSelect()->hideByDefault(); 27 | 28 | $this->field('cover_image')->noSelect()->load('cover')->hideByDefault(); 29 | $this->field('titles_string')->noSelect()->load('songs')->hideByDefault(); 30 | $this->field('tag_names_string')->noSelect()->load('tags')->hideByDefault(); 31 | 32 | $this->filter('number_of_songs', new NumberOfSongsFilter); 33 | $this->filter('has_song_with_title', new HasSongWithTitleFilter); 34 | $this->filter('album_cover.album_id'); 35 | 36 | $this->sort('number_of_songs', new NumberOfSongsSort); 37 | $this->sort('band_name', new BandNameSort); 38 | 39 | $this->relation('songs'); 40 | $this->relation('band'); 41 | $this->relation('cover'); 42 | $this->relation('albumCover', AlbumCoverJoryResource::class); 43 | $this->relation('customSongs2', CustomSongJoryResource::class); 44 | $this->relation('customSongs3', SongJoryResource::class); 45 | $this->relation('tags'); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/JoryResources/Unregistered/BandJoryResourceWithExplicitSelect.php: -------------------------------------------------------------------------------- 1 | explicitSelect(); 18 | 19 | $this->field('id')->filterable(function (Filter $filter) { 20 | $filter->operators(['=', '>', '<', '<=', '>=', '<>', '!=', 'in', 'not_in']); 21 | })->sortable(); 22 | 23 | $this->field('name')->filterable()->sortable(); 24 | 25 | $this->field('year_start')->filterable()->sortable(); 26 | 27 | $this->field('year_end')->filterable()->sortable(); 28 | 29 | $this->field('all_albums_string')->noSelect()->load('albums')->hideByDefault(); 30 | $this->field('titles_string')->noSelect()->load('songs')->hideByDefault(); 31 | $this->field('first_title_string')->noSelect()->load('firstSong')->hideByDefault(); 32 | $this->field('image_urls_string')->noSelect()->load('images')->hideByDefault(); 33 | 34 | $this->filter('has_album_with_name', new HasAlbumWithNameFilter); 35 | $this->filter('number_of_albums_in_year', new NumberOfAlbumsInYearFilter)->operators([ 36 | '=', 37 | '>', 38 | '<', 39 | '<=', 40 | '>=', 41 | '<>', 42 | '!=', 43 | ]); 44 | 45 | $this->limitDefault(30)->limitMax(120); 46 | 47 | $this->relation('albums'); 48 | $this->relation('people'); 49 | $this->relation('songs'); 50 | $this->relation('firstSong'); 51 | $this->relation('images'); 52 | } 53 | 54 | public function authorize($builder, $user = null): void 55 | { 56 | if($user && $user->id == 1){ 57 | $builder->where('id', '>=', 3); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/JoryResources/Unregistered/CustomSongJoryResource.php: -------------------------------------------------------------------------------- 1 | field('id')->filterable()->sortable(); 17 | $this->field('title')->filterable()->sortable(); 18 | $this->field('album_id')->filterable()->sortable(); 19 | $this->field('custom_field'); 20 | $this->field('description', new SongDescription); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/JoryResources/Unregistered/CustomSongJoryResource2.php: -------------------------------------------------------------------------------- 1 | field('id')->filterable()->sortable(); 16 | $this->field('title')->filterable()->sortable(); 17 | $this->field('album_id')->filterable()->sortable(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/JoryResources/Unregistered/ImageJoryResourceWithExplicitSelect.php: -------------------------------------------------------------------------------- 1 | explicitSelect(); 20 | 21 | // Fields 22 | $this->field('id')->filterable()->sortable(); 23 | $this->field('url')->filterable()->sortable(); 24 | $this->field('imageable_id')->filterable()->sortable(); 25 | $this->field('imageable_type')->filterable()->sortable(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/JoryResources/Unregistered/InstrumentJoryResourceWithExplicitSelect.php: -------------------------------------------------------------------------------- 1 | explicitSelect(); 17 | 18 | // Fields 19 | $this->field('id')->filterable()->sortable(); 20 | $this->field('name')->filterable(function (Filter $filter){ 21 | $filter->scope(new NumberOfAlbumsInYearFilter); 22 | })->sortable(); 23 | $this->field('type_name')->filterable()->sortable()->hideByDefault(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/JoryResources/Unregistered/PersonJoryResourceWithCallables.php: -------------------------------------------------------------------------------- 1 | field('id'); 19 | $this->field('first_name')->filterable(function (Filter $filter){ 20 | $filter->scope(function ($builder, string $operator = null, $data = null){ 21 | if($data['is_reversed']){ 22 | $data = strrev($data['value']); 23 | } 24 | 25 | FilterHelper::applyWhere($builder, 'first_name', $operator, $data); 26 | }); 27 | }); 28 | $this->field('last_name')->filterable()->sortable(function(Sort $sort){ 29 | $sort->scope(function($builder, string $order = 'asc'){ 30 | $builder->orderBy('last_name', $order === 'asc' ? 'desc' : 'asc'); 31 | }); 32 | }); 33 | 34 | $this->filter('full_name', function ($builder, string $operator = null, $data = null){ 35 | $builder->where('first_name', $operator, $data) 36 | ->orWhere('last_name', $operator, $data); 37 | }); 38 | 39 | $this->sort('last_name_alias', function($builder, string $order = 'asc'){ 40 | $builder->orderBy('last_name', $order); 41 | }); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/JoryResources/Unregistered/PersonJoryResourceWithExplicitSelect.php: -------------------------------------------------------------------------------- 1 | explicitSelect(); 17 | 18 | // Fields 19 | $this->field('id')->select('people.id')->filterable()->sortable(); 20 | $this->field('first_name')->filterable()->sortable(); 21 | $this->field('last_name')->filterable()->sortable(); 22 | $this->field('date_of_birth')->select(['date_of_birth'])->filterable()->sortable(); 23 | $this->field('full_name') 24 | ->select('first_name', 'last_name') 25 | ->filterable(function (Filter $filter){ 26 | $filter->scope(new FullNameFilter); 27 | }); 28 | 29 | // Custom attributes 30 | $this->field('instruments_string')->noSelect()->load('instruments')->hideByDefault(); 31 | $this->field('first_image_url')->noSelect()->load('firstImage')->hideByDefault(); 32 | 33 | $this->filter('band.albums.songs.title'); 34 | $this->filter('instruments.name'); 35 | 36 | // Relations 37 | $this->relation('instruments'); 38 | $this->relation('firstImage'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/JoryResources/Unregistered/PersonJoryResourceWithScopes.php: -------------------------------------------------------------------------------- 1 | field('id')->filterable()->sortable(); 21 | $this->field('first_name')->filterable(function ($filter){ 22 | $filter->scope(new SpecialFirstNameFilter()); 23 | })->sortable(); 24 | $this->field('last_name')->filterable()->sortable(); 25 | $this->field('date_of_birth')->filterable()->sortable(function(Sort $sort){ 26 | $sort->scope(new FirstNameSort); 27 | }); 28 | $this->field('full_name')->filterable(function (Filter $filter){ 29 | $filter->scope(new FullNameFilter); 30 | }); 31 | 32 | // Custom attributes 33 | $this->field('instruments_string')->load('instruments')->hideByDefault(); 34 | $this->field('first_image_url')->load('firstImage')->hideByDefault(); 35 | 36 | $this->filter('band.albums.songs.title'); 37 | $this->filter('instruments.name'); 38 | 39 | // Relations 40 | $this->relation('instruments'); 41 | $this->relation('firstImage'); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/JoryResources/Unregistered/SongJoryResourceWithAlternateUri.php: -------------------------------------------------------------------------------- 1 | field('id')->sortable(); 16 | 17 | $this->field('title')->filterable()->sortable(); 18 | 19 | $this->field('album_id')->hideByDefault()->filterable(function (Filter $filter) { 20 | $filter->operators(['=']); 21 | }); 22 | 23 | $this->limitDefault(50)->limitMax(250); 24 | 25 | $this->relation('album'); 26 | $this->relation('testRelationWithoutJoryResource'); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/JoryResources/Unregistered/SongJoryResourceWithConfigThree.php: -------------------------------------------------------------------------------- 1 | field('id'); 17 | $this->field('title'); 18 | $this->field('album_id'); 19 | 20 | $this->limitDefault(null)->limitMax(10); 21 | 22 | $this->sort('title')->default(2, 'desc'); 23 | $this->sort('album_name', new SongAlbumNameSort)->default(1, 'asc'); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/JoryResources/Unregistered/SongJoryResourceWithConfigTwo.php: -------------------------------------------------------------------------------- 1 | field('id'); 16 | $this->field('title')->filterable()->sortable(); 17 | $this->field('album_id'); 18 | 19 | $this->limitDefault(null)->limitMax(null); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/JoryResources/Unregistered/SongJoryResourceWithExplicitSelect.php: -------------------------------------------------------------------------------- 1 | explicitSelect(); 15 | 16 | // Fields 17 | $this->field('id')->filterable()->sortable(); 18 | $this->field('title')->filterable()->sortable(); 19 | $this->field('album_id')->filterable()->sortable(); 20 | 21 | // Custom attributes 22 | $this->field('album_name')->noSelect()->load('album')->hideByDefault(); 23 | 24 | // Custom filters 25 | $this->filter('album_name'); 26 | 27 | // Relations 28 | $this->relation('album'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/JoryResources/Unregistered/TagJoryResourceWithExplicitSelect.php: -------------------------------------------------------------------------------- 1 | explicitSelect(); 20 | 21 | // Fields 22 | $this->field('id')->filterable()->sortable(); 23 | $this->field('name')->filterable()->sortable(); 24 | 25 | $this->field('song_titles_string')->noSelect()->load('songs')->hideByDefault(); 26 | 27 | // Relations 28 | $this->relation('albums'); 29 | $this->relation('songs'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/Models/AlbumCover.php: -------------------------------------------------------------------------------- 1 | 'integer', 13 | 'album_id' => 'integer', 14 | ]; 15 | 16 | public function album() 17 | { 18 | return $this->belongsTo(Album::class); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/Models/Band.php: -------------------------------------------------------------------------------- 1 | 'integer', 17 | 'year_start' => 'integer', 18 | 'year_end' => 'integer', 19 | ]; 20 | 21 | public function people() 22 | { 23 | return $this->belongsToMany(Person::class, 'band_members'); 24 | } 25 | 26 | public function albums() 27 | { 28 | return $this->hasMany(Album::class); 29 | } 30 | 31 | public function songs() 32 | { 33 | return $this->hasManyThrough(Song::class, Album::class); 34 | } 35 | 36 | public function firstSong() 37 | { 38 | return $this->hasOneThrough(Song::class, Album::class)->orderBy('songs.id'); 39 | } 40 | 41 | public function getAllAlbumsStringAttribute() 42 | { 43 | $result = ''; 44 | 45 | $first = true; 46 | foreach ($this->albums as $album) { 47 | if ($first) { 48 | $first = false; 49 | } else { 50 | $result .= ', '; 51 | } 52 | $result .= $album->name; 53 | } 54 | 55 | return $result; 56 | } 57 | 58 | public function images() 59 | { 60 | return $this->morphMany(Image::class, 'imageable'); 61 | } 62 | 63 | public function getTitlesStringAttribute() 64 | { 65 | return implode(', ', $this->songs->pluck('title')->toArray()); 66 | } 67 | 68 | public function getFirstTitleStringAttribute() 69 | { 70 | return $this->firstSong->title; 71 | } 72 | 73 | public function getImageUrlsStringAttribute() 74 | { 75 | return implode(', ', $this->images->pluck('url')->toArray()); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /tests/Models/Groupie.php: -------------------------------------------------------------------------------- 1 | 'integer', 18 | ]; 19 | 20 | public function person() 21 | { 22 | return $this->belongsTo(Person::class); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/Models/Image.php: -------------------------------------------------------------------------------- 1 | 'integer', 11 | 'imageable_id' => 'integer', 12 | ]; 13 | 14 | /** 15 | * Get the owning imageable model. 16 | */ 17 | public function imageable() 18 | { 19 | return $this->morphTo(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/Models/Instrument.php: -------------------------------------------------------------------------------- 1 | 'integer', 15 | ]; 16 | 17 | public function people() 18 | { 19 | return $this->belongsToMany(Person::class); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/Models/Model.php: -------------------------------------------------------------------------------- 1 | format('Y/m/d'); 12 | } 13 | 14 | protected $hidden = [ 15 | 'pivot', 16 | ]; 17 | 18 | protected $casts = [ 19 | 'id' => 'integer', 20 | 'date_of_birth' => 'datetime', 21 | ]; 22 | 23 | protected $appends = [ 24 | 'full_name', 25 | ]; 26 | 27 | public function instruments() 28 | { 29 | return $this->belongsToMany(Instrument::class, 'instrument_person'); 30 | } 31 | 32 | public function getFullNameAttribute() 33 | { 34 | return $this->first_name.' '.$this->last_name; 35 | } 36 | 37 | public function groupies() 38 | { 39 | return $this->hasMany(Groupie::class); 40 | } 41 | 42 | public function band() 43 | { 44 | return $this->belongsToMany(Band::class, 'band_members'); 45 | } 46 | 47 | public function firstImage() 48 | { 49 | return $this->morphOne(Image::class, 'imageable'); 50 | } 51 | 52 | public function getInstrumentsStringAttribute() 53 | { 54 | return implode(', ', $this->instruments->pluck('name')->toArray()); 55 | } 56 | 57 | public function getFirstImageUrlAttribute() 58 | { 59 | return $this->firstImage->url; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/Models/Song.php: -------------------------------------------------------------------------------- 1 | 'integer', 13 | 'album_id' => 'integer', 14 | ]; 15 | 16 | public function album() 17 | { 18 | return $this->belongsTo(Album::class); 19 | } 20 | 21 | public function getAlbumNameAttribute() 22 | { 23 | return $this->album->name; 24 | } 25 | 26 | public function getCustomFieldAttribute() 27 | { 28 | return 'custom_value'; 29 | } 30 | 31 | public function testRelationWithoutJoryResource() 32 | { 33 | return $this->hasMany(ModelWithoutJoryResource::class); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/Models/SongWithCustomJoryResource.php: -------------------------------------------------------------------------------- 1 | 'integer', 17 | 'band_id' => 'integer', 18 | 'release_date' => 'datetime', 19 | ]; 20 | 21 | protected function serializeDate(\DateTimeInterface $date) 22 | { 23 | return $date->format('Y-m-d H:i:s'); 24 | } 25 | 26 | public function songs() 27 | { 28 | return $this->hasMany(Song::class); 29 | } 30 | 31 | public function customSongs1() 32 | { 33 | return $this->songs(); 34 | } 35 | 36 | public function customSongs2() 37 | { 38 | return $this->songs(); 39 | } 40 | 41 | public function customSongs3 () 42 | { 43 | return $this->songs(); 44 | } 45 | 46 | public function band() 47 | { 48 | return $this->belongsTo(Band::class); 49 | } 50 | 51 | public function cover() 52 | { 53 | return $this->hasOne(AlbumCover::class); 54 | } 55 | 56 | public function albumCover() 57 | { 58 | return $this->hasOne(AlbumCover::class); 59 | } 60 | 61 | public function tags() 62 | { 63 | return $this->morphToMany(Tag::class, 'taggable'); 64 | } 65 | 66 | public function getCoverImageAttribute() 67 | { 68 | return $this->cover->image; 69 | } 70 | 71 | public function getTitlesStringAttribute() 72 | { 73 | return implode(', ', $this->songs->pluck('title')->toArray()); 74 | } 75 | 76 | public function getTagNamesStringAttribute() 77 | { 78 | return implode(', ', $this->tags->pluck('name')->toArray()); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /tests/Models/SubFolder/NonModelClass.php: -------------------------------------------------------------------------------- 1 | 'integer', 13 | 'taggable_id' => 'integer', 14 | ]; 15 | 16 | public function songs() 17 | { 18 | return $this->morphedByMany(Song::class, 'taggable'); 19 | } 20 | 21 | public function albums() 22 | { 23 | return $this->morphedByMany(Album::class, 'taggable'); 24 | } 25 | 26 | public function getSongTitlesStringAttribute() 27 | { 28 | return implode(', ', $this->songs->pluck('title')->toArray()); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/Models/User.php: -------------------------------------------------------------------------------- 1 | applyJson('{"song":{"filter":{"f":"title","o":"like","d":"%love"},"fld":["title"]},"song:count as songcount":{"filter":{"f":"title","o":"like","d":"%love"},"fld":["title"]}}') 17 | ->toArray(); 18 | 19 | $this->assertEquals([ 20 | 'song' => [ 21 | ['title' => 'Whole Lotta Love'], 22 | ['title' => 'May This Be Love'], 23 | ['title' => 'Bold as Love'], 24 | ['title' => 'And the Gods Made Love'], 25 | ], 26 | 'songcount' => 4, 27 | ], $actual); 28 | 29 | $this->assertQueryCount(2); 30 | } 31 | 32 | #[Test] 33 | public function it_can_return_multiple_resources_applying_json_using_apply() 34 | { 35 | $actual = Jory::multiple() 36 | ->apply('{"song":{"filter":{"f":"title","o":"like","d":"%love"},"fld":["title"]},"song:count as songcount":{"filter":{"f":"title","o":"like","d":"%love"},"fld":["title"]}}') 37 | ->toArray(); 38 | 39 | $this->assertEquals([ 40 | 'song' => [ 41 | ['title' => 'Whole Lotta Love'], 42 | ['title' => 'May This Be Love'], 43 | ['title' => 'Bold as Love'], 44 | ['title' => 'And the Gods Made Love'], 45 | ], 46 | 'songcount' => 4, 47 | ], $actual); 48 | 49 | $this->assertQueryCount(2); 50 | } 51 | 52 | #[Test] 53 | public function it_can_return_multiple_resources_applying_an_array() 54 | { 55 | $actual = Jory::multiple() 56 | ->applyArray([ 57 | 'song' => [ 58 | 'flt' => [ 59 | 'f' => 'title', 60 | 'o' => 'like', 61 | 'd' => '%love', 62 | ], 63 | 'fld' => ['title'] 64 | ], 65 | 'song:count as songcount' => [ 66 | 'flt' => [ 67 | 'f' => 'title', 68 | 'o' => 'like', 69 | 'd' => '%love', 70 | ], 71 | 'fld' => ['title'] 72 | ] 73 | ]) 74 | ->toArray(); 75 | 76 | $this->assertEquals([ 77 | 'song' => [ 78 | ['title' => 'Whole Lotta Love'], 79 | ['title' => 'May This Be Love'], 80 | ['title' => 'Bold as Love'], 81 | ['title' => 'And the Gods Made Love'], 82 | ], 83 | 'songcount' => 4, 84 | ], $actual); 85 | 86 | $this->assertQueryCount(2); 87 | } 88 | 89 | #[Test] 90 | public function it_can_return_multiple_resources_applying_an_array_using_apply() 91 | { 92 | $actual = Jory::multiple() 93 | ->apply([ 94 | 'song' => [ 95 | 'flt' => [ 96 | 'f' => 'title', 97 | 'o' => 'like', 98 | 'd' => '%love', 99 | ], 100 | 'fld' => ['title'] 101 | ], 102 | 'song:count as songcount' => [ 103 | 'flt' => [ 104 | 'f' => 'title', 105 | 'o' => 'like', 106 | 'd' => '%love', 107 | ], 108 | 'fld' => ['title'] 109 | ] 110 | ]) 111 | ->toArray(); 112 | 113 | $this->assertEquals([ 114 | 'song' => [ 115 | ['title' => 'Whole Lotta Love'], 116 | ['title' => 'May This Be Love'], 117 | ['title' => 'Bold as Love'], 118 | ['title' => 'And the Gods Made Love'], 119 | ], 120 | 'songcount' => 4, 121 | ], $actual); 122 | 123 | $this->assertQueryCount(2); 124 | } 125 | 126 | #[Test] 127 | public function it_defaults_to_applying_the_data_in_the_request_when_nothing_is_applied() 128 | { 129 | $response = $this->json('GET', 'jory', [ 130 | 'jory' => [ 131 | 'song' => [ 132 | 'flt' => [ 133 | 'f' => 'title', 134 | 'o' => 'like', 135 | 'd' => '%love', 136 | ], 137 | 'fld' => ['title'] 138 | ], 139 | 'song:count as songcount' => [ 140 | 'flt' => [ 141 | 'f' => 'title', 142 | 'o' => 'like', 143 | 'd' => '%love', 144 | ], 145 | 'fld' => ['title'] 146 | ], 147 | ], 148 | ]); 149 | 150 | $response->assertStatus(200)->assertExactJson([ 151 | 'data' => [ 152 | 'song' => [ 153 | ['title' => 'Whole Lotta Love'], 154 | ['title' => 'May This Be Love'], 155 | ['title' => 'Bold as Love'], 156 | ['title' => 'And the Gods Made Love'], 157 | ], 158 | 'songcount' => 4, 159 | ] 160 | ]); 161 | 162 | $this->assertQueryCount(2); 163 | } 164 | 165 | #[Test] 166 | public function it_throws_an_exception_when_invalid_data_is_applied() 167 | { 168 | $this->expectException(LaravelJoryException::class); 169 | $this->expectExceptionMessage('Unexpected type given. Please provide an array or Json string.'); 170 | 171 | Jory::multiple()->apply(new \stdClass()); 172 | } 173 | } 174 | 175 | -------------------------------------------------------------------------------- /tests/OffsetLimitTest.php: -------------------------------------------------------------------------------- 1 | json('GET', 'jory/song', [ 13 | 'jory' => '{"offset":140,"limit":20}', 14 | ]); 15 | 16 | $response->assertStatus(200)->assertExactJson([ 17 | 'data' => [ 18 | [ 19 | 'id' => 141, 20 | 'album_id' => 12, 21 | 'title' => 'Rainy Day, Dream Away', 22 | ], 23 | [ 24 | 'id' => 142, 25 | 'album_id' => 12, 26 | 'title' => '1983... (A Merman I Should Turn to Be)', 27 | ], 28 | [ 29 | 'id' => 143, 30 | 'album_id' => 12, 31 | 'title' => 'Moon, Turn the Tides...Gently Gently Away', 32 | ], 33 | [ 34 | 'id' => 144, 35 | 'album_id' => 12, 36 | 'title' => 'Still Raining, Still Dreaming', 37 | ], 38 | [ 39 | 'id' => 145, 40 | 'album_id' => 12, 41 | 'title' => 'House Burning Down', 42 | ], 43 | [ 44 | 'id' => 146, 45 | 'album_id' => 12, 46 | 'title' => 'All Along the Watchtower', 47 | ], 48 | [ 49 | 'id' => 147, 50 | 'album_id' => 12, 51 | 'title' => 'Voodoo Child (Slight Return)', 52 | ], 53 | ], 54 | ]); 55 | 56 | $this->assertQueryCount(1); 57 | } 58 | 59 | #[Test] 60 | public function it_can_apply_a_limit_without_an_offset() 61 | { 62 | $response = $this->json('GET', 'jory/song', [ 63 | 'jory' => '{"limit":3}', 64 | ]); 65 | 66 | $response->assertStatus(200)->assertExactJson([ 67 | 'data' => [ 68 | [ 69 | 'id' => 1, 70 | 'album_id' => 1, 71 | 'title' => 'Gimme Shelter', 72 | ], 73 | [ 74 | 'id' => 2, 75 | 'album_id' => 1, 76 | 'title' => 'Love In Vain (Robert Johnson)', 77 | ], 78 | [ 79 | 'id' => 3, 80 | 'album_id' => 1, 81 | 'title' => 'Country Honk', 82 | ], 83 | ], 84 | ]); 85 | 86 | $this->assertQueryCount(1); 87 | } 88 | 89 | #[Test] 90 | public function it_can_apply_an_offset_and_limit_combined_with_with_sorts_and_filters() 91 | { 92 | $response = $this->json('GET', 'jory/song', [ 93 | 'jory' => '{"flt":{"f":"title","o":"like","d":"%love%"},"srt":["title"],"offset":2,"limit":3}', 94 | ]); 95 | 96 | $response->assertStatus(200)->assertExactJson([ 97 | 'data' => [ 98 | [ 99 | 'id' => 130, 100 | 'album_id' => 11, 101 | 'title' => 'Little Miss Lover', 102 | ], 103 | [ 104 | 'id' => 2, 105 | 'album_id' => 1, 106 | 'title' => 'Love In Vain (Robert Johnson)', 107 | ], 108 | [ 109 | 'id' => 112, 110 | 'album_id' => 10, 111 | 'title' => 'Love or Confusion', 112 | ], 113 | ], 114 | ]); 115 | 116 | $this->assertQueryCount(1); 117 | } 118 | 119 | #[Test] 120 | public function it_can_apply_an_offset_and_limit_combined_with_with_sorts_and_filters_on_relations() 121 | { 122 | $response = $this->json('GET', 'jory/band', [ 123 | 'jory' => '{"flt":{"f":"name","d":"Beatles"},"rlt":{"songs":{"flt":{"f":"title","o":"like","d":"%a%"},"srt":["title"],"offset":10,"limit":5,"fld":["id","title"]}}}', 124 | ]); 125 | 126 | $response->assertStatus(200)->assertExactJson([ 127 | 'data' => [ 128 | [ 129 | 'id' => 3, 130 | 'name' => 'Beatles', 131 | 'year_start' => 1960, 132 | 'year_end' => 1970, 133 | 'songs' => [ 134 | [ 135 | 'id' => 103, 136 | 'title' => 'I\'ve Got a Feeling', 137 | ], 138 | [ 139 | 'id' => 75, 140 | 'title' => 'Lovely Rita', 141 | ], 142 | [ 143 | 'id' => 68, 144 | 'title' => 'Lucy in the Sky with Diamonds', 145 | ], 146 | [ 147 | 'id' => 102, 148 | 'title' => 'Maggie Mae', 149 | ], 150 | [ 151 | 'id' => 81, 152 | 'title' => 'Maxwell\'s Silver Hammer', 153 | ], 154 | ], 155 | ], 156 | ], 157 | ]); 158 | 159 | $this->assertQueryCount(2); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /tests/Parsers/RequestParserTest.php: -------------------------------------------------------------------------------- 1 | json('GET', 'jory/person', [ 14 | 'jory' => '{"filter":{"f": "first_name","d":"John"},"fld":["id","last_name"]}', 15 | ]); 16 | 17 | $response->assertStatus(200)->assertExactJson([ 18 | 'data' => [ 19 | [ 20 | 'id' => 8, 21 | 'last_name' => 'Bonham', 22 | ], 23 | [ 24 | 'id' => 9, 25 | 'last_name' => 'Lennon', 26 | ], 27 | ], 28 | ]); 29 | } 30 | 31 | #[Test] 32 | public function it_defaults_to_empty_when_no_data_is_passed() 33 | { 34 | $response = $this->json('GET', 'jory/band'); 35 | 36 | $response->assertStatus(200)->assertExactJson([ 37 | 'data' => [ 38 | [ 39 | 'id' => 1, 40 | 'name' => 'Rolling Stones', 41 | 'year_start' => 1962, 42 | 'year_end' => null, 43 | ], 44 | [ 45 | 'id' => 2, 46 | 'name' => 'Led Zeppelin', 47 | 'year_start' => 1968, 48 | 'year_end' => 1980, 49 | ], 50 | [ 51 | 'id' => 3, 52 | 'name' => 'Beatles', 53 | 'year_start' => 1960, 54 | 'year_end' => 1970, 55 | ], 56 | [ 57 | 'id' => 4, 58 | 'name' => 'Jimi Hendrix Experience', 59 | 'year_start' => 1966, 60 | 'year_end' => 1970, 61 | ], 62 | ], 63 | ]); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /tests/Scopes/AlbumCoverAlbumNameSort.php: -------------------------------------------------------------------------------- 1 | join('albums', 'album_covers.album_id', 'albums.id')->orderBy('albums.name', $order); 22 | } 23 | } -------------------------------------------------------------------------------- /tests/Scopes/AlbumNameFilter.php: -------------------------------------------------------------------------------- 1 | whereHas('album', function ($builder) use ($operator, $data) { 25 | $builder->where('name', $operator, $data); 26 | }); 27 | } 28 | } -------------------------------------------------------------------------------- /tests/Scopes/AlphabeticNameSort.php: -------------------------------------------------------------------------------- 1 | orderBy('name', $order); 22 | } 23 | } -------------------------------------------------------------------------------- /tests/Scopes/BandNameSort.php: -------------------------------------------------------------------------------- 1 | join('bands', 'band_id', 'bands.id')->orderBy('bands.name', $order); 22 | } 23 | } -------------------------------------------------------------------------------- /tests/Scopes/CustomFilterFieldFilter.php: -------------------------------------------------------------------------------- 1 | orderBy('first_name', $order); 22 | } 23 | } -------------------------------------------------------------------------------- /tests/Scopes/FullNameFilter.php: -------------------------------------------------------------------------------- 1 | where('first_name', 'like', '%'.$data.'%'); 25 | $builder->orWhere('last_name', 'like', '%'.$data.'%'); 26 | } 27 | } -------------------------------------------------------------------------------- /tests/Scopes/HasAlbumWithNameFilter.php: -------------------------------------------------------------------------------- 1 | whereHas('albums', function ($builder) use ($operator, $data) { 25 | $builder->where('name', $operator, $data); 26 | }); 27 | } 28 | } -------------------------------------------------------------------------------- /tests/Scopes/HasSmallIdFilter.php: -------------------------------------------------------------------------------- 1 | where('id', '<', 3); 20 | } 21 | } -------------------------------------------------------------------------------- /tests/Scopes/HasSongWithTitleFilter.php: -------------------------------------------------------------------------------- 1 | whereHas('songs', function ($builder) use ($operator, $data) { 25 | $builder->where('title', $operator, $data); 26 | }); 27 | } 28 | } -------------------------------------------------------------------------------- /tests/Scopes/NameFilter.php: -------------------------------------------------------------------------------- 1 | has('people'); 27 | } 28 | } -------------------------------------------------------------------------------- /tests/Scopes/NumberOfAlbumsInYearFilter.php: -------------------------------------------------------------------------------- 1 | whereHas('albums', function ($builder) use ($year) { 28 | $builder->where('release_date', '>=', $year.'-01-01'); 29 | $builder->where('release_date', '<=', $year.'-12-31'); 30 | }, $operator, $value); 31 | } 32 | } -------------------------------------------------------------------------------- /tests/Scopes/NumberOfSongsFilter.php: -------------------------------------------------------------------------------- 1 | has('songs', $operator, $data); 25 | } 26 | } -------------------------------------------------------------------------------- /tests/Scopes/NumberOfSongsSort.php: -------------------------------------------------------------------------------- 1 | withCount('songs')->orderBy('songs_count', $order); 22 | } 23 | } -------------------------------------------------------------------------------- /tests/Scopes/SongAlbumNameSort.php: -------------------------------------------------------------------------------- 1 | join('albums', 'songs.album_id', 'albums.id')->orderBy('albums.name', $order); 22 | } 23 | } -------------------------------------------------------------------------------- /tests/Scopes/SpecialFirstNameFilter.php: -------------------------------------------------------------------------------- 1 | where('first_name', '=', 'John'); 20 | } 21 | } --------------------------------------------------------------------------------