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 @@
-
+
@@ -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();
}