From 6cdc2abc13486eae91e96020d0662ea0922a266a Mon Sep 17 00:00:00 2001 From: Rodrigo Quelca Date: Tue, 1 Oct 2024 09:33:24 -0400 Subject: [PATCH 01/10] FOUR-18606: Search text within the list of cases --- ProcessMaker/Filters/V1_1/Filter.php | 244 ++++++++++++++++++ .../Http/Requests/CaseListRequest.php | 2 +- ProcessMaker/Models/CaseStarted.php | 21 +- .../Repositories/CaseApiRepository.php | 39 ++- tests/Feature/Api/V1_1/CaseControllerTest.php | 16 +- 5 files changed, 299 insertions(+), 23 deletions(-) create mode 100644 ProcessMaker/Filters/V1_1/Filter.php diff --git a/ProcessMaker/Filters/V1_1/Filter.php b/ProcessMaker/Filters/V1_1/Filter.php new file mode 100644 index 0000000000..644d55f2aa --- /dev/null +++ b/ProcessMaker/Filters/V1_1/Filter.php @@ -0,0 +1,244 @@ +', + '<', + '>=', + '<=', + 'between', + 'in', + 'contains', + ]; + + public function __construct($definition) + { + $this->subjectType = $definition['subject']['type']; + $this->subjectValue = Arr::get($definition, 'subject.value'); + $this->operator = $definition['operator']; + $this->value = $definition['value']; + $this->or = Arr::get($definition, 'or', []); + + $this->detectRawValue(); + } + + public static function filter(Builder $query, string|array $filterDefinitions): void + { + if (is_string($filterDefinitions)) { + $filterDefinitions = json_decode($filterDefinitions, true); + } + + if (!$filterDefinitions) { + return; + } + + $query->where(function ($query) use ($filterDefinitions) { + foreach ($filterDefinitions as $filter) { + (new self($filter))->addToQuery($query); + } + }); + } + + public function addToQuery($query): void + { + if (!empty($this->or)) { + $query->where(fn ($query) => $this->apply($query)); + } else { + $this->apply($query); + } + } + + private function apply($query): void + { + if ($valueAliasMethod = $this->valueAliasMethod()) { + $this->valueAliasAdapter($valueAliasMethod, $query); + } else { + $this->applyQueryBuilderMethod($query); + } + + if (!empty($this->or)) { + $query->orWhere(function ($orQuery) { + foreach ($this->or as $or) { + (new self($or))->addToQuery($orQuery); + } + }); + } + } + + private function applyQueryBuilderMethod($query) + { + $method = $this->method(); + + if (in_array($method, ['whereIn', 'whereBetween', 'whereJsonContains'])) { + $query->$method( + $this->subject(), + $this->value(), + ); + } elseif ($this->isJsonData()) { + $this->manuallyAddJsonWhere($query); + } else { + $query->$method( + $this->subject(), + $this->operator(), + $this->value(), + ); + } + } + + private function manuallyAddJsonWhere($query): void + { + $parts = explode('.', $this->subjectValue); + + array_shift($parts); + + $selector = implode('"."', $parts); + $operator = $this->operator(); + $value = $this->value(); + + if (!is_numeric($value)) { + $value = DB::connection()->getPdo()->quote($value); + } + + if ($operator === 'like') { + // For JSON data is required to do a CAST in order to make insensitive the comparison + $query->whereRaw( + "cast(json_unquote(json_extract(`data`, '$.\"{$selector}\"')) as CHAR) {$operator} {$value}" + ); + } else { + $query->whereRaw("json_unquote(json_extract(`data`, '$.\"{$selector}\"')) {$operator} {$value}"); + } + } + + private function operator() + { + if (!in_array($this->operator, $this->operatorWhitelist)) { + abort(422, "Invalid operator: {$this->operator}"); + } + + return $this->operator; + } + + private function method() + { + switch($this->operator) { + case 'in': + $method = 'whereIn'; + if ($this->isJsonData()) { + $method = 'whereJsonContains'; + } + break; + case 'between': + $method = 'whereBetween'; + break; + default: + $method = 'where'; + } + + return $method; + } + + private function isJsonData() + { + return $this->subjectType === self::TYPE_FIELD && str_starts_with($this->subjectValue, 'data.'); + } + + private function subject() + { + if ($this->isJsonData()) { + return str_replace('.', '->', $this->subjectValue); + } + + return $this->subjectValue; + } + + public function value() + { + if ($this->operator === 'contains') { + return '%' . $this->value . '%'; + } + + if ($this->operator === 'starts_with') { + return $this->value . '%'; + } + + if ($this->filteringWithRawValue()) { + return $this->getRawValue(); + } + + return $this->value; + } + + private function valueAliasMethod() + { + $method = null; + + switch ($this->subjectType) { + case self::TYPE_STATUS: + $method = 'valueAliasStatus'; + break; + } + + return $method; + } + + private function valueAliasAdapter(string $method, Builder $query): void + { + $operator = $this->operator(); + + if ($operator === 'in') { + $operator = '='; + } + + $values = (array) $this->value(); + $expression = (object) ['operator' => $operator]; + $model = $query->getModel(); + + if ($method === 'valueAliasParticipant') { + $values = $this->convertUserIdsToUsernames($values); + } + + foreach ($values as $i => $value) { + if ($i === 0) { + $query->where($model->$method($value, $expression)); + } else { + $query->orWhere($model->$method($value, $expression)); + } + } + } + + private function convertUserIdsToUsernames($values) + { + return array_map(function ($value) { + $username = User::find($value)?->username; + + return isset($username) ? $username : $value; + }, $values); + } +} diff --git a/ProcessMaker/Http/Requests/CaseListRequest.php b/ProcessMaker/Http/Requests/CaseListRequest.php index c55e944406..51708ecf9c 100644 --- a/ProcessMaker/Http/Requests/CaseListRequest.php +++ b/ProcessMaker/Http/Requests/CaseListRequest.php @@ -26,7 +26,7 @@ public function rules(): array 'userId' => 'sometimes|integer', 'status' => 'sometimes|in:IN_PROGRESS,COMPLETED', 'sortBy' => ['sometimes', 'string', new SortBy], - 'filterBy' => 'sometimes|array', + 'filterBy' => 'sometimes|json', 'search' => 'sometimes|string', 'pageSize' => 'sometimes|integer|min:1', 'page' => 'sometimes|integer|min:1', diff --git a/ProcessMaker/Models/CaseStarted.php b/ProcessMaker/Models/CaseStarted.php index 3840551816..dedc8c3177 100644 --- a/ProcessMaker/Models/CaseStarted.php +++ b/ProcessMaker/Models/CaseStarted.php @@ -3,9 +3,9 @@ namespace ProcessMaker\Models; use Database\Factories\CaseStartedFactory; -use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Casts\AsCollection; use Illuminate\Database\Eloquent\Factories\Factory; +use Illuminate\Database\Eloquent\Factories\HasFactory; use ProcessMaker\Models\ProcessMakerModel; class CaseStarted extends ProcessMakerModel @@ -59,4 +59,23 @@ public function user() { return $this->belongsTo(User::class); } + + public function valueAliasStatus($value, $expression) + { + $statusMap = [ + 'in progress' => 'IN_PROGRESS', + 'completed' => 'COMPLETED', + 'error' => 'ERROR', + 'canceled' => 'CANCELED', + ]; + + $value = mb_strtolower($value); + + return function ($query) use ($value, $statusMap, $expression) { + if (array_key_exists($value, $statusMap)) { + $value = $statusMap[$value]; + } + $query->where('case_status', $expression->operator, $value); + }; + } } diff --git a/ProcessMaker/Repositories/CaseApiRepository.php b/ProcessMaker/Repositories/CaseApiRepository.php index ccc6b14b92..658899993c 100644 --- a/ProcessMaker/Repositories/CaseApiRepository.php +++ b/ProcessMaker/Repositories/CaseApiRepository.php @@ -6,6 +6,7 @@ use Illuminate\Http\Request; use ProcessMaker\Contracts\CaseApiRepositoryInterface; use ProcessMaker\Exception\CaseValidationException; +use ProcessMaker\Filters\V1_1\Filter; use ProcessMaker\Models\CaseParticipated; use ProcessMaker\Models\CaseStarted; @@ -73,6 +74,7 @@ public function getAllCases(Request $request): Builder { $query = CaseStarted::select($this->defaultFields); $this->applyFilters($request, $query); + return $query; } @@ -88,6 +90,7 @@ public function getInProgressCases(Request $request): Builder $query = CaseParticipated::select($this->defaultFields) ->where('case_status', 'IN_PROGRESS'); $this->applyFilters($request, $query); + return $query; } @@ -103,6 +106,7 @@ public function getCompletedCases(Request $request): Builder $query = CaseParticipated::select($this->defaultFields) ->where('case_status', 'COMPLETED'); $this->applyFilters($request, $query); + return $query; } @@ -165,22 +169,31 @@ public function search(Request $request, Builder $query): void */ public function filterBy(Request $request, Builder $query): void { - if ($request->has('filterBy')) { - $filterByValue = $request->get('filterBy'); + // Check if filterBy exists in the request + if (!$request->has('filterBy')) { + return; + } - foreach ($filterByValue as $key => $value) { - if (!in_array($key, $this->filterableFields)) { - throw new CaseValidationException("Filter by field $key is not allowed."); - } + // Get the filter input and apply the filter only if it's not empty + $filters = $request->input('filterBy', ''); + if (empty($filters)) { + return; + } - if (in_array($key, $this->dateFields)) { - $query->whereDate($key, $value); - continue; - } + // Apply the filters to the query + $this->executeFilters($query, $filters); + } - $query->where($key, $value); - } - } + /** + * Execute advanced filters to the query. + * + * @param Builder $query + * @param array|string $filters + * @return void + */ + protected function executeFilters(Builder $query, $filters): void + { + Filter::filter($query, $filters); } /** diff --git a/tests/Feature/Api/V1_1/CaseControllerTest.php b/tests/Feature/Api/V1_1/CaseControllerTest.php index 31333a9316..9f3fa3577c 100644 --- a/tests/Feature/Api/V1_1/CaseControllerTest.php +++ b/tests/Feature/Api/V1_1/CaseControllerTest.php @@ -208,13 +208,14 @@ public function test_get_all_cases_filter_by(): void $response = $this->apiCall('GET', route('api.1.1.cases.all_cases')); $response->assertStatus(200); $response->assertJsonCount(6, 'data'); - - $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', ['filterBy' => ['case_number' => $caseNumber]])); + $filterBy = ['filterBy' =>'[{"subject":{"type":"Field","value":"case_number"},"operator":"=","value":"' . $caseNumber . '"}]']; + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', $filterBy)); $response->assertStatus(200); $response->assertJsonCount(1, 'data'); $response->assertJsonFragment(['case_number' => $caseNumber]); - $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', ['filterBy' => ['case_status' => 'IN_PROGRESS']])); + $filterBy = ['filterBy' =>'[{"subject":{"type":"Field","value":"case_status"},"operator":"=","value":"IN_PROGRESS"}]']; + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', $filterBy)); $response->assertStatus(200); $total = $casesA->where('case_status', 'IN_PROGRESS')->count() + $casesB->where('case_status', 'IN_PROGRESS')->count(); $response->assertJsonCount($total, 'data'); @@ -225,13 +226,12 @@ public function test_get_all_cases_filter_by(): void public function test_get_all_cases_filter_by_invalid_field(): void { $invalidField = 'invalid_field'; - - $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', ['filterBy' => ''])); + $filterBy = ['filterBy' =>'']; + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', $filterBy)); $response->assertStatus(422); - $response->assertJsonPath('message', 'The Filter by field must be an array.'); - + $response->assertJsonPath('message', 'The Filter by field must be a valid JSON string.'); $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', ['filterBy' => [$invalidField => 'value']])); $response->assertStatus(422); - $response->assertJsonPath('message', "Filter by field $invalidField is not allowed."); + $response->assertJsonPath('message', 'The Filter by field must be a valid JSON string.'); } } From ede7566810dcda4c30b41ade7d102f01e277bd66 Mon Sep 17 00:00:00 2001 From: Rodrigo Quelca Date: Tue, 1 Oct 2024 11:34:49 -0400 Subject: [PATCH 02/10] second stage tests --- tests/Feature/Api/V1_1/CaseControllerTest.php | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/Feature/Api/V1_1/CaseControllerTest.php b/tests/Feature/Api/V1_1/CaseControllerTest.php index 9f3fa3577c..78689c540f 100644 --- a/tests/Feature/Api/V1_1/CaseControllerTest.php +++ b/tests/Feature/Api/V1_1/CaseControllerTest.php @@ -155,6 +155,7 @@ public function test_get_all_cases_sort_by_case_number(): void { $userA = $this->createUser('user_a'); $cases = $this->createCasesStartedForUser($userA->id, 10); + $casesSorted = $cases->sortBy('case_number'); $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', ['sortBy' => 'case_number:asc'])); @@ -204,6 +205,7 @@ public function test_get_all_cases_filter_by(): void $casesA = $this->createCasesStartedForUser($userA->id, 5); $caseNumber = 123456; $casesB = $this->createCasesStartedForUser($userA->id, 1, ['case_number' => $caseNumber, 'case_status' => 'IN_PROGRESS']); + $initiatedAt = $casesA->first()->initiated_at->format('Y-m-d'); $response = $this->apiCall('GET', route('api.1.1.cases.all_cases')); $response->assertStatus(200); @@ -221,6 +223,28 @@ public function test_get_all_cases_filter_by(): void $response->assertJsonCount($total, 'data'); $response->assertJsonFragment(['case_status' => 'IN_PROGRESS']); $response->assertJsonMissing(['case_status' => 'COMPLETED']); + + $filterBy = ['filterBy' =>'[{"subject":{"type":"Field","value":"user_id"},"operator":"=","value":"' . $userA->id . '"},{"subject":{"type":"Field","value":"case_status"},"operator":"=","value":"IN_PROGRESS"}]']; + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', $filterBy)); + $response->assertStatus(200); + $total = $casesA->where('user_id', $userA->id)->where('case_status', 'IN_PROGRESS')->count() + $casesB->where('user_id', $userA->id)->where('case_status', 'IN_PROGRESS')->count(); + $response->assertJsonCount($total, 'data'); + $response->assertJsonFragment(['case_status' => 'IN_PROGRESS']); + $response->assertJsonFragment(['user_id' => $userA->id]); + + $filterBy = ['filterBy' =>'[{"subject":{"type":"Field","value":"user_id"},"operator":"=","value":"' . $userA->id . '"},{"subject":{"type":"Field","value":"case_status"},"operator":"=","value":"IN_PROGRESS"}, {"subject":{"type":"Field","value":"created_at"},"operator":">","value":"2023-02-10"}]']; + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', $filterBy)); + $response->assertStatus(200); + $total = $casesA->where('user_id', $userA->id)->where('case_status', 'IN_PROGRESS')->where('created_at', '>', '2023-02-10')->count() + $casesB->where('user_id', $userA->id)->where('case_status', 'IN_PROGRESS')->where('created_at', '>', '2023-02-10')->count(); + $response->assertJsonCount($total, 'data'); + $response->assertJsonFragment(['case_status' => 'IN_PROGRESS']); + $response->assertJsonFragment(['user_id' => $userA->id]); + + $filterBy = ['filterBy' =>'[{"subject":{"type":"Field","value":"user_id"},"operator":"=","value":"' . $userA->id . '"},{"subject":{"type":"Field","value":"case_status"},"operator":"=","value":"IN_PROGRESS"}, {"subject":{"type":"Field","value":"created_at"},"operator":">","value":"2023-02-10"}, {"subject":{"type":"Field","value":"completed_at"},"operator":">","value":"2023-04-01"}]']; + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', $filterBy)); + $response->assertStatus(200); + $total = $casesA->where('user_id', $userA->id)->where('case_status', 'IN_PROGRESS')->where('created_at', '>', '2023-02-10')->where('completed_at', '>', '2023-04-01')->count() + $casesB->where('user_id', $userA->id)->where('case_status', 'IN_PROGRESS')->where('created_at', '>', '2023-02-10')->where('completed_at', '>', '2023-04-01')->count(); + $response->assertJsonCount($total, 'data'); } public function test_get_all_cases_filter_by_invalid_field(): void From c2a42dd4a907aa69997f2e1210d724b422d47cf9 Mon Sep 17 00:00:00 2001 From: Rodrigo Quelca Date: Tue, 1 Oct 2024 15:29:35 -0400 Subject: [PATCH 03/10] remofe the V1_1 filter and use the current filter --- ProcessMaker/Filters/Filter.php | 303 +----------------- ProcessMaker/Filters/V1_1/Filter.php | 244 -------------- .../Repositories/CaseApiRepository.php | 4 +- 3 files changed, 4 insertions(+), 547 deletions(-) delete mode 100644 ProcessMaker/Filters/V1_1/Filter.php diff --git a/ProcessMaker/Filters/Filter.php b/ProcessMaker/Filters/Filter.php index 528995037b..843a468549 100644 --- a/ProcessMaker/Filters/Filter.php +++ b/ProcessMaker/Filters/Filter.php @@ -2,260 +2,23 @@ namespace ProcessMaker\Filters; -use Illuminate\Support\Facades\DB; use Illuminate\Database\Eloquent\Builder; -use Illuminate\Support\Arr; -use ProcessMaker\Models\ProcessRequestToken; -use ProcessMaker\Models\User; -use ProcessMaker\Traits\InteractsWithRawFilter; -class Filter +class Filter extends BaseFilter { - use InteractsWithRawFilter; - public const TYPE_PARTICIPANTS = 'Participants'; - public const TYPE_PARTICIPANTS_FULLNAME = 'ParticipantsFullName'; - - public const TYPE_ASSIGNEES_FULLNAME = 'AssigneesFullName'; - - public const TYPE_STATUS = 'Status'; - - public const TYPE_ALTERNATIVE = 'Alternative'; - - public const TYPE_FIELD = 'Field'; - public const TYPE_PROCESS = 'Process'; public const TYPE_RELATIONSHIP = 'Relationship'; - public string|null $subjectValue; - - public string $subjectType; - - public string $operator; - - public $value; - - public array $or; - - public array $operatorWhitelist = [ - '=', - '!=', - '>', - '<', - '>=', - '<=', - 'between', - 'in', - 'contains', - 'regex', - 'starts_with', - ]; - - public function __construct($definition) - { - $this->subjectType = $definition['subject']['type']; - $this->subjectValue = Arr::get($definition, 'subject.value'); - $this->operator = $definition['operator']; - $this->value = $definition['value']; - $this->or = Arr::get($definition, 'or', []); - - $this->detectRawValue(); - } - - public static function filter(Builder $query, string|array $filterDefinitions): void - { - if (is_string($filterDefinitions)) { - $filterDefinitions = json_decode($filterDefinitions, true); - } - - if (!$filterDefinitions) { - return; - } - - $query->where(function ($query) use ($filterDefinitions) { - foreach ($filterDefinitions as $filter) { - (new self($filter))->addToQuery($query); - } - }); - } - - public function addToQuery(Builder $query): void - { - if (!empty($this->or)) { - $query->where(fn ($query) => $this->apply($query)); - } else { - $this->apply($query); - } - } - - private function apply($query): void - { - if ($valueAliasMethod = $this->valueAliasMethod()) { - $this->valueAliasAdapter($valueAliasMethod, $query); - } elseif ($this->subjectType === self::TYPE_PROCESS) { - $this->filterByProcessId($query); - } elseif ($this->subjectType === self::TYPE_RELATIONSHIP) { - $this->filterByRelationship($query); - } elseif ($this->isJsonData() && $query->getModel() instanceof ProcessRequestToken) { - $this->filterByRequestData($query); - } else { - $this->applyQueryBuilderMethod($query); - } - - if (!empty($this->or)) { - $query->orWhere(function ($orQuery) { - foreach ($this->or as $or) { - (new self($or))->addToQuery($orQuery); - } - }); - } - } - - private function applyQueryBuilderMethod($query) - { - $method = $this->method(); - - if (in_array($method, ['whereIn', 'whereBetween', 'whereJsonContains'])) { - $query->$method( - $this->subject(), - $this->value(), - ); - } elseif ($this->isJsonData()) { - $this->manuallyAddJsonWhere($query); - } else { - $query->$method( - $this->subject(), - $this->operator(), - $this->value(), - ); - } - } - - /** - * We must do this manually because Laravel bindings cast - * floats/doubles to strings and that wont work to compare - * json values - * - * @param [type] $query - * @return void - */ - private function manuallyAddJsonWhere($query): void - { - $parts = explode('.', $this->subjectValue); - - array_shift($parts); - - $selector = implode('"."', $parts); - $operator = $this->operator(); - $value = $this->value(); - - if (!is_numeric($value)) { - $value = DB::connection()->getPdo()->quote($value); - } - - if ($operator === 'like') { - // For JSON data is required to do a CAST in order to make insensitive the comparison - $query->whereRaw( - "cast(json_unquote(json_extract(`data`, '$.\"{$selector}\"')) as CHAR) {$operator} {$value}" - ); - } else { - $query->whereRaw("json_unquote(json_extract(`data`, '$.\"{$selector}\"')) {$operator} {$value}"); - } - } - - private function operator() - { - if (!in_array($this->operator, $this->operatorWhitelist)) { - abort(422, "Invalid operator: {$this->operator}"); - } - - if ($this->operator === 'contains' || $this->operator === 'starts_with') { - return 'like'; - } - - if ($this->operator === 'regex') { - $this->operator = 'REGEXP'; - } - - return $this->operator; - } - - private function method() - { - switch($this->operator) { - case 'in': - $method = 'whereIn'; - if ($this->isJsonData()) { - $method = 'whereJsonContains'; - } - break; - case 'between': - $method = 'whereBetween'; - break; - default: - $method = 'where'; - } - - return $method; - } - - private function isJsonData() - { - return $this->subjectType === self::TYPE_FIELD && str_starts_with($this->subjectValue, 'data.'); - } - - private function subject() - { - if ($this->isJsonData()) { - return str_replace('.', '->', $this->subjectValue); - } - - if ($this->subjectType === self::TYPE_PARTICIPANTS) { - return 'user_id'; - } - - if ($this->subjectType === self::TYPE_PROCESS) { - return 'process_id'; - } - - if ($this->subjectType === self::TYPE_RELATIONSHIP) { - return $this->relationshipSubjectTypeParts()[1]; - } - - return $this->subjectValue; - } - - private function relationshipSubjectTypeParts(): array - { - return explode('.', $this->subjectValue); - } - - public function value() - { - if ($this->operator === 'contains') { - return '%' . $this->value . '%'; - } - - if ($this->operator === 'starts_with') { - return $this->value . '%'; - } - - if ($this->filteringWithRawValue()) { - return $this->getRawValue(); - } - - return $this->value; - } - /** * Forward Status and Participant subjects to PMQL methods on the models. * * For now, we only need Participants and Status because Request and Requester * are columns on the tables (process_request_id and user_id). */ - private function valueAliasMethod() + protected function valueAliasMethod() { $method = null; @@ -279,66 +42,4 @@ private function valueAliasMethod() return $method; } - - private function valueAliasAdapter(string $method, Builder $query): void - { - $operator = $this->operator(); - - if ($operator === 'in') { - $operator = '='; - } - - $values = (array) $this->value(); - $expression = (object) ['operator' => $operator]; - $model = $query->getModel(); - - if ($method === 'valueAliasParticipant') { - $values = $this->convertUserIdsToUsernames($values); - } - - foreach ($values as $i => $value) { - if ($i === 0) { - $query->where($model->$method($value, $expression)); - } else { - $query->orWhere($model->$method($value, $expression)); - } - } - } - - private function convertUserIdsToUsernames($values) - { - return array_map(function ($value) { - $username = User::find($value)?->username; - - return isset($username) ? $username : $value; - }, $values); - } - - private function filterByProcessId(Builder $query): void - { - if ($query->getModel() instanceof ProcessRequestToken) { - $query->whereIn('process_request_id', function ($query) { - $query->select('id') - ->from('process_requests') - ->whereIn('process_id', (array) $this->value()); - }); - } else { - $this->applyQueryBuilderMethod($query); - } - } - - private function filterByRelationship(Builder $query): void - { - $relationshipName = $this->relationshipSubjectTypeParts()[0]; - $query->whereHas($relationshipName, function ($rel) { - $this->applyQueryBuilderMethod($rel); - }); - } - - private function filterByRequestData(Builder $query): void - { - $query->whereHas('processRequest', function ($rel) { - $this->applyQueryBuilderMethod($rel); - }); - } } diff --git a/ProcessMaker/Filters/V1_1/Filter.php b/ProcessMaker/Filters/V1_1/Filter.php deleted file mode 100644 index 644d55f2aa..0000000000 --- a/ProcessMaker/Filters/V1_1/Filter.php +++ /dev/null @@ -1,244 +0,0 @@ -', - '<', - '>=', - '<=', - 'between', - 'in', - 'contains', - ]; - - public function __construct($definition) - { - $this->subjectType = $definition['subject']['type']; - $this->subjectValue = Arr::get($definition, 'subject.value'); - $this->operator = $definition['operator']; - $this->value = $definition['value']; - $this->or = Arr::get($definition, 'or', []); - - $this->detectRawValue(); - } - - public static function filter(Builder $query, string|array $filterDefinitions): void - { - if (is_string($filterDefinitions)) { - $filterDefinitions = json_decode($filterDefinitions, true); - } - - if (!$filterDefinitions) { - return; - } - - $query->where(function ($query) use ($filterDefinitions) { - foreach ($filterDefinitions as $filter) { - (new self($filter))->addToQuery($query); - } - }); - } - - public function addToQuery($query): void - { - if (!empty($this->or)) { - $query->where(fn ($query) => $this->apply($query)); - } else { - $this->apply($query); - } - } - - private function apply($query): void - { - if ($valueAliasMethod = $this->valueAliasMethod()) { - $this->valueAliasAdapter($valueAliasMethod, $query); - } else { - $this->applyQueryBuilderMethod($query); - } - - if (!empty($this->or)) { - $query->orWhere(function ($orQuery) { - foreach ($this->or as $or) { - (new self($or))->addToQuery($orQuery); - } - }); - } - } - - private function applyQueryBuilderMethod($query) - { - $method = $this->method(); - - if (in_array($method, ['whereIn', 'whereBetween', 'whereJsonContains'])) { - $query->$method( - $this->subject(), - $this->value(), - ); - } elseif ($this->isJsonData()) { - $this->manuallyAddJsonWhere($query); - } else { - $query->$method( - $this->subject(), - $this->operator(), - $this->value(), - ); - } - } - - private function manuallyAddJsonWhere($query): void - { - $parts = explode('.', $this->subjectValue); - - array_shift($parts); - - $selector = implode('"."', $parts); - $operator = $this->operator(); - $value = $this->value(); - - if (!is_numeric($value)) { - $value = DB::connection()->getPdo()->quote($value); - } - - if ($operator === 'like') { - // For JSON data is required to do a CAST in order to make insensitive the comparison - $query->whereRaw( - "cast(json_unquote(json_extract(`data`, '$.\"{$selector}\"')) as CHAR) {$operator} {$value}" - ); - } else { - $query->whereRaw("json_unquote(json_extract(`data`, '$.\"{$selector}\"')) {$operator} {$value}"); - } - } - - private function operator() - { - if (!in_array($this->operator, $this->operatorWhitelist)) { - abort(422, "Invalid operator: {$this->operator}"); - } - - return $this->operator; - } - - private function method() - { - switch($this->operator) { - case 'in': - $method = 'whereIn'; - if ($this->isJsonData()) { - $method = 'whereJsonContains'; - } - break; - case 'between': - $method = 'whereBetween'; - break; - default: - $method = 'where'; - } - - return $method; - } - - private function isJsonData() - { - return $this->subjectType === self::TYPE_FIELD && str_starts_with($this->subjectValue, 'data.'); - } - - private function subject() - { - if ($this->isJsonData()) { - return str_replace('.', '->', $this->subjectValue); - } - - return $this->subjectValue; - } - - public function value() - { - if ($this->operator === 'contains') { - return '%' . $this->value . '%'; - } - - if ($this->operator === 'starts_with') { - return $this->value . '%'; - } - - if ($this->filteringWithRawValue()) { - return $this->getRawValue(); - } - - return $this->value; - } - - private function valueAliasMethod() - { - $method = null; - - switch ($this->subjectType) { - case self::TYPE_STATUS: - $method = 'valueAliasStatus'; - break; - } - - return $method; - } - - private function valueAliasAdapter(string $method, Builder $query): void - { - $operator = $this->operator(); - - if ($operator === 'in') { - $operator = '='; - } - - $values = (array) $this->value(); - $expression = (object) ['operator' => $operator]; - $model = $query->getModel(); - - if ($method === 'valueAliasParticipant') { - $values = $this->convertUserIdsToUsernames($values); - } - - foreach ($values as $i => $value) { - if ($i === 0) { - $query->where($model->$method($value, $expression)); - } else { - $query->orWhere($model->$method($value, $expression)); - } - } - } - - private function convertUserIdsToUsernames($values) - { - return array_map(function ($value) { - $username = User::find($value)?->username; - - return isset($username) ? $username : $value; - }, $values); - } -} diff --git a/ProcessMaker/Repositories/CaseApiRepository.php b/ProcessMaker/Repositories/CaseApiRepository.php index 658899993c..316183f7c6 100644 --- a/ProcessMaker/Repositories/CaseApiRepository.php +++ b/ProcessMaker/Repositories/CaseApiRepository.php @@ -6,7 +6,7 @@ use Illuminate\Http\Request; use ProcessMaker\Contracts\CaseApiRepositoryInterface; use ProcessMaker\Exception\CaseValidationException; -use ProcessMaker\Filters\V1_1\Filter; +use ProcessMaker\Filters\CasesFilter; use ProcessMaker\Models\CaseParticipated; use ProcessMaker\Models\CaseStarted; @@ -193,7 +193,7 @@ public function filterBy(Request $request, Builder $query): void */ protected function executeFilters(Builder $query, $filters): void { - Filter::filter($query, $filters); + CasesFilter::filter($query, $filters); } /** From d306b6a1dc8983b6a432f7a58e9b30b73b452da0 Mon Sep 17 00:00:00 2001 From: Rodrigo Quelca Date: Tue, 1 Oct 2024 15:31:50 -0400 Subject: [PATCH 04/10] add missing files --- ProcessMaker/Filters/BaseFilter.php | 315 +++++++++++++++++++++++++++ ProcessMaker/Filters/CasesFilter.php | 17 ++ 2 files changed, 332 insertions(+) create mode 100644 ProcessMaker/Filters/BaseFilter.php create mode 100644 ProcessMaker/Filters/CasesFilter.php diff --git a/ProcessMaker/Filters/BaseFilter.php b/ProcessMaker/Filters/BaseFilter.php new file mode 100644 index 0000000000..966ba9b76b --- /dev/null +++ b/ProcessMaker/Filters/BaseFilter.php @@ -0,0 +1,315 @@ +', + '<', + '>=', + '<=', + 'between', + 'in', + 'contains', + 'regex', + 'starts_with', + ]; + + public function __construct($definition) + { + $this->subjectType = $definition['subject']['type']; + $this->subjectValue = Arr::get($definition, 'subject.value'); + $this->operator = $definition['operator']; + $this->value = $definition['value']; + $this->or = Arr::get($definition, 'or', []); + + $this->detectRawValue(); + } + + public static function filter(Builder $query, string|array $filterDefinitions): void + { + if (is_string($filterDefinitions)) { + $filterDefinitions = json_decode($filterDefinitions, true); + } + + if (!$filterDefinitions) { + return; + } + + $query->where(function ($query) use ($filterDefinitions) { + foreach ($filterDefinitions as $filter) { + (new static($filter))->addToQuery($query); + } + }); + } + + public function addToQuery(Builder $query): void + { + if (!empty($this->or)) { + $query->where(fn ($query) => $this->apply($query)); + } else { + $this->apply($query); + } + } + + private function apply($query): void + { + if ($valueAliasMethod = $this->valueAliasMethod()) { + $this->valueAliasAdapter($valueAliasMethod, $query); + } elseif ($this->subjectType === self::TYPE_PROCESS) { + $this->filterByProcessId($query); + } elseif ($this->subjectType === self::TYPE_RELATIONSHIP) { + $this->filterByRelationship($query); + } elseif ($this->isJsonData() && $query->getModel() instanceof ProcessRequestToken) { + $this->filterByRequestData($query); + } else { + $this->applyQueryBuilderMethod($query); + } + + if (!empty($this->or)) { + $query->orWhere(function ($orQuery) { + foreach ($this->or as $or) { + (new static($or))->addToQuery($orQuery); + } + }); + } + } + + private function applyQueryBuilderMethod($query) + { + $method = $this->method(); + + if (in_array($method, ['whereIn', 'whereBetween', 'whereJsonContains'])) { + $query->$method( + $this->subject(), + $this->value(), + ); + } elseif ($this->isJsonData()) { + $this->manuallyAddJsonWhere($query); + } else { + $query->$method( + $this->subject(), + $this->operator(), + $this->value(), + ); + } + } + + /** + * We must do this manually because Laravel bindings cast + * floats/doubles to strings and that wont work to compare + * json values + * + * @param [type] $query + * @return void + */ + private function manuallyAddJsonWhere($query): void + { + $parts = explode('.', $this->subjectValue); + + array_shift($parts); + + $selector = implode('"."', $parts); + $operator = $this->operator(); + $value = $this->value(); + + if (!is_numeric($value)) { + $value = DB::connection()->getPdo()->quote($value); + } + + if ($operator === 'like') { + // For JSON data is required to do a CAST in order to make insensitive the comparison + $query->whereRaw( + "cast(json_unquote(json_extract(`data`, '$.\"{$selector}\"')) as CHAR) {$operator} {$value}" + ); + } else { + $query->whereRaw("json_unquote(json_extract(`data`, '$.\"{$selector}\"')) {$operator} {$value}"); + } + } + + private function operator() + { + if (!in_array($this->operator, $this->operatorWhitelist)) { + abort(422, "Invalid operator: {$this->operator}"); + } + + if ($this->operator === 'contains' || $this->operator === 'starts_with') { + return 'like'; + } + + if ($this->operator === 'regex') { + $this->operator = 'REGEXP'; + } + + return $this->operator; + } + + private function method() + { + switch($this->operator) { + case 'in': + $method = 'whereIn'; + if ($this->isJsonData()) { + $method = 'whereJsonContains'; + } + break; + case 'between': + $method = 'whereBetween'; + break; + default: + $method = 'where'; + } + + return $method; + } + + private function isJsonData() + { + return $this->subjectType === self::TYPE_FIELD && str_starts_with($this->subjectValue, 'data.'); + } + + private function subject() + { + if ($this->isJsonData()) { + return str_replace('.', '->', $this->subjectValue); + } + + if ($this->subjectType === self::TYPE_PARTICIPANTS) { + return 'user_id'; + } + + if ($this->subjectType === self::TYPE_PROCESS) { + return 'process_id'; + } + + if ($this->subjectType === self::TYPE_RELATIONSHIP) { + return $this->relationshipSubjectTypeParts()[1]; + } + + return $this->subjectValue; + } + + private function relationshipSubjectTypeParts(): array + { + return explode('.', $this->subjectValue); + } + + public function value() + { + if ($this->operator === 'contains') { + return '%' . $this->value . '%'; + } + + if ($this->operator === 'starts_with') { + return $this->value . '%'; + } + + if ($this->filteringWithRawValue()) { + return $this->getRawValue(); + } + + return $this->value; + } + + abstract protected function valueAliasMethod(); + + private function valueAliasAdapter(string $method, Builder $query): void + { + $operator = $this->operator(); + + if ($operator === 'in') { + $operator = '='; + } + + $values = (array) $this->value(); + $expression = (object) ['operator' => $operator]; + $model = $query->getModel(); + + if ($method === 'valueAliasParticipant') { + $values = $this->convertUserIdsToUsernames($values); + } + + foreach ($values as $i => $value) { + if ($i === 0) { + $query->where($model->$method($value, $expression)); + } else { + $query->orWhere($model->$method($value, $expression)); + } + } + } + + private function convertUserIdsToUsernames($values) + { + return array_map(function ($value) { + $username = User::find($value)?->username; + + return isset($username) ? $username : $value; + }, $values); + } + + private function filterByProcessId(Builder $query): void + { + if ($query->getModel() instanceof ProcessRequestToken) { + $query->whereIn('process_request_id', function ($query) { + $query->select('id') + ->from('process_requests') + ->whereIn('process_id', (array) $this->value()); + }); + } else { + $this->applyQueryBuilderMethod($query); + } + } + + private function filterByRelationship(Builder $query): void + { + $relationshipName = $this->relationshipSubjectTypeParts()[0]; + $query->whereHas($relationshipName, function ($rel) { + $this->applyQueryBuilderMethod($rel); + }); + } + + private function filterByRequestData(Builder $query): void + { + $query->whereHas('processRequest', function ($rel) { + $this->applyQueryBuilderMethod($rel); + }); + } +} diff --git a/ProcessMaker/Filters/CasesFilter.php b/ProcessMaker/Filters/CasesFilter.php new file mode 100644 index 0000000000..66982a9950 --- /dev/null +++ b/ProcessMaker/Filters/CasesFilter.php @@ -0,0 +1,17 @@ +subjectType === self::TYPE_STATUS) { + return 'valueAliasStatus'; + } + + return null; + } +} From e3d2225da3690407b6835ed121cbadbe87f6566d Mon Sep 17 00:00:00 2001 From: Rodrigo Quelca Date: Wed, 2 Oct 2024 09:32:39 -0400 Subject: [PATCH 05/10] complete alias methods for case participated model --- ProcessMaker/Models/CaseParticipated.php | 2 ++ ProcessMaker/Models/CaseStarted.php | 21 ++-------------- .../Traits/HandlesValueAliasStatus.php | 25 +++++++++++++++++++ 3 files changed, 29 insertions(+), 19 deletions(-) create mode 100644 ProcessMaker/Traits/HandlesValueAliasStatus.php diff --git a/ProcessMaker/Models/CaseParticipated.php b/ProcessMaker/Models/CaseParticipated.php index 29ce39e7b0..206c4de742 100644 --- a/ProcessMaker/Models/CaseParticipated.php +++ b/ProcessMaker/Models/CaseParticipated.php @@ -7,10 +7,12 @@ use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Database\Eloquent\Factories\HasFactory; use ProcessMaker\Models\ProcessMakerModel; +use ProcessMaker\Traits\HandlesValueAliasStatus; class CaseParticipated extends ProcessMakerModel { use HasFactory; + use HandlesValueAliasStatus; protected $table = 'cases_participated'; diff --git a/ProcessMaker/Models/CaseStarted.php b/ProcessMaker/Models/CaseStarted.php index dedc8c3177..a1eb1ff185 100644 --- a/ProcessMaker/Models/CaseStarted.php +++ b/ProcessMaker/Models/CaseStarted.php @@ -7,10 +7,12 @@ use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Database\Eloquent\Factories\HasFactory; use ProcessMaker\Models\ProcessMakerModel; +use ProcessMaker\Traits\HandlesValueAliasStatus; class CaseStarted extends ProcessMakerModel { use HasFactory; + use HandlesValueAliasStatus; protected $table = 'cases_started'; @@ -59,23 +61,4 @@ public function user() { return $this->belongsTo(User::class); } - - public function valueAliasStatus($value, $expression) - { - $statusMap = [ - 'in progress' => 'IN_PROGRESS', - 'completed' => 'COMPLETED', - 'error' => 'ERROR', - 'canceled' => 'CANCELED', - ]; - - $value = mb_strtolower($value); - - return function ($query) use ($value, $statusMap, $expression) { - if (array_key_exists($value, $statusMap)) { - $value = $statusMap[$value]; - } - $query->where('case_status', $expression->operator, $value); - }; - } } diff --git a/ProcessMaker/Traits/HandlesValueAliasStatus.php b/ProcessMaker/Traits/HandlesValueAliasStatus.php new file mode 100644 index 0000000000..7607ec506f --- /dev/null +++ b/ProcessMaker/Traits/HandlesValueAliasStatus.php @@ -0,0 +1,25 @@ + 'IN_PROGRESS', + 'completed' => 'COMPLETED', + 'error' => 'ERROR', + 'canceled' => 'CANCELED', + ]; + + $value = mb_strtolower($value); + + return function ($query) use ($value, $statusMap, $expression) { + if (array_key_exists($value, $statusMap)) { + $value = $statusMap[$value]; + } + $query->where('case_status', $expression->operator, $value); + }; + } +} From 195505eb346e5c016955344b0ddafec530e69a69 Mon Sep 17 00:00:00 2001 From: Rodrigo Quelca Date: Wed, 2 Oct 2024 12:44:39 -0400 Subject: [PATCH 06/10] remove status commit --- ProcessMaker/Filters/CasesFilter.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/ProcessMaker/Filters/CasesFilter.php b/ProcessMaker/Filters/CasesFilter.php index 66982a9950..dbab3e3fc4 100644 --- a/ProcessMaker/Filters/CasesFilter.php +++ b/ProcessMaker/Filters/CasesFilter.php @@ -4,8 +4,6 @@ class CasesFilter extends BaseFilter { - public const TYPE_STATUS = 'Status'; - protected function valueAliasMethod() { if ($this->subjectType === self::TYPE_STATUS) { From 12328db3d1b4d2c21585493e5b013747ed3d88e4 Mon Sep 17 00:00:00 2001 From: Rodrigo Quelca Date: Wed, 2 Oct 2024 15:48:34 -0400 Subject: [PATCH 07/10] add a invalid json for test --- tests/Feature/Api/V1_1/CaseControllerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Feature/Api/V1_1/CaseControllerTest.php b/tests/Feature/Api/V1_1/CaseControllerTest.php index 78689c540f..acaa757858 100644 --- a/tests/Feature/Api/V1_1/CaseControllerTest.php +++ b/tests/Feature/Api/V1_1/CaseControllerTest.php @@ -250,7 +250,7 @@ public function test_get_all_cases_filter_by(): void public function test_get_all_cases_filter_by_invalid_field(): void { $invalidField = 'invalid_field'; - $filterBy = ['filterBy' =>'']; + $filterBy = ['filterBy' =>'[invalid_json']; $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', $filterBy)); $response->assertStatus(422); $response->assertJsonPath('message', 'The Filter by field must be a valid JSON string.'); From 19a85ac8ab859d25f2670387946d042580ec3cc9 Mon Sep 17 00:00:00 2001 From: Rodrigo Quelca Date: Wed, 2 Oct 2024 16:18:37 -0400 Subject: [PATCH 08/10] add logger to test assert --- tests/Feature/Api/V1_1/CaseControllerTest.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/Feature/Api/V1_1/CaseControllerTest.php b/tests/Feature/Api/V1_1/CaseControllerTest.php index 9832963be1..f42bbe86ad 100644 --- a/tests/Feature/Api/V1_1/CaseControllerTest.php +++ b/tests/Feature/Api/V1_1/CaseControllerTest.php @@ -245,6 +245,9 @@ public function test_get_all_cases_filter_by(): void $filterBy = ['filterBy' =>'[{"subject":{"type":"Field","value":"user_id"},"operator":"=","value":"' . $userA->id . '"},{"subject":{"type":"Field","value":"case_status"},"operator":"=","value":"IN_PROGRESS"}, {"subject":{"type":"Field","value":"created_at"},"operator":">","value":"2023-02-10"}, {"subject":{"type":"Field","value":"completed_at"},"operator":">","value":"2023-04-01"}]']; $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', $filterBy)); + $json = $response->json(); + $metaTotal = $json['meta']['total']; + $this->assertEquals($total, $metaTotal, 'The total count of cases does not match the expected value. ' . json_encode($json)); $response->assertStatus(200); $total = $casesA->where('user_id', $userA->id)->where('case_status', 'IN_PROGRESS')->where('created_at', '>', '2023-02-10')->where('completed_at', '>', '2023-04-01')->count() + $casesB->where('user_id', $userA->id)->where('case_status', 'IN_PROGRESS')->where('created_at', '>', '2023-02-10')->where('completed_at', '>', '2023-04-01')->count(); $response->assertJsonCount($total, 'data'); From 469829ddec13cafb2964bab764c92ef1fbb75623 Mon Sep 17 00:00:00 2001 From: Rodrigo Quelca Date: Wed, 2 Oct 2024 16:32:21 -0400 Subject: [PATCH 09/10] refactor test_get_all_cases_filter_by --- tests/Feature/Api/V1_1/CaseControllerTest.php | 127 ++++++++++++++++-- 1 file changed, 117 insertions(+), 10 deletions(-) diff --git a/tests/Feature/Api/V1_1/CaseControllerTest.php b/tests/Feature/Api/V1_1/CaseControllerTest.php index f42bbe86ad..9d4cc39dc7 100644 --- a/tests/Feature/Api/V1_1/CaseControllerTest.php +++ b/tests/Feature/Api/V1_1/CaseControllerTest.php @@ -207,49 +207,156 @@ public function test_get_all_cases_filter_by(): void $userA = $this->createUser('user_a'); $casesA = $this->createCasesStartedForUser($userA->id, 5); $caseNumber = 123456; - $casesB = $this->createCasesStartedForUser($userA->id, 1, ['case_number' => $caseNumber, 'case_status' => 'IN_PROGRESS']); + $casesB = $this->createCasesStartedForUser($userA->id, 1, [ + 'case_number' => $caseNumber, + 'case_status' => 'IN_PROGRESS', + ]); $initiatedAt = $casesA->first()->initiated_at->format('Y-m-d'); + // Test: Get all cases $response = $this->apiCall('GET', route('api.1.1.cases.all_cases')); $response->assertStatus(200); $response->assertJsonCount(6, 'data'); - $filterBy = ['filterBy' =>'[{"subject":{"type":"Field","value":"case_number"},"operator":"=","value":"' . $caseNumber . '"}]']; + + // Test: Filter by case number + $filterBy = [ + 'filterBy' => json_encode([ + [ + 'subject' => ['type' => 'Field', 'value' => 'case_number'], + 'operator' => '=', + 'value' => $caseNumber, + ], + ]), + ]; $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', $filterBy)); $response->assertStatus(200); $response->assertJsonCount(1, 'data'); $response->assertJsonFragment(['case_number' => $caseNumber]); - $filterBy = ['filterBy' =>'[{"subject":{"type":"Field","value":"case_status"},"operator":"=","value":"IN_PROGRESS"}]']; + // Test: Filter by case status + $filterBy = [ + 'filterBy' => json_encode([ + [ + 'subject' => ['type' => 'Field', 'value' => 'case_status'], + 'operator' => '=', + 'value' => 'IN_PROGRESS', + ], + ]), + ]; $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', $filterBy)); $response->assertStatus(200); - $total = $casesA->where('case_status', 'IN_PROGRESS')->count() + $casesB->where('case_status', 'IN_PROGRESS')->count(); + + $total = $casesA->where('case_status', 'IN_PROGRESS')->count() + + $casesB->where('case_status', 'IN_PROGRESS')->count(); + $response->assertJsonCount($total, 'data'); $response->assertJsonFragment(['case_status' => 'IN_PROGRESS']); $response->assertJsonMissing(['case_status' => 'COMPLETED']); - $filterBy = ['filterBy' =>'[{"subject":{"type":"Field","value":"user_id"},"operator":"=","value":"' . $userA->id . '"},{"subject":{"type":"Field","value":"case_status"},"operator":"=","value":"IN_PROGRESS"}]']; + // Test: Filter by user ID and case status + $filterBy = [ + 'filterBy' => json_encode([ + [ + 'subject' => ['type' => 'Field', 'value' => 'user_id'], + 'operator' => '=', + 'value' => $userA->id, + ], + [ + 'subject' => ['type' => 'Field', 'value' => 'case_status'], + 'operator' => '=', + 'value' => 'IN_PROGRESS', + ], + ]), + ]; $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', $filterBy)); $response->assertStatus(200); - $total = $casesA->where('user_id', $userA->id)->where('case_status', 'IN_PROGRESS')->count() + $casesB->where('user_id', $userA->id)->where('case_status', 'IN_PROGRESS')->count(); + + $total = $casesA->where('user_id', $userA->id) + ->where('case_status', 'IN_PROGRESS')->count() + + $casesB->where('user_id', $userA->id) + ->where('case_status', 'IN_PROGRESS')->count(); + $response->assertJsonCount($total, 'data'); $response->assertJsonFragment(['case_status' => 'IN_PROGRESS']); $response->assertJsonFragment(['user_id' => $userA->id]); - $filterBy = ['filterBy' =>'[{"subject":{"type":"Field","value":"user_id"},"operator":"=","value":"' . $userA->id . '"},{"subject":{"type":"Field","value":"case_status"},"operator":"=","value":"IN_PROGRESS"}, {"subject":{"type":"Field","value":"created_at"},"operator":">","value":"2023-02-10"}]']; + // Test: Filter by user ID, case status, and created_at + $filterBy = [ + 'filterBy' => json_encode([ + [ + 'subject' => ['type' => 'Field', 'value' => 'user_id'], + 'operator' => '=', + 'value' => $userA->id, + ], + [ + 'subject' => ['type' => 'Field', 'value' => 'case_status'], + 'operator' => '=', + 'value' => 'IN_PROGRESS', + ], + [ + 'subject' => ['type' => 'Field', 'value' => 'created_at'], + 'operator' => '>', + 'value' => '2023-02-10', + ], + ]), + ]; $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', $filterBy)); $response->assertStatus(200); - $total = $casesA->where('user_id', $userA->id)->where('case_status', 'IN_PROGRESS')->where('created_at', '>', '2023-02-10')->count() + $casesB->where('user_id', $userA->id)->where('case_status', 'IN_PROGRESS')->where('created_at', '>', '2023-02-10')->count(); + + $total = $casesA->where('user_id', $userA->id) + ->where('case_status', 'IN_PROGRESS') + ->where('created_at', '>', '2023-02-10')->count() + + $casesB->where('user_id', $userA->id) + ->where('case_status', 'IN_PROGRESS') + ->where('created_at', '>', '2023-02-10')->count(); + $response->assertJsonCount($total, 'data'); $response->assertJsonFragment(['case_status' => 'IN_PROGRESS']); $response->assertJsonFragment(['user_id' => $userA->id]); - $filterBy = ['filterBy' =>'[{"subject":{"type":"Field","value":"user_id"},"operator":"=","value":"' . $userA->id . '"},{"subject":{"type":"Field","value":"case_status"},"operator":"=","value":"IN_PROGRESS"}, {"subject":{"type":"Field","value":"created_at"},"operator":">","value":"2023-02-10"}, {"subject":{"type":"Field","value":"completed_at"},"operator":">","value":"2023-04-01"}]']; + // Test: Filter by user ID, case status, created_at, and completed_at + $filterBy = [ + 'filterBy' => json_encode([ + [ + 'subject' => ['type' => 'Field', 'value' => 'user_id'], + 'operator' => '=', + 'value' => $userA->id, + ], + [ + 'subject' => ['type' => 'Field', 'value' => 'case_status'], + 'operator' => '=', + 'value' => 'IN_PROGRESS', + ], + [ + 'subject' => ['type' => 'Field', 'value' => 'created_at'], + 'operator' => '>', + 'value' => '2023-02-10', + ], + [ + 'subject' => ['type' => 'Field', 'value' => 'completed_at'], + 'operator' => '>', + 'value' => '2023-04-01', + ], + ]), + ]; $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', $filterBy)); + + $total = $casesA->where('user_id', $userA->id) + ->where('case_status', 'IN_PROGRESS') + ->where('created_at', '>', '2023-02-10') + ->where('completed_at', '>', '2023-04-01')->count() + + $casesB->where('user_id', $userA->id) + ->where('case_status', 'IN_PROGRESS') + ->where('created_at', '>', '2023-02-10') + ->where('completed_at', '>', '2023-04-01')->count(); + $json = $response->json(); $metaTotal = $json['meta']['total']; $this->assertEquals($total, $metaTotal, 'The total count of cases does not match the expected value. ' . json_encode($json)); + $response->assertStatus(200); - $total = $casesA->where('user_id', $userA->id)->where('case_status', 'IN_PROGRESS')->where('created_at', '>', '2023-02-10')->where('completed_at', '>', '2023-04-01')->count() + $casesB->where('user_id', $userA->id)->where('case_status', 'IN_PROGRESS')->where('created_at', '>', '2023-02-10')->where('completed_at', '>', '2023-04-01')->count(); + + print_r($total); $response->assertJsonCount($total, 'data'); } From 3dca091fd3ee1614203eb515b85bdb8c43220cf2 Mon Sep 17 00:00:00 2001 From: Rodrigo Quelca Date: Wed, 2 Oct 2024 17:08:16 -0400 Subject: [PATCH 10/10] test improvement --- tests/Feature/Api/V1_1/CaseControllerTest.php | 113 +++++++++++------- 1 file changed, 71 insertions(+), 42 deletions(-) diff --git a/tests/Feature/Api/V1_1/CaseControllerTest.php b/tests/Feature/Api/V1_1/CaseControllerTest.php index 9d4cc39dc7..a512bdffdc 100644 --- a/tests/Feature/Api/V1_1/CaseControllerTest.php +++ b/tests/Feature/Api/V1_1/CaseControllerTest.php @@ -202,23 +202,13 @@ public function test_get_all_cases_sort_by_invalid_field(): void $response->assertJsonFragment(['message' => "Sort by field $invalidField is not allowed."]); } - public function test_get_all_cases_filter_by(): void + public function test_filter_by_case_number(): void { $userA = $this->createUser('user_a'); - $casesA = $this->createCasesStartedForUser($userA->id, 5); $caseNumber = 123456; - $casesB = $this->createCasesStartedForUser($userA->id, 1, [ - 'case_number' => $caseNumber, - 'case_status' => 'IN_PROGRESS', - ]); - $initiatedAt = $casesA->first()->initiated_at->format('Y-m-d'); - - // Test: Get all cases - $response = $this->apiCall('GET', route('api.1.1.cases.all_cases')); - $response->assertStatus(200); - $response->assertJsonCount(6, 'data'); + $this->createCasesStartedForUser($userA->id, 5); + $this->createCasesStartedForUser($userA->id, 1, ['case_number' => $caseNumber]); - // Test: Filter by case number $filterBy = [ 'filterBy' => json_encode([ [ @@ -228,12 +218,23 @@ public function test_get_all_cases_filter_by(): void ], ]), ]; + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', $filterBy)); $response->assertStatus(200); $response->assertJsonCount(1, 'data'); $response->assertJsonFragment(['case_number' => $caseNumber]); + } + + public function test_filter_by_case_status(): void + { + $userA = $this->createUser('user_a'); + $casesA = $this->createCasesStartedForUser($userA->id, 5); + $caseNumber = 123456; + $casesB = $this->createCasesStartedForUser($userA->id, 1, [ + 'case_number' => $caseNumber, + 'case_status' => 'IN_PROGRESS', + ]); - // Test: Filter by case status $filterBy = [ 'filterBy' => json_encode([ [ @@ -243,17 +244,27 @@ public function test_get_all_cases_filter_by(): void ], ]), ]; + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', $filterBy)); $response->assertStatus(200); $total = $casesA->where('case_status', 'IN_PROGRESS')->count() + - $casesB->where('case_status', 'IN_PROGRESS')->count(); - + $casesB->where('case_status', 'IN_PROGRESS')->count(); $response->assertJsonCount($total, 'data'); $response->assertJsonFragment(['case_status' => 'IN_PROGRESS']); $response->assertJsonMissing(['case_status' => 'COMPLETED']); + } + + public function test_filter_by_user_and_case_status(): void + { + $userA = $this->createUser('user_a'); + $casesA = $this->createCasesStartedForUser($userA->id, 5); + $caseNumber = 123456; + $casesB = $this->createCasesStartedForUser($userA->id, 1, [ + 'case_number' => $caseNumber, + 'case_status' => 'IN_PROGRESS', + ]); - // Test: Filter by user ID and case status $filterBy = [ 'filterBy' => json_encode([ [ @@ -268,19 +279,29 @@ public function test_get_all_cases_filter_by(): void ], ]), ]; + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', $filterBy)); $response->assertStatus(200); $total = $casesA->where('user_id', $userA->id) - ->where('case_status', 'IN_PROGRESS')->count() + - $casesB->where('user_id', $userA->id) - ->where('case_status', 'IN_PROGRESS')->count(); - + ->where('case_status', 'IN_PROGRESS')->count() + + $casesB->where('user_id', $userA->id) + ->where('case_status', 'IN_PROGRESS')->count(); $response->assertJsonCount($total, 'data'); $response->assertJsonFragment(['case_status' => 'IN_PROGRESS']); $response->assertJsonFragment(['user_id' => $userA->id]); + } + + public function test_filter_by_user_case_status_and_created_at(): void + { + $userA = $this->createUser('user_a'); + $casesA = $this->createCasesStartedForUser($userA->id, 5); + $caseNumber = 123456; + $casesB = $this->createCasesStartedForUser($userA->id, 1, [ + 'case_number' => $caseNumber, + 'case_status' => 'IN_PROGRESS', + ]); - // Test: Filter by user ID, case status, and created_at $filterBy = [ 'filterBy' => json_encode([ [ @@ -300,21 +321,31 @@ public function test_get_all_cases_filter_by(): void ], ]), ]; + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', $filterBy)); $response->assertStatus(200); $total = $casesA->where('user_id', $userA->id) - ->where('case_status', 'IN_PROGRESS') - ->where('created_at', '>', '2023-02-10')->count() + - $casesB->where('user_id', $userA->id) - ->where('case_status', 'IN_PROGRESS') - ->where('created_at', '>', '2023-02-10')->count(); - + ->where('case_status', 'IN_PROGRESS') + ->where('created_at', '>', '2023-02-10')->count() + + $casesB->where('user_id', $userA->id) + ->where('case_status', 'IN_PROGRESS') + ->where('created_at', '>', '2023-02-10')->count(); $response->assertJsonCount($total, 'data'); $response->assertJsonFragment(['case_status' => 'IN_PROGRESS']); $response->assertJsonFragment(['user_id' => $userA->id]); + } + + public function test_filter_by_user_case_status_created_at_and_completed_at(): void + { + $userA = $this->createUser('user_a'); + $casesA = $this->createCasesStartedForUser($userA->id, 5); + $caseNumber = 123456; + $casesB = $this->createCasesStartedForUser($userA->id, 1, [ + 'case_number' => $caseNumber, + 'case_status' => 'IN_PROGRESS', + ]); - // Test: Filter by user ID, case status, created_at, and completed_at $filterBy = [ 'filterBy' => json_encode([ [ @@ -339,31 +370,29 @@ public function test_get_all_cases_filter_by(): void ], ]), ]; + $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', $filterBy)); + $response->assertStatus(200); $total = $casesA->where('user_id', $userA->id) - ->where('case_status', 'IN_PROGRESS') - ->where('created_at', '>', '2023-02-10') - ->where('completed_at', '>', '2023-04-01')->count() + - $casesB->where('user_id', $userA->id) - ->where('case_status', 'IN_PROGRESS') - ->where('created_at', '>', '2023-02-10') - ->where('completed_at', '>', '2023-04-01')->count(); + ->where('case_status', 'IN_PROGRESS') + ->where('created_at', '>', '2023-02-10') + ->where('completed_at', '>', '2023-04-01')->count() + + $casesB->where('user_id', $userA->id) + ->where('case_status', 'IN_PROGRESS') + ->where('created_at', '>', '2023-02-10') + ->where('completed_at', '>', '2023-04-01')->count(); + $response->assertJsonCount($total, 'data'); $json = $response->json(); $metaTotal = $json['meta']['total']; $this->assertEquals($total, $metaTotal, 'The total count of cases does not match the expected value. ' . json_encode($json)); - - $response->assertStatus(200); - - print_r($total); - $response->assertJsonCount($total, 'data'); } public function test_get_all_cases_filter_by_invalid_field(): void { $invalidField = 'invalid_field'; - $filterBy = ['filterBy' =>'[invalid_json']; + $filterBy = ['filterBy' => '[invalid_json']; $response = $this->apiCall('GET', route('api.1.1.cases.all_cases', $filterBy)); $response->assertStatus(422); $response->assertJsonPath('message', 'The Filter by field must be a valid JSON string.');