Allow account endpoint to be filtered on various fields.

This commit is contained in:
James Cole 2024-08-03 06:00:22 +02:00
parent c8646e20cb
commit b213148ae8
No known key found for this signature in database
GPG Key ID: B49A324B7EAD6D80
11 changed files with 259 additions and 106 deletions

View File

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace FireflyIII\JsonApi\V2\Accounts;
use FireflyIII\Models\Account;
use FireflyIII\Rules\Account\IsValidAccountType;
use FireflyIII\Rules\IsAllowedGroupAction;
use FireflyIII\Rules\IsDateOrTime;
use FireflyIII\Rules\IsValidDateRange;
@ -20,6 +21,7 @@ class AccountCollectionQuery extends ResourceQuery
public function rules(): array
{
Log::debug(__METHOD__);
$validFilters = config('api.valid_api_filters')[Account::class];
return [
'fields' => [
@ -47,7 +49,8 @@ class AccountCollectionQuery extends ResourceQuery
'filter' => [
'nullable',
'array',
JsonApiRule::filter(),
JsonApiRule::filter($validFilters),
new IsValidAccountType()
],
'include' => [
'nullable',

View File

@ -2,6 +2,8 @@
namespace FireflyIII\JsonApi\V2\Accounts;
use FireflyIII\Rules\BelongsUser;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\Rule;
use LaravelJsonApi\Laravel\Http\Requests\ResourceRequest;
use LaravelJsonApi\Validation\Rule as JsonApiRule;
@ -16,8 +18,17 @@ class AccountRequest extends ResourceRequest
*/
public function rules(): array
{
Log::debug(__METHOD__);
die('am i used');
return [
// @TODO
'type' => [
new BelongsUser()
],
'name' => [
'nullable',
'string',
'max:255',
],
];
}

View File

@ -79,11 +79,13 @@ class AccountSchema extends Schema
*/
public function filters(): array
{
// Log::debug(__METHOD__);
return [
Filter::make('id'),
];
Log::debug(__METHOD__);
$array = [];
$config = config('api.valid_api_filters')[Account::class];
foreach ($config as $entry) {
$array[] = Filter::make($entry);
}
return $array;
}
public function repository(): AccountRepository

View File

@ -23,6 +23,8 @@ declare(strict_types=1);
namespace FireflyIII\JsonApi\V2\Accounts\Capabilities;
use FireflyIII\Models\Account;
use FireflyIII\Support\Http\Api\AccountFilter;
use FireflyIII\Support\JsonApi\CollectsCustomParameters;
use FireflyIII\Support\JsonApi\Concerns\UsergroupAware;
use FireflyIII\Support\JsonApi\Enrichments\AccountEnrichment;
@ -44,6 +46,7 @@ class AccountQuery extends QueryAll implements HasPagination
use UsergroupAware;
use ValidateSortParameters;
use CollectsCustomParameters;
use AccountFilter;
#[\Override]
/**
@ -56,6 +59,7 @@ class AccountQuery extends QueryAll implements HasPagination
Log::debug(__METHOD__);
// collect filters
$filters = $this->queryParameters->filter();
// collect sort options
$sort = $this->queryParameters->sortFields();
// collect pagination based on the page
@ -77,7 +81,7 @@ class AccountQuery extends QueryAll implements HasPagination
// add sort and filter parameters to the query.
$query = $this->addSortParams($query, $sort);
$query = $this->addFilterParams('account', $query, $filters);
$query = $this->addFilterParams(Account::class, $query, $filters);
// collect the result.

View File

@ -25,6 +25,7 @@ namespace FireflyIII\Policies;
use FireflyIII\Models\Account;
use FireflyIII\User;
use Illuminate\Support\Facades\Log;
class AccountPolicy
{
@ -33,7 +34,7 @@ class AccountPolicy
*/
public function view(User $user, Account $account): bool
{
die('OK');
die('OK1');
return true;
return auth()->check() && $user->id === $account->user_id;
@ -46,9 +47,7 @@ class AccountPolicy
*/
public function viewAny(): bool
{
die('OK');
return true;
Log::debug(__METHOD__);
return auth()->check();
}

View File

@ -0,0 +1,57 @@
<?php
/*
* IsValidAccountType.php
* Copyright (c) 2024 james@firefly-iii.org.
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
declare(strict_types=1);
namespace FireflyIII\Rules\Account;
use Closure;
use FireflyIII\Support\Http\Api\AccountFilter;
use Illuminate\Contracts\Validation\ValidationRule;
class IsValidAccountType implements ValidationRule
{
use AccountFilter;
/**
* @inheritDoc
*/
#[\Override] public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (!is_array($value)) {
$value = [$value];
}
// only check the type.
if (array_key_exists('type', $value)) {
$value = $value['type'];
$filtered = [];
$keys = array_keys($this->types);
/** @var mixed $entry */
foreach ($value as $entry) {
$entry = (string) $entry;
if (!in_array($entry, $keys)) {
$fail('something');
}
}
}
}
}

View File

@ -30,63 +30,62 @@ use FireflyIII\Models\AccountType;
*/
trait AccountFilter
{
protected array $types = [
'all' => [
AccountType::DEFAULT,
AccountType::CASH,
AccountType::ASSET,
AccountType::EXPENSE,
AccountType::REVENUE,
AccountType::INITIAL_BALANCE,
AccountType::BENEFICIARY,
AccountType::IMPORT,
AccountType::RECONCILIATION,
AccountType::LOAN,
AccountType::DEBT,
AccountType::MORTGAGE,
],
'asset' => [AccountType::DEFAULT, AccountType::ASSET],
'cash' => [AccountType::CASH],
'expense' => [AccountType::EXPENSE, AccountType::BENEFICIARY],
'revenue' => [AccountType::REVENUE],
'special' => [AccountType::CASH, AccountType::INITIAL_BALANCE, AccountType::IMPORT, AccountType::RECONCILIATION],
'hidden' => [AccountType::INITIAL_BALANCE, AccountType::IMPORT, AccountType::RECONCILIATION],
'liability' => [AccountType::DEBT, AccountType::LOAN, AccountType::MORTGAGE, AccountType::CREDITCARD],
'liabilities' => [AccountType::DEBT, AccountType::LOAN, AccountType::MORTGAGE, AccountType::CREDITCARD],
AccountType::DEFAULT => [AccountType::DEFAULT],
AccountType::CASH => [AccountType::CASH],
AccountType::ASSET => [AccountType::ASSET],
AccountType::EXPENSE => [AccountType::EXPENSE],
AccountType::REVENUE => [AccountType::REVENUE],
AccountType::INITIAL_BALANCE => [AccountType::INITIAL_BALANCE],
AccountType::BENEFICIARY => [AccountType::BENEFICIARY],
AccountType::IMPORT => [AccountType::IMPORT],
AccountType::RECONCILIATION => [AccountType::RECONCILIATION],
AccountType::LOAN => [AccountType::LOAN],
AccountType::MORTGAGE => [AccountType::MORTGAGE],
AccountType::DEBT => [AccountType::DEBT],
AccountType::CREDITCARD => [AccountType::CREDITCARD],
'default account' => [AccountType::DEFAULT],
'cash account' => [AccountType::CASH],
'asset account' => [AccountType::ASSET],
'expense account' => [AccountType::EXPENSE],
'revenue account' => [AccountType::REVENUE],
'initial balance account' => [AccountType::INITIAL_BALANCE],
'reconciliation' => [AccountType::RECONCILIATION],
'loan' => [AccountType::LOAN],
'mortgage' => [AccountType::MORTGAGE],
'debt' => [AccountType::DEBT],
'credit card' => [AccountType::CREDITCARD],
'credit-card' => [AccountType::CREDITCARD],
'creditcard' => [AccountType::CREDITCARD],
'cc' => [AccountType::CREDITCARD],
];
/**
* All the available types.
*/
protected function mapAccountTypes(string $type): array
{
$types = [
'all' => [
AccountType::DEFAULT,
AccountType::CASH,
AccountType::ASSET,
AccountType::EXPENSE,
AccountType::REVENUE,
AccountType::INITIAL_BALANCE,
AccountType::BENEFICIARY,
AccountType::IMPORT,
AccountType::RECONCILIATION,
AccountType::LOAN,
AccountType::DEBT,
AccountType::MORTGAGE,
],
'asset' => [AccountType::DEFAULT, AccountType::ASSET],
'cash' => [AccountType::CASH],
'expense' => [AccountType::EXPENSE, AccountType::BENEFICIARY],
'revenue' => [AccountType::REVENUE],
'special' => [AccountType::CASH, AccountType::INITIAL_BALANCE, AccountType::IMPORT, AccountType::RECONCILIATION],
'hidden' => [AccountType::INITIAL_BALANCE, AccountType::IMPORT, AccountType::RECONCILIATION],
'liability' => [AccountType::DEBT, AccountType::LOAN, AccountType::MORTGAGE, AccountType::CREDITCARD],
'liabilities' => [AccountType::DEBT, AccountType::LOAN, AccountType::MORTGAGE, AccountType::CREDITCARD],
AccountType::DEFAULT => [AccountType::DEFAULT],
AccountType::CASH => [AccountType::CASH],
AccountType::ASSET => [AccountType::ASSET],
AccountType::EXPENSE => [AccountType::EXPENSE],
AccountType::REVENUE => [AccountType::REVENUE],
AccountType::INITIAL_BALANCE => [AccountType::INITIAL_BALANCE],
AccountType::BENEFICIARY => [AccountType::BENEFICIARY],
AccountType::IMPORT => [AccountType::IMPORT],
AccountType::RECONCILIATION => [AccountType::RECONCILIATION],
AccountType::LOAN => [AccountType::LOAN],
AccountType::MORTGAGE => [AccountType::MORTGAGE],
AccountType::DEBT => [AccountType::DEBT],
AccountType::CREDITCARD => [AccountType::CREDITCARD],
'default account' => [AccountType::DEFAULT],
'cash account' => [AccountType::CASH],
'asset account' => [AccountType::ASSET],
'expense account' => [AccountType::EXPENSE],
'revenue account' => [AccountType::REVENUE],
'initial balance account' => [AccountType::INITIAL_BALANCE],
'reconciliation' => [AccountType::RECONCILIATION],
'loan' => [AccountType::LOAN],
'mortgage' => [AccountType::MORTGAGE],
'debt' => [AccountType::DEBT],
'credit card' => [AccountType::CREDITCARD],
'credit-card' => [AccountType::CREDITCARD],
'creditcard' => [AccountType::CREDITCARD],
'cc' => [AccountType::CREDITCARD],
];
return $types[$type] ?? $types['all'];
return $this->types[$type] ?? $this->types['all'];
}
}

View File

@ -23,8 +23,10 @@ declare(strict_types=1);
namespace FireflyIII\Support\JsonApi;
use FireflyIII\Models\Account;
use FireflyIII\Support\Http\Api\AccountFilter;
use Illuminate\Contracts\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Log;
use LaravelJsonApi\Core\Query\FilterParameters;
use LaravelJsonApi\Core\Query\SortFields;
@ -51,35 +53,80 @@ trait ExpandsQuery
return $query;
}
private function parseAccountTypeFilter(array $value): array
{
$return = [];
foreach ($value as $entry) {
$return = array_merge($return, $this->mapAccountTypes($entry));
}
return array_unique($return);
}
private function parseAllFilters(string $class, FilterParameters $filters): array
{
$config = config('api.valid_api_filters')[$class];
$parsed = [];
foreach ($filters->all() as $filter) {
$key = $filter->key();
if (!in_array($key, $config, true)) {
continue;
}
// make array if not array:
$value = $filter->value();
if (null === $value) {
continue;
}
if (!is_array($value)) {
$value = [$value];
}
switch ($filter->key()) {
case 'name':
$parsed['name'] = $value;
break;
case 'type':
$parsed['type'] = $this->parseAccountTypeFilter($value);
break;
}
}
return $parsed;
}
final protected function addFilterParams(string $class, Builder $query, ?FilterParameters $filters): Builder
{
Log::debug(__METHOD__);
if (null === $filters) {
return $query;
}
$config = config(sprintf('firefly.valid_query_filters.%s', $class)) ?? [];
if (0 === count($filters->all())) {
return $query;
}
$query->where(function (Builder $q) use ($config, $filters): void {
foreach ($filters->all() as $filter) {
if (in_array($filter->key(), $config, true)) {
foreach ($filter->value() as $value) {
$q->where($filter->key(), 'LIKE', sprintf('%%%s%%', $value));
// parse filters valid for this class.
$parsed = $this->parseAllFilters($class, $filters);
// expand query for each query filter
$config = config('api.valid_query_filters')[$class];
$query->where(function (Builder $q) use ($config, $parsed): void {
foreach ($parsed as $key => $filter) {
if (in_array($key, $config, true)) {
Log::debug(sprintf('Add query filter "%s"', $key));
// add type to query:
foreach ($filter as $value) {
$q->where($key, 'LIKE', sprintf('%%%s%%', $value));
}
}
}
});
// some filters are special, i.e. the account type filter.
$typeFilters = $filters->value('type', false);
if (false !== $typeFilters && count($typeFilters) > 0) {
$query->leftJoin('account_types', 'accounts.account_type_id', '=', 'account_types.id');
foreach ($typeFilters as $typeFilter) {
$types = $this->mapAccountTypes($typeFilter);
$query->whereIn('account_types.type', $types);
// TODO this is special treatment, but alas, unavoidable right now.
if ($class === Account::class && array_key_exists('type', $parsed)) {
if (count($parsed['type']) > 0) {
$query->leftJoin('account_types', 'accounts.account_type_id', '=', 'account_types.id');
$query->whereIn('account_types.type', $parsed['type']);
}
}
return $query;
}
}

View File

@ -33,7 +33,7 @@ trait ValidateSortParameters
return false;
}
$config = config(sprintf('firefly.full_data_set.%s', $class)) ?? [];
$config = config(sprintf('api.full_data_set.%s', $class)) ?? [];
foreach ($params->all() as $field) {
if (in_array($field->name(), $config, true)) {

58
config/api.php Normal file
View File

@ -0,0 +1,58 @@
<?php
/*
* api.php
* Copyright (c) 2024 james@firefly-iii.org.
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
declare(strict_types=1);
use FireflyIII\Models\Account;
return [
// allowed filters (search) for APIs
'filters' => [
'allowed' => [
'accounts' => [
'name' => 'string',
'active' => 'boolean',
'iban' => 'iban',
'balance' => 'numeric',
'last_activity' => 'date',
'balance_difference' => 'numeric',
],
],
],
// allowed sort columns for APIs
'sorting' => [
'allowed' => [
'transactions' => ['description', 'amount'],
'accounts' => ['name', 'active', 'iban', 'balance', 'last_activity', 'balance_difference', 'current_debt'],
],
],
'full_data_set' => [
'account' => ['last_activity', 'balance_difference', 'current_balance', 'current_debt'],
],
'valid_query_filters' => [
Account::class => ['id','name', 'iban', 'active'],
],
'valid_api_filters' => [
Account::class => ['id', 'name', 'iban', 'active', 'type'],
],
];

View File

@ -920,31 +920,4 @@ return [
// preselected account lists possibilities:
'preselected_accounts' => ['all', 'assets', 'liabilities'],
// allowed filters (search) for APIs
'filters' => [
'allowed' => [
'accounts' => [
'name' => 'string',
'active' => 'boolean',
'iban' => 'iban',
'balance' => 'numeric',
'last_activity' => 'date',
'balance_difference' => 'numeric',
],
],
],
// allowed sort columns for APIs
'sorting' => [
'allowed' => [
'transactions' => ['description', 'amount'],
'accounts' => ['name', 'active', 'iban', 'balance', 'last_activity', 'balance_difference', 'current_debt'],
],
],
'full_data_set' => [
'account' => ['last_activity', 'balance_difference', 'current_balance', 'current_debt'],
],
'valid_query_filters' => [
'account' => ['name', 'iban', 'active'],
],
];