Fix most of the index with json laravel api endpoints.

This commit is contained in:
James Cole 2024-08-03 13:15:39 +02:00
parent 5e6034fc86
commit 762d898fee
No known key found for this signature in database
GPG Key ID: B49A324B7EAD6D80
9 changed files with 221 additions and 49 deletions

View File

@ -63,7 +63,7 @@ class AccountSchema extends Schema
Attribute::make('current_debt')->sortable(),
// dynamic data
Attribute::make('last_activity'),
Attribute::make('last_activity')->sortable(),
Attribute::make('balance_difference')->sortable(), // only used for sort.
// group

View File

@ -31,13 +31,13 @@ use FireflyIII\Support\JsonApi\Enrichments\AccountEnrichment;
use FireflyIII\Support\JsonApi\ExpandsQuery;
use FireflyIII\Support\JsonApi\FiltersPagination;
use FireflyIII\Support\JsonApi\SortsCollection;
use FireflyIII\Support\JsonApi\SortsQueryResults;
use FireflyIII\Support\JsonApi\ValidateSortParameters;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\Log;
use LaravelJsonApi\Contracts\Pagination\Page;
use LaravelJsonApi\Contracts\Store\HasPagination;
use LaravelJsonApi\NonEloquent\Capabilities\QueryAll;
use LaravelJsonApi\NonEloquent\Concerns\PaginatesEnumerables;
class AccountQuery extends QueryAll implements HasPagination
{
@ -48,6 +48,8 @@ class AccountQuery extends QueryAll implements HasPagination
use ValidateSortParameters;
use CollectsCustomParameters;
use AccountFilter;
use SortsQueryResults;
//use PaginatesEnumerables;
#[\Override]
@ -59,17 +61,15 @@ class AccountQuery extends QueryAll implements HasPagination
public function get(): iterable
{
Log::debug(__METHOD__);
// collect filters
$filters = $this->queryParameters->filter();
// collect sort options
$sort = $this->queryParameters->sortFields();
// collect pagination based on the page
$pagination = $this->filtersPagination($this->queryParameters->page());
// check if we need all accounts, regardless of pagination
// This is necessary when the user wants to sort on specific params.
$needsAll = $this->needsFullDataset('account', $sort);
$needsAll = $this->needsFullDataset(Account::class, $sort);
// params that were not recognised, may be my own custom stuff.
$otherParams = $this->getOtherParams($this->queryParameters->unrecognisedParameters());
@ -77,34 +77,56 @@ class AccountQuery extends QueryAll implements HasPagination
// start the query
$query = $this->userGroup->accounts();
// if (!$needsAll) {
// Log::debug('Does not need full dataset, will paginate.');
// $query = $this->addPagination($query, $pagination);
// }
// add sort and filter parameters to the query.
$query = $this->addSortParams(Account::class, $query, $sort);
$query = $this->addFilterParams(Account::class, $query, $filters);
$query = $this->addFilterParams(Account::class, $query, $this->queryParameters->filter());
// collect the result.
$collection = $query->get(['accounts.*']);
// sort the data after the query, and return it right away.
$sorted = $this->sortCollection(Account::class, $collection, $sort);
$collection = $this->sortCollection(Account::class, $collection, $sort);
// take from the collection the filtered page + page number:
$currentPage = $sorted->skip($pagination['number'] - 1 * $pagination['size'])->take($pagination['size']);
// if the entire collection needs to be enriched and sorted, do so now:
$totalCount = $collection->count();
Log::debug(sprintf('Total is %d', $totalCount));
if ($needsAll) {
Log::debug('Needs the entire collection');
// enrich the entire collection
$enrichment = new AccountEnrichment();
$enrichment->setStart($otherParams['start'] ?? null);
$enrichment->setEnd($otherParams['end'] ?? null);
$collection = $enrichment->enrich($collection);
// TODO sort the set based on post-query sort options:
$collection = $this->postQuerySort(Account::class, $collection, $sort);
// take the current page from the enriched set.
$currentPage = $collection->skip(($pagination['number'] - 1) * $pagination['size'])->take($pagination['size']);
}
if (!$needsAll) {
Log::debug('Needs only partial collection');
// take from the collection the filtered page + page number:
$currentPage = $collection->skip(($pagination['number'] - 1) * $pagination['size'])->take($pagination['size']);
// enrich only the current page.
$enrichment = new AccountEnrichment();
$enrichment->setStart($otherParams['start'] ?? null);
$enrichment->setEnd($otherParams['end'] ?? null);
$currentPage = $enrichment->enrich($currentPage);
}
// get current page?
Log::debug(sprintf('Skip %d, take %d', ($pagination['number'] - 1) * $pagination['size'], $pagination['size']));
//$currentPage = $collection->skip(($pagination['number'] - 1) * $pagination['size'])->take($pagination['size']);
Log::debug(sprintf('New collection size: %d', $currentPage->count()));
// enrich the current page.
$enrichment = new AccountEnrichment();
$enrichment->setStart($otherParams['start'] ?? null);
$enrichment->setEnd($otherParams['end'] ?? null);
$currentPage = $enrichment->enrich($currentPage);
// TODO add filters after the query, if there are filters that cannot be applied to the database
// TODO same for sort things.
return new LengthAwarePaginator($currentPage,$sorted->count(),$pagination['size'],$pagination['number']);
return new LengthAwarePaginator($currentPage, $totalCount, $pagination['size'], $pagination['number']);
}
/**

View File

@ -37,12 +37,14 @@ class IsValidAccountType implements ValidationRule
#[\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'];
if (!is_array($value)) {
$value = [$value];
}
$filtered = [];
$keys = array_keys($this->types);
/** @var mixed $entry */

View File

@ -37,6 +37,9 @@ trait CollectsCustomParameters
if (array_key_exists('endPeriod', $params)) {
$return['end'] = Carbon::parse($params['endPeriod']);
}
if(array_key_exists('currentMoment', $params)) {
$return['today'] = Carbon::parse($params['currentMoment']);
}
return $return;
}

