From 0fbd8a48faad759cfa07f2a8b94be5760573e898 Mon Sep 17 00:00:00 2001 From: Aleksei Zarubin <12220926+alexzarbn@users.noreply.github.com> Date: Sun, 19 Dec 2021 16:14:17 +0500 Subject: [PATCH] feat: introduce case-insensitive search and setup Circle CI (#139) --- .circleci/config.yml | 170 ++++++++++++++++++ .github/workflows/ci.yml | 133 -------------- README.md | 8 +- config/orion.php | 3 + src/Drivers/Standard/ParamsValidator.php | 1 + src/Drivers/Standard/QueryBuilder.php | 40 ++++- .../RequestBody/Search/SearchBuilder.php | 4 + ...onStandardIndexFilteringOperationsTest.php | 55 ++++-- .../StandardIndexSearchingOperationsTest.php | 41 ++++- tests/Feature/TestCase.php | 1 + 10 files changed, 298 insertions(+), 158 deletions(-) create mode 100644 .circleci/config.yml delete mode 100644 .github/workflows/ci.yml diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 00000000..42dce9f8 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,170 @@ +version: 2.1 + +executors: + sqlite: + parameters: + php-version: + type: string + docker: + - image: cimg/php:<< parameters.php-version >> + mysql: + parameters: + php-version: + type: string + docker: + - image: cimg/php:<< parameters.php-version >> + - image: mysql:5.7 + environment: + MYSQL_ALLOW_EMPTY_PASSWORD: yes + MYSQL_DATABASE: orion + pgsql: + parameters: + php-version: + type: string + docker: + - image: cimg/php:<< parameters.php-version >> + - image: postgres:10.8 + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: orion + +jobs: + test: + parameters: + php-version: + type: string + laravel-version: + type: string + database: + type: string + executor: + name: << parameters.database >> + php-version: << parameters.php-version >> + steps: + - checkout + + # install extensions + - when: + condition: + and: + - equal: ["7.3", <>] + - equal: ["sqlite", <>] + steps: + - run: + name: "Install Sqlite extension" + command: sudo apt-get update && sudo apt-get install -y php7.3-sqlite3 && sudo rm -rf /var/lib/apt/lists/* + + # restore composer cache + - restore_cache: + keys: + # "composer.lock" can be used if it is committed to the repo + - v1-dependencies-{{ checksum "composer.json" }} + # fallback to using the latest cache if no exact match is found + - v1-dependencies- + + # install dependencies + - run: + name: "Install dependencies" + command: composer update --with "illuminate/contracts=<< parameters.laravel-version>>" --prefer-dist --no-progress --no-suggest + + # install laravel/legacy-factories and PHPUnit 9 only for Laravel 8.0 + - when: + condition: + equal: ["^8.0", <>] + steps: + - run: + name: "Install laravel/legacy-factories" + command: composer require "laravel/legacy-factories" --prefer-dist --no-progress --no-suggest + - run: + name: "Install PHPUnit 9" + command: composer update --with "phpunit/phpunit=^9.0" --prefer-dist --no-progress --no-suggest + - run: + name: "Upgrade PHPUnit config" + command: vendor/bin/phpunit -c phpunit.xml.dist --migrate-configuration + + # save composer cache + - save_cache: + key: v1-dependencies-{{ checksum "composer.json" }} + paths: + - ./vendor + + # Run test suite with Sqlite + - when: + condition: + equal: ["sqlite", <>] + steps: + - run: + name: "Run test suite" + command: vendor/bin/phpunit --debug --verbose + environment: + DB_CONNECTION: sqlite + DB_DATABASE: ":memory:" + + # Run test suite with MySQL + - when: + condition: + equal: ["mysql", <>] + steps: + - run: + name: "Run test suite" + command: vendor/bin/phpunit --debug --verbose + environment: + DB_CONNECTION: mysql + DB_PORT: 3306 + DB_DATABASE: orion + DB_USERNAME: root + + # Run test suite with PostgreSQL + - when: + condition: + equal: ["pgsql", <>] + steps: + - run: + name: "Run test suite" + command: vendor/bin/phpunit --debug --verbose + environment: + DB_CONNECTION: pgsql + DB_PORT: 5432 + DB_DATABASE: orion + DB_USERNAME: postgres + DB_PASSWORD: postgres + +workflows: + tests: + jobs: + - test: + name: Tests on PHP << matrix.php-version >> with Laravel << matrix.laravel-version >> and << matrix.database >> + matrix: + parameters: + php-version: ["7.3", "7.4", "8.0"] + laravel-version: [ "5.7.*", "5.8.*", "^6.0", "^7.0", "^8.0" ] + database: ["sqlite", "mysql", "pgsql"] + exclude: + - php-version: "7.4" + laravel-version: "5.7.*" + database: "sqlite" + - php-version: "7.4" + laravel-version: "5.7.*" + database: "mysql" + - php-version: "7.4" + laravel-version: "5.7.*" + database: "pgsql" + - php-version: "8.0" + laravel-version: "5.7.*" + database: "sqlite" + - php-version: "8.0" + laravel-version: "5.7.*" + database: "mysql" + - php-version: "8.0" + laravel-version: "5.7.*" + database: "pgsql" + - php-version: "8.0" + laravel-version: "5.8.*" + database: "sqlite" + - php-version: "8.0" + laravel-version: "5.8.*" + database: "mysql" + - php-version: "8.0" + laravel-version: "5.8.*" + database: "pgsql" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 7d0430bf..00000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,133 +0,0 @@ -name: default - -on: - push: - branches: [ main, next ] - pull_request: - branches: [ main ] - -jobs: - build: - - runs-on: ubuntu-latest - - strategy: - fail-fast: false - matrix: - php-version: [ '7.3', '7.4', '8.0' ] - laravel-version: [ '5.7.*', '5.8.*', '^6.0', '^7.0', '^8.0' ] - database: [ 'sqlite', 'mysql', 'pgsql' ] - exclude: - - php-version: '7.4' - laravel-version: '5.7.*' - database: 'sqlite' - - php-version: '7.4' - laravel-version: '5.7.*' - database: 'mysql' - - php-version: '7.4' - laravel-version: '5.7.*' - database: 'pgsql' - - php-version: '8.0' - laravel-version: '5.7.*' - database: 'sqlite' - - php-version: '8.0' - laravel-version: '5.7.*' - database: 'mysql' - - php-version: '8.0' - laravel-version: '5.7.*' - database: 'pgsql' - - php-version: '8.0' - laravel-version: '5.8.*' - database: 'sqlite' - - php-version: '8.0' - laravel-version: '5.8.*' - database: 'mysql' - - php-version: '8.0' - laravel-version: '5.8.*' - database: 'pgsql' - - name: Tests on PHP ${{ matrix.php-version }} with Laravel ${{ matrix.laravel-version }} and ${{ matrix.database }} - - services: - mysql: - image: mysql:5.7 - env: - MYSQL_ALLOW_EMPTY_PASSWORD: yes - MYSQL_DATABASE: orion - ports: - - 3306:3306 - options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 - - pgsql: - image: postgres:10.8 - env: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DB: orion - ports: - - 5432:5432 - options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 - - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php-version }} - coverage: none - - - name: Validate composer.json and composer.lock - run: composer validate - - - name: Cache Composer packages - id: composer-cache - uses: actions/cache@v2 - with: - path: vendor - key: ${{ runner.os }}-php-${{ matrix.php-version }}-${{ hashFiles('**/composer.lock') }} - restore-keys: | - ${{ runner.os }}-php-${{ matrix.php-version }}- - - - name: Install dependencies - if: steps.composer-cache.outputs.cache-hit != 'true' - run: composer update --with "illuminate/contracts=${{ matrix.laravel-version }}" --prefer-dist --no-progress --no-suggest - - - name: Install laravel/legacy-factories only for Laravel 8.0 - if: matrix.laravel-version == '^8.0' - run: composer require "laravel/legacy-factories" --prefer-dist --no-progress --no-suggest - - - name: Upgrade to PhpUnit 9 for PHP 8 - if: matrix.laravel-version == '^8.0' - run: composer update --with "phpunit/phpunit=^9.0" --prefer-dist --no-progress --no-suggest - - - name: Upgrade PhpUnit Config for PHP 8 - if: matrix.laravel-version == '^8.0' - run: vendor/bin/phpunit -c phpunit.xml.dist --migrate-configuration - - - name: Run test suite with Sqlite - if: matrix.database == 'sqlite' - run: vendor/bin/phpunit --debug - env: - DB_CONNECTION: sqlite - DB_DATABASE: ':memory:' - - - name: Run test suite with MySQL - if: matrix.database == 'mysql' - run: vendor/bin/phpunit --debug - env: - DB_CONNECTION: mysql - DB_PORT: ${{ job.services.mysql.ports[3306] }} - DB_DATABASE: orion - DB_USERNAME: root - - - name: Run test suite with PostgreSQL - if: matrix.database == 'pgsql' - run: vendor/bin/phpunit --debug - env: - DB_CONNECTION: pgsql - DB_PORT: ${{ job.services.pgsql.ports[5432] }} - DB_DATABASE: orion - DB_USERNAME: postgres - DB_PASSWORD: postgres \ No newline at end of file diff --git a/README.md b/README.md index b7eaae3c..2e66e288 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@

-Latest Version on Packagist +Latest Version on Packagist Build Status

@@ -15,6 +15,12 @@ Laravel Orion allows you to build a fully featured REST API based on your Eloque Documentation for Laravel Orion can be found on the [website](https://tailflow.github.io/laravel-orion-docs/). +## Supported By + + + + + ## License The Laravel Orion is open-source software licensed under the [MIT license](https://opensource.org/licenses/MIT). diff --git a/config/orion.php b/config/orion.php index ad94b01b..0e491262 100644 --- a/config/orion.php +++ b/config/orion.php @@ -31,4 +31,7 @@ 'transactions' => [ 'enabled' => false, ], + 'search' => [ + 'case_sensitive' => true, // TODO: set to "false" by default in 3.0 release + ] ]; diff --git a/src/Drivers/Standard/ParamsValidator.php b/src/Drivers/Standard/ParamsValidator.php index edb48e86..6c67954d 100644 --- a/src/Drivers/Standard/ParamsValidator.php +++ b/src/Drivers/Standard/ParamsValidator.php @@ -78,6 +78,7 @@ public function validateSearch(Request $request): void [ 'search' => ['sometimes', 'array'], 'search.value' => ['string', 'nullable'], + 'search.case_sensitive' => ['bool'] ] )->validate(); } diff --git a/src/Drivers/Standard/QueryBuilder.php b/src/Drivers/Standard/QueryBuilder.php index ad113c7f..3dccd573 100644 --- a/src/Drivers/Standard/QueryBuilder.php +++ b/src/Drivers/Standard/QueryBuilder.php @@ -8,6 +8,7 @@ use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Arr; use Orion\Http\Requests\Request; +use RuntimeException; class QueryBuilder implements \Orion\Contracts\QueryBuilder { @@ -214,6 +215,10 @@ protected function buildPivotFilterQueryWhereClause( bool $or = false ) { if (is_array($filterDescriptor['value']) && in_array(null, $filterDescriptor['value'], true)) { + if ((float) app()->version() <= 7.0) { + throw new RuntimeException("Filtering by nullable pivot fields is only supported for Laravel version > 8.0"); + } + $query = $query->{$or ? 'orWherePivotNull' : 'wherePivotNull'}($field); $filterDescriptor['value'] = collect($filterDescriptor['value'])->filter()->values()->toArray(); @@ -288,6 +293,13 @@ public function applySearchingToQuery($query, Request $request): void $query->where( function ($whereQuery) use ($searchables, $requestedSearchDescriptor) { $requestedSearchString = $requestedSearchDescriptor['value']; + + $caseSensitive = (bool) Arr::get( + $requestedSearchDescriptor, + 'case_sensitive', + config('orion.search.case_sensitive') + ); + /** * @var Builder $whereQuery */ @@ -298,10 +310,17 @@ function ($whereQuery) use ($searchables, $requestedSearchDescriptor) { $whereQuery->orWhereHas( $relation, - function ($relationQuery) use ($relationField, $requestedSearchString) { + function ($relationQuery) use ($relationField, $requestedSearchString, $caseSensitive) { /** * @var Builder $relationQuery */ + if (!$caseSensitive) { + return $relationQuery->whereRaw( + "lower({$relationField}) like lower(?)", + ['%' . $requestedSearchString . '%'] + ); + } + return $relationQuery->where( $relationField, 'like', @@ -310,11 +329,20 @@ function ($relationQuery) use ($relationField, $requestedSearchString) { } ); } else { - $whereQuery->orWhere( - $this->getQualifiedFieldName($searchable), - 'like', - '%' . $requestedSearchString . '%' - ); + $qualifiedFieldName = $this->getQualifiedFieldName($searchable); + + if (!$caseSensitive) { + $whereQuery->orWhereRaw( + "lower({$qualifiedFieldName}) like lower(?)", + ['%' . $requestedSearchString . '%'] + ); + } else { + $whereQuery->orWhere( + $qualifiedFieldName, + 'like', + '%' . $requestedSearchString . '%' + ); + } } } } diff --git a/src/Specs/Builders/Partials/RequestBody/Search/SearchBuilder.php b/src/Specs/Builders/Partials/RequestBody/Search/SearchBuilder.php index a8911505..ec82aed3 100644 --- a/src/Specs/Builders/Partials/RequestBody/Search/SearchBuilder.php +++ b/src/Specs/Builders/Partials/RequestBody/Search/SearchBuilder.php @@ -22,6 +22,10 @@ public function build(): ?array 'description' => 'A search for the given value will be performed on the following fields: ' . collect( $this->controller->searchableBy() )->join(', ') + ], + 'case_sensitive' => [ + 'type' => 'boolean', + 'description' => '(default: true) Set it to false to perform search in case-insensitive way' ] ] ]; diff --git a/tests/Feature/Relations/BelongsToMany/BelongsToManyRelationStandardIndexFilteringOperationsTest.php b/tests/Feature/Relations/BelongsToMany/BelongsToManyRelationStandardIndexFilteringOperationsTest.php index a9172801..b93881ed 100644 --- a/tests/Feature/Relations/BelongsToMany/BelongsToManyRelationStandardIndexFilteringOperationsTest.php +++ b/tests/Feature/Relations/BelongsToMany/BelongsToManyRelationStandardIndexFilteringOperationsTest.php @@ -40,8 +40,13 @@ public function getting_a_list_of_relation_resources_filtered_by_pivot_field(): } /** @test */ - public function getting_a_list_of_relation_resources_filtered_by_null_pivot_field_value_using_equality_operator(): void + public function getting_a_list_of_relation_resources_filtered_by_null_pivot_field_value_using_equality_operator( + ): void { + if ((float) app()->version() <= 7.0) { + $this->markTestSkipped('Unsupported framework version'); + } + /** @var User $user */ $user = factory(User::class)->create(); @@ -54,19 +59,27 @@ public function getting_a_list_of_relation_resources_filtered_by_null_pivot_fiel Gate::policy(User::class, GreenPolicy::class); Gate::policy(Role::class, GreenPolicy::class); - $response = $this->post("/api/users/{$user->id}/roles/search", [ + if ((float) app()->version() <= 7.0) { + $this->expectExceptionMessage( + "Filtering by nullable pivot fields is only supported for Laravel version > 8.0" + ); + } + + $response = $this->withoutExceptionHandling()->post("/api/users/{$user->id}/roles/search", [ 'filters' => [ ['field' => 'pivot.custom_name', 'operator' => '=', 'value' => null], ], ]); - $this->assertResourcesPaginated( - $response, - $this->makePaginator( - [$user->roles()->where('roles.id', $roleWithoutCustomName->id)->first()->toArray()], - "users/{$user->id}/roles/search" - ) - ); + if ((float) app()->version() > 7.0) { + $this->assertResourcesPaginated( + $response, + $this->makePaginator( + [$user->roles()->where('roles.id', $roleWithoutCustomName->id)->first()->toArray()], + "users/{$user->id}/roles/search" + ) + ); + } } /** @test */ @@ -84,18 +97,26 @@ public function getting_a_list_of_relation_resources_filtered_by_null_pivot_fiel Gate::policy(User::class, GreenPolicy::class); Gate::policy(Role::class, GreenPolicy::class); - $response = $this->post("/api/users/{$user->id}/roles/search", [ + if ((float) app()->version() <= 7.0) { + $this->expectExceptionMessage( + "Filtering by nullable pivot fields is only supported for Laravel version > 8.0" + ); + } + + $response = $this->withoutExceptionHandling()->post("/api/users/{$user->id}/roles/search", [ 'filters' => [ ['field' => 'pivot.custom_name', 'operator' => 'in', 'value' => [null]], ], ]); - $this->assertResourcesPaginated( - $response, - $this->makePaginator( - [$user->roles()->where('roles.id', $roleWithoutCustomName->id)->first()->toArray()], - "users/{$user->id}/roles/search" - ) - ); + if ((float) app()->version() > 7.0) { + $this->assertResourcesPaginated( + $response, + $this->makePaginator( + [$user->roles()->where('roles.id', $roleWithoutCustomName->id)->first()->toArray()], + "users/{$user->id}/roles/search" + ) + ); + } } } diff --git a/tests/Feature/StandardIndexSearchingOperationsTest.php b/tests/Feature/StandardIndexSearchingOperationsTest.php index 43c09f4e..47066bca 100644 --- a/tests/Feature/StandardIndexSearchingOperationsTest.php +++ b/tests/Feature/StandardIndexSearchingOperationsTest.php @@ -10,7 +10,7 @@ class StandardIndexSearchingOperationsTest extends TestCase { /** @test */ - public function searching_for_resources_by_model_field(): void + public function case_sensitive_search_for_resources_by_model_field(): void { $matchingPost = factory(Post::class)->create(['title' => 'match'])->fresh(); factory(Post::class)->create(['title' => 'different'])->fresh(); @@ -27,6 +27,24 @@ public function searching_for_resources_by_model_field(): void ); } + /** @test */ + public function case_insensitive_search_for_resources_by_model_field(): void + { + $matchingPost = factory(Post::class)->create(['title' => 'mAtCh'])->fresh(); + factory(Post::class)->create(['title' => 'different'])->fresh(); + + Gate::policy(Post::class, GreenPolicy::class); + + $response = $this->post('/api/posts/search', [ + 'search' => ['value' => 'match', 'case_sensitive' => false] + ]); + + $this->assertResourcesPaginated( + $response, + $this->makePaginator([$matchingPost], 'posts/search') + ); + } + /** @test */ public function searching_for_resources_by_relation_field(): void { @@ -48,6 +66,27 @@ public function searching_for_resources_by_relation_field(): void ); } + /** @test */ + public function case_insensitive_search_for_resources_by_relation_field(): void + { + $matchingPostUser = factory(User::class)->create(['name' => 'mAtCh']); + $matchingPost = factory(Post::class)->create(['user_id' => $matchingPostUser->id])->fresh(); + + $nonMatchingPostUser = factory(User::class)->make(['name' => 'not match']); + factory(Post::class)->create(['user_id' => $nonMatchingPostUser->id])->fresh(); + + Gate::policy(Post::class, GreenPolicy::class); + + $response = $this->post('/api/posts/search', [ + 'search' => ['value' => 'match', 'case_sensitive' => false] + ]); + + $this->assertResourcesPaginated( + $response, + $this->makePaginator([$matchingPost], 'posts/search') + ); + } + /** @test */ public function searching_for_resources_with_empty_search_value(): void { diff --git a/tests/Feature/TestCase.php b/tests/Feature/TestCase.php index 8ce2a053..ca33e866 100644 --- a/tests/Feature/TestCase.php +++ b/tests/Feature/TestCase.php @@ -15,6 +15,7 @@ abstract class TestCase extends BaseTestCase protected function setUp(): void { parent::setUp(); + $this->withAuth(); }