diff --git a/app/JsonApi/V2/Accounts/AccountCollectionQuery.php b/app/JsonApi/V2/Accounts/AccountCollectionQuery.php index 464256fe4a..759ca94ab9 100644 --- a/app/JsonApi/V2/Accounts/AccountCollectionQuery.php +++ b/app/JsonApi/V2/Accounts/AccountCollectionQuery.php @@ -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', diff --git a/app/JsonApi/V2/Accounts/AccountRequest.php b/app/JsonApi/V2/Accounts/AccountRequest.php index dfcca68373..82b2607982 100644 --- a/app/JsonApi/V2/Accounts/AccountRequest.php +++ b/app/JsonApi/V2/Accounts/AccountRequest.php @@ -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', + ], ]; } diff --git a/app/JsonApi/V2/Accounts/AccountSchema.php b/app/JsonApi/V2/Accounts/AccountSchema.php index 7dfca106e0..3a8bc3e4e4 100644 --- a/app/JsonApi/V2/Accounts/AccountSchema.php +++ b/app/JsonApi/V2/Accounts/AccountSchema.php @@ -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 diff --git a/app/JsonApi/V2/Accounts/Capabilities/AccountQuery.php b/app/JsonApi/V2/Accounts/Capabilities/AccountQuery.php index a0c40474d3..c7bdc57dbb 100644 --- a/app/JsonApi/V2/Accounts/Capabilities/AccountQuery.php +++ b/app/JsonApi/V2/Accounts/Capabilities/AccountQuery.php @@ -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. diff --git a/app/Policies/AccountPolicy.php b/app/Policies/AccountPolicy.php index 9820911f1c..3e4b6f85c6 100644 --- a/app/Policies/AccountPolicy.php +++ b/app/Policies/AccountPolicy.php @@ -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(); } diff --git a/app/Rules/Account/IsValidAccountType.php b/app/Rules/Account/IsValidAccountType.php new file mode 100644 index 0000000000..0bace91250 --- /dev/null +++ b/app/Rules/Account/IsValidAccountType.php @@ -0,0 +1,57 @@ +types); + /** @var mixed $entry */ + foreach ($value as $entry) { + $entry = (string) $entry; + if (!in_array($entry, $keys)) { + $fail('something'); + } + } + } + } +} diff --git a/app/Support/Http/Api/AccountFilter.php b/app/Support/Http/Api/AccountFilter.php index 7c41a9d6c5..4f28f35710 100644 --- a/app/Support/Http/Api/AccountFilter.php +++ b/app/Support/Http/Api/AccountFilter.php @@ -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']; } } diff --git a/app/Support/JsonApi/ExpandsQuery.php b/app/Support/JsonApi/ExpandsQuery.php index e633fdb908..529bd780b8 100644 --- a/app/Support/JsonApi/ExpandsQuery.php +++ b/app/Support/JsonApi/ExpandsQuery.php @@ -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; } } diff --git a/app/Support/JsonApi/ValidateSortParameters.php b/app/Support/JsonApi/ValidateSortParameters.php index 83d2a52491..fecc424113 100644 --- a/app/Support/JsonApi/ValidateSortParameters.php +++ b/app/Support/JsonApi/ValidateSortParameters.php @@ -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)) { diff --git a/config/api.php b/config/api.php new file mode 100644 index 0000000000..7ef00a2d63 --- /dev/null +++ b/config/api.php @@ -0,0 +1,58 @@ + [ + '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'], + ], +]; diff --git a/config/firefly.php b/config/firefly.php index 3ee8912d49..730ca7bf2c 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -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'], - ], ];