View File

@ -0,0 +1,133 @@
<?php
/*
* SortsQueryResults.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\Support\JsonApi;
use FireflyIII\Models\Account;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use LaravelJsonApi\Core\Query\SortField;
use LaravelJsonApi\Core\Query\SortFields;
trait SortsQueryResults
{
final protected function postQuerySort(string $class, Collection $collection, SortFields $parameters): Collection
{
Log::debug(__METHOD__);
foreach ($parameters->all() as $field) {
$collection = $this->sortQueryCollection($class, $collection, $field);
}
return $collection;
}
/**
* TODO improve this.
*
* @param string $class
* @param Collection $collection
* @param SortField $field
*
* @return Collection
*/
private function sortQueryCollection(string $class, Collection $collection, SortField $field): Collection
{
// here be custom sort things.
// sort by balance
if (Account::class === $class && 'balance' === $field->name()) {
$ascending = $field->isAscending();
$collection = $collection->sort(function (Account $left, Account $right) use ($ascending): int {
$leftSum = $this->sumBalance($left->balance);
$rightSum = $this->sumBalance($right->balance);
return $ascending ? bccomp($leftSum, $rightSum) : bccomp($rightSum, $leftSum);
});
}
if (Account::class === $class && 'balance_difference' === $field->name()) {
$ascending = $field->isAscending();
$collection = $collection->sort(function (Account $left, Account $right) use ($ascending): int {
$leftSum = $this->sumBalanceDifference($left->balance);
$rightSum = $this->sumBalanceDifference($right->balance);
return $ascending ? bccomp($leftSum, $rightSum) : bccomp($rightSum, $leftSum);
});
}
// sort by account number
if (Account::class === $class && 'account_number' === $field->name()) {
$ascending = $field->isAscending();
$collection = $collection->sort(function (Account $left, Account $right) use ($ascending): int {
$leftNr = sprintf('%s%s', $left->iban, $left->account_number);
$rightNr = sprintf('%s%s', $right->iban, $right->account_number);
return $ascending ? strcmp($leftNr, $rightNr) : strcmp($rightNr, $leftNr);
});
}
// sort by last activity
if (Account::class === $class && 'last_activity' === $field->name()) {
$ascending = $field->isAscending();
$collection = $collection->sort(function (Account $left, Account $right) use ($ascending): int {
$leftNr = (int)$left->last_activity?->format('U');
$rightNr = (int)$right->last_activity?->format('U');
if($ascending){
return $leftNr <=> $rightNr;
}
return $rightNr <=> $leftNr;
//return (int) ($ascending ? $rightNr < $leftNr : $leftNr < $rightNr );
});
}
// sort by balance difference.
return $collection;
}
private function sumBalance(?array $balance): string
{
if (null === $balance) {
return '-10000000000'; // minus one billion
}
if (0 === count($balance)) {
return '-10000000000'; // minus one billion
}
$sum = '0';
foreach ($balance as $entry) {
$sum = bcadd($sum, $entry['balance']);
}
return $sum;
}
private function sumBalanceDifference(?array $balance): string
{
if (null === $balance) {
return '-10000000000'; // minus one billion
}
if (0 === count($balance)) {
return '-10000000000'; // minus one billion
}
$sum = '0';
foreach ($balance as $entry) {
$sum = bcadd($sum, $entry['balance_difference']);
}
return $sum;
}
}

View File

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

View File

