├── .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 | 
2 | [](https://packagist.org/packages/joskolenberg/laravel-jory)
3 | [](https://packagist.org/packages/joskolenberg/laravel-jory)
4 | [](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 | }
--------------------------------------------------------------------------------