@ -46,15 +46,16 @@ return [
'accounts' => ['name', 'active', 'iban', 'order', 'account_number', 'balance', 'last_activity', 'balance_difference', 'current_debt'],
],
],
// valid query columns for sorting:
// valid query columns for sorting the query
'valid_query_sort' => [
Account::class => ['id','name', 'active', 'iban', 'order'],
Account::class => ['id', 'name', 'active', 'iban', 'order'],
],
'valid_api_sort' => [
Account::class => [],
// valid query columns for sorting the query results
'valid_api_sort' => [
Account::class => ['account_number'],
],
'full_data_set' => [
'account' => ['last_activity', 'balance_difference', 'current_balance', 'current_debt'],
Account::class => ['last_activity', 'balance', 'balance_difference', 'current_debt', 'account_number'],
],
'valid_query_filters' => [
Account::class => ['id', 'name', 'iban', 'active'],

View File

@ -48,7 +48,12 @@ const params = new Proxy(new URLSearchParams(window.location.search), {
get: (searchParams, prop) => searchParams.get(prop),
});
sortingColumn = params.column ?? '';
sortDirection = params.direction ?? '';
sortDirection = 'asc';
if(sortingColumn[0] === '-') {
sortingColumn = sortingColumn.substring(1);
sortDirection = 'desc';
}
page = parseInt(params.page ?? 1);
@ -77,6 +82,7 @@ let index = function () {
filters: {
active: null,
name: null,
type: type,
},
pageOptions: {
isLoading: true,
@ -345,10 +351,11 @@ let index = function () {
//const sorting = [{column: this.pageOptions.sortingColumn, direction: this.pageOptions.sortDirection}];
// filter instructions
let filters = [];
let filters = {};
for (let k in this.filters) {
if (this.filters.hasOwnProperty(k) && null !== this.filters[k]) {
filters.push({column: k, filter: this.filters[k]});
filters[k] = this.filters[k];
//filters.push({column: k, filter: this.filters[k]});
}
}
@ -358,9 +365,9 @@ let index = function () {
const today = new Date();
let params = {
sorting: sorting,
filters: filters,
// today: today,
sort: sorting,
filter: filters,
currentMoment: today,
// type: type,
page: {number: this.page},
startPeriod: start,
@ -368,8 +375,8 @@ let index = function () {
};
if (!this.tableColumns.balance_difference.enabled) {
delete params.start;
delete params.end;
delete params.startPeriod;
delete params.enPeriod;
}
this.accounts = [];
let groupedAccounts = {};
@ -404,7 +411,6 @@ let index = function () {
balance: current.attributes.balance,
native_balance: current.attributes.native_balance,
};
// get group info:
let groupId = current.attributes.object_group_id;
if(!this.pageOptions.groupedAccounts) {

View File

@ -125,12 +125,12 @@
{{ __('list.interest') }}
</th>
<th x-show="tableColumns.number.visible && tableColumns.number.enabled">
<a href="#" x-on:click.prevent="sort('iban')">
<a href="#" x-on:click.prevent="sort('account_number')">
{{ __('list.account_number') }}
</a>
<em x-show="pageOptions.sortingColumn === 'iban' && pageOptions.sortDirection === 'asc'"
<em x-show="pageOptions.sortingColumn === 'account_number' && pageOptions.sortDirection === 'asc'"
class="fa-solid fa-arrow-down-a-z"></em>
<em x-show="pageOptions.sortingColumn === 'iban' && pageOptions.sortDirection === 'desc'"
<em x-show="pageOptions.sortingColumn === 'account_number' && pageOptions.sortDirection === 'desc'"
class="fa-solid fa-arrow-down-z-a"></em>
</th>
<th x-show="tableColumns.current_balance.visible && tableColumns.current_balance.enabled">
@ -262,13 +262,17 @@
</template>
</td>
<td x-show="tableColumns.current_balance.visible && tableColumns.current_balance.enabled">
<template x-if="null !== account.balance">
<template x-for="balance in account.balance">
<span x-show="balance.balance < 0" class="text-danger"
<span>
<span x-show="parseFloat(balance.balance) < 0.0" class="text-danger"
x-text="formatMoney(balance.balance, balance.currency_code)"></span>
<span x-show="balance.balance == 0" class="text-muted"
<span x-show="parseFloat(balance.balance) === 0.0" class="text-muted"
x-text="formatMoney(balance.balance, balance.currency_code)"></span>
<span x-show="balance.balance > 0" class="text-success"
<span x-show="parseFloat(balance.balance) > 0.0" class="text-success"
x-text="formatMoney(balance.balance, balance.currency_code)"></span>
</span>
</template>
</template>
</td>
<td x-show="tableColumns.amount_due.visible && tableColumns.amount_due.enabled">
@ -284,14 +288,17 @@
<span x-text="account.last_activity"></span>
</td>
<td x-show="tableColumns.balance_difference.visible && tableColumns.balance_difference.enabled">
<template x-if="null !== account.balance">
<template x-for="balance in account.balance">
<span>
<span x-show="null != balance.balance_difference && balance.balance_difference < 0" class="text-danger"
x-text="formatMoney(balance.balance_difference, balance.currency_code)"></span>
<span x-show="null != balance.balance_difference && balance.balance_difference == 0" class="text-muted"
x-text="formatMoney(balance.balance_difference, balance.currency_code)"></span>
<span x-show="null != balance.balance_difference && balance.balance_difference > 0" class="text-success"
x-text="formatMoney(balance.balance_difference, balance.currency_code)"></span>
</span>
</template>
</template>
</td>
<td x-show="tableColumns.menu.visible && tableColumns.menu.enabled">