add: adding pagination package

This commit is contained in:
Patrick Stewart 2024-12-15 14:34:50 -07:00
parent fd9e5c32c5
commit 56743e5b56
13 changed files with 648 additions and 0 deletions

View file

@ -0,0 +1,2 @@
analyzer:
strong-mode: true

58
packages/pagination/.gitignore vendored Normal file
View file

@ -0,0 +1,58 @@
# See https://www.dartlang.org/tools/private-files.html
# Files and directories created by pub
.packages
.pub/
build/
# If you're building an application, you may want to check-in your pubspec.lock
pubspec.lock
# Directory created by dartdoc
# If you don't generate documentation locally you can remove this line.
doc/api/
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff:
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/dictionaries
# Sensitive or high-churn files:
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.xml
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
# Gradle:
.idea/**/gradle.xml
.idea/**/libraries
# Mongo Explorer plugin:
.idea/**/mongoSettings.xml
## File-based project format:
*.iws
## Plugin-specific files:
# IntelliJ
/out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
.dart_tool

View file

@ -0,0 +1,12 @@
Primary Authors
===============
* __[Thomas Hii](dukefirehawk.apps@gmail.com)__
Thomas is the current maintainer of the code base. He has refactored and migrated the
code base to support NNBD.
* __[Tobe O](thosakwe@gmail.com)__
Tobe has written much of the original code prior to NNBD migration. He has moved on and
is no longer involved with the project.

View file

@ -0,0 +1,49 @@
# Change Log
## 8.2.0
* Require Dart >= 3.3
* Updated `lints` to 4.0.0
## 8.1.0
* Updated repository link
* Updated `lints` to 3.0.0
* Fixed linter warnings
## 8.0.0
* Require Dart >= 3.0
## 7.0.0
* Require Dart >= 2.17
## 6.0.0
* Require Dart >= 2.16
## 5.0.0
* Skipped release
## 4.0.0
* Skipped release
## 3.1.0
* Updated linter to `package:lints`
## 3.0.1
* Updated README
## 3.0.0
* Migrated to support Dart >= 2.12 NNBD
## 2.0.0
* Dart2 + Angel2 update.

View file

@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2021, dukefirehawk.com
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View file

@ -0,0 +1,59 @@
# Angel3 Paginate
![Pub Version (including pre-releases)](https://img.shields.io/pub/v/platform_pagination?include_prereleases)
[![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety)
[![Discord](https://img.shields.io/discord/1060322353214660698)](https://discord.gg/3X6bxTUdCM)
[![License](https://img.shields.io/github/license/dart-backend/angel)](https://github.com/dart-backend/angel/tree/master/packages/paginate/LICENSE)
Platform-agnostic pagination library, with custom support for the [Angel3](https://angel3-framework.web.app/).
## Installation
In your `pubspec.yaml` file:
```yaml
dependencies:
platform_pagination: ^6.0.0
```
## Usage
This library exports a `Paginator<T>`, which can be used to efficiently produce instances of `PaginationResult<T>`. Pagination results, when serialized to JSON, look like this:
```json
{
"total" : 75,
"items_per_page" : 10,
"previous_page" : 3,
"current_page" : 4,
"next_page" : 5,
"start_index" : 30,
"end_index" : 39,
"data" : ["<items...>"]
}
```
Results can be parsed from Maps using the `PaginationResult<T>.fromMap` constructor, and serialized via their `toJson()` method.
To create a paginator:
```dart
import 'package:platform_pagination/platform_pagination.dart';
void main() {
var p = new Paginator(iterable);
// Get the current page (default: page 1)
var page = p.current;
print(page.total);
print(page.startIndex);
print(page.data); // The actual items on this page.
p.next(); // Advance a page
p.back(); // Back one page
p.goToPage(10); // Go to page number (1-based, not a 0-based index)
}
```
The entire Paginator API is documented, so check out the DartDocs.
Paginators by default cache paginations, to improve performance as you shift through pages. This can be especially helpful in a client-side application where your UX involves a fast response time, i.e. a search page.

View file

@ -0,0 +1 @@
include: package:lints/recommended.yaml

View file

@ -0,0 +1,15 @@
import 'package:platform_pagination/platform_pagination.dart';
void main() {
var iterable = [1, 2, 3, 4];
var p = Paginator(iterable);
// Get the current page (default: page 1)
var page = p.current!;
print(page.total);
print(page.startIndex);
print(page.data); // The actual items on this page.
p.next(); // Advance a page
p.back(); // Back one page
p.goToPage(10); // Go to page number (1-based, not a 0-based index)
}

View file

@ -0,0 +1,194 @@
/// Efficiently paginates collections of items in an object-oriented manner.
class Paginator<T> {
final Map<int, PaginationResult<T>> _cache = {};
PaginationResult<T>? _current;
int _page = 0;
/// The collection of items to be paginated.
final Iterable<T> _items;
/// The maximum number of items to be shown per page.
final int itemsPerPage;
/// If `true` (default), then the results of paginations will be saved by page number.
///
/// For example, you would only have to paginate page 1 once. Future calls would return a cached version.
final bool useCache;
Paginator(this._items, {this.itemsPerPage = 5, this.useCache = true});
/// Returns `true` if there are more items at lesser page indices than the current one.
bool get canGoBack => _page > 0;
/// Returns `true` if there are more items at greater page indices than the current one.
bool get canGoForward => _page < _lastPage();
/// The current page index.
int get index => _page;
/// Returns the greatest possible page number for this collection, given the number of [itemsPerPage].
int get lastPageNumber => _lastPage();
/// The current page number. This is not the same as [index].
///
/// This getter will return user-friendly numbers. The lowest value it will ever return is `1`.
int get pageNumber => _page < 1 ? 1 : (_page + 1);
/// Fetches the current page. This will be cached until [back] or [next] is called.
///
/// If [useCache] is `true` (default), then computations will be cached even after the page changes.
PaginationResult<T>? get current {
if (_current != null) {
return _current;
} else {
return _current = _getPage();
}
}
PaginationResult<T> _computePage() {
var len = _items.length;
//var it = _items.skip(_page * (itemsPerPage ?? 5));
var it = _items.skip(_page * (itemsPerPage));
var offset = len - it.length;
it = it.take(itemsPerPage);
var last = _lastPage();
// print('cur: $_page, last: $last');
return _PaginationResultImpl(it,
currentPage: _page + 1,
previousPage: _page <= 0 ? -1 : _page,
nextPage: _page >= last - 1 ? -1 : _page + 2,
startIndex: it.isEmpty ? -1 : offset,
endIndex: offset + it.length - 1,
itemsPerPage:
itemsPerPage < _items.length ? itemsPerPage : _items.length,
total: len);
}
PaginationResult<T> _getPage() {
if (useCache != false) {
return _cache.putIfAbsent(_page, () => _computePage());
} else {
return _computePage();
}
}
int _lastPage() {
var n = (_items.length / itemsPerPage).round();
// print('items: ${_items.length}');
// print('per page: $itemsPerPage');
// print('quotient: $n');
var remainder = _items.length - (n * itemsPerPage);
// print('remainder: $remainder');
return (remainder <= 0) ? n : n + 1;
}
/// Attempts to go the specified page. If it fails, then it will remain on the current page.
///
/// Keep in mind - this just not be a zero-based index, but a one-based page number. The lowest
/// allowed value is `1`.
void goToPage(int page) {
if (page > 0 && page <= _lastPage()) {
_page = page - 1;
_current = null;
}
}
/// Moves the paginator back one page, if possible.
void back() {
if (_page > 0) {
_page--;
_current = null;
}
}
/// Advances the paginator one page, if possible.
void next() {
if (_page < _lastPage()) {
_page++;
_current = null;
}
}
}
/// Stores the result of a pagination.
abstract class PaginationResult<T> {
factory PaginationResult.fromMap(Map<String, dynamic> map) =>
_PaginationResultImpl((map['data'] as List).cast<T>(),
currentPage: map['current_page'] as int?,
endIndex: map['end_index'] as int?,
itemsPerPage: map['items_per_page'] as int?,
nextPage: map['next_page'] as int?,
previousPage: map['previous_page'] as int?,
startIndex: map['start_index'] as int?,
total: map['total'] as int?);
Iterable<T> get data;
int? get currentPage;
int? get previousPage;
int? get nextPage;
int? get itemsPerPage;
int? get total;
int? get startIndex;
int? get endIndex;
Map<String, dynamic> toJson();
}
class _PaginationResultImpl<T> implements PaginationResult<T> {
final Iterable<T> _data;
Iterable<T>? _cachedData;
@override
final int? currentPage;
_PaginationResultImpl(this._data,
{this.currentPage,
this.endIndex,
this.itemsPerPage,
this.nextPage,
this.previousPage,
this.startIndex,
this.total});
@override
Iterable<T> get data => _cachedData ?? (_cachedData = List<T>.from(_data));
@override
final int? endIndex;
@override
final int? itemsPerPage;
@override
final int? nextPage;
@override
final int? previousPage;
@override
final int? startIndex;
@override
final int? total;
@override
Map<String, dynamic> toJson() {
return {
'total': total,
'items_per_page': itemsPerPage,
'previous_page': previousPage,
'current_page': currentPage,
'next_page': nextPage,
'start_index': startIndex,
'end_index': endIndex,
'data': data
};
}
}

View file

@ -0,0 +1,37 @@
name: platform_pagination
version: 8.2.0
description: Platform-agnostic pagination library, with custom support for the Angel3 framework.
homepage: https://angel3-framework.web.app/
repository: https://github.com/dart-backend/angel/tree/master/packages/paginate
environment:
sdk: '>=3.3.0 <4.0.0'
dependencies:
platform_foundation: ^8.0.0
dev_dependencies:
platform_testing: ^8.0.0
logging: ^1.2.0
test: ^1.24.0
lints: ^4.0.0
# dependency_overrides:
# angel3_framework:
# path: ../framework
# angel3_test:
# path: ../test
# angel3_container:
# path: ../container/angel_container
# angel3_http_exception:
# path: ../http_exception
# angel3_model:
# path: ../model
# angel3_route:
# path: ../route
# angel3_mock_request:
# path: ../mock_request
# angel3_websocket:
# path: ../websocket
# angel3_client:
# path: ../client
# angel3_auth:
# path: ../auth
# angel3_validate:
# path: ../validate

View file

@ -0,0 +1,8 @@
import 'package:test/test.dart';
import 'bounds_test.dart' as bounds;
import 'paginate_test.dart' as paginate;
void main() {
group('bounds', bounds.main);
group('paginate', paginate.main);
}

View file

@ -0,0 +1,57 @@
import 'dart:convert';
import 'package:platform_foundation/core.dart';
import 'package:platform_pagination/platform_pagination.dart';
import 'package:platform_testing/testing.dart';
import 'package:logging/logging.dart';
import 'package:test/test.dart';
final List<Map<String, String>> mjAlbums = [
{'billie': 'jean'},
{'off': 'the_wall'},
{'michael': 'jackson'}
];
void main() {
late TestClient client;
setUp(() async {
var app = Application();
app.get('/api/songs', (req, res) {
var p = Paginator(mjAlbums, itemsPerPage: mjAlbums.length);
p.goToPage(int.parse(req.queryParameters['page'] as String? ?? '1'));
return p.current;
});
client = await connectTo(app);
app.logger = Logger('angel_paginate')
..onRecord.listen((rec) {
print(rec);
if (rec.error != null) print(rec.error);
if (rec.stackTrace != null) print(rec.stackTrace);
});
});
tearDown(() => client.close());
test('limit exceeds size of collection', () async {
var response = await client.get(Uri(
path: '/api/songs',
queryParameters: {r'$limit': (mjAlbums.length + 1).toString()}));
var page = PaginationResult<Map<String, dynamic>>.fromMap(
json.decode(response.body) as Map<String, dynamic>);
print('page: ${page.toJson()}');
expect(page.total, mjAlbums.length);
expect(page.itemsPerPage, mjAlbums.length);
expect(page.previousPage, -1);
expect(page.currentPage, 1);
expect(page.nextPage, -1);
expect(page.startIndex, 0);
expect(page.endIndex, mjAlbums.length - 1);
expect(page.data, mjAlbums);
});
}

View file

@ -0,0 +1,127 @@
import 'package:platform_pagination/platform_pagination.dart';
import 'package:test/test.dart';
// Count-down from 100, then 101 at the end...
final List<int> data = List<int>.generate(100, (i) => 100 - i)..add(101);
void main() {
group('cache', () {
var cached = Paginator<int>(data),
uncached = Paginator<int>(data, useCache: false);
test('always cache current', () {
expect(cached.current, cached.current);
expect(uncached.current, uncached.current);
});
test('only cache prev/next if useCache != false', () {
var cached1 = cached.current;
cached.goToPage(4);
var cached4 = cached.current;
cached.goToPage(1);
expect(cached.current, cached1);
cached.goToPage(4);
expect(cached.current, cached4);
var uncached1 = uncached.current;
uncached.goToPage(4);
var uncached4 = uncached.current;
uncached.goToPage(1);
expect(uncached.current, isNot(uncached1));
uncached.goToPage(4);
expect(uncached.current, isNot(uncached4));
});
});
test('default state', () {
var paginator = Paginator<int>(data);
expect(paginator.index, 0);
expect(paginator.pageNumber, 1);
expect(paginator.itemsPerPage, 5);
expect(paginator.useCache, true);
expect(paginator.canGoBack, false);
expect(paginator.canGoForward, true);
expect(paginator.lastPageNumber, 21);
// Going back should do nothing
var p = paginator.pageNumber;
paginator.back();
expect(paginator.pageNumber, p);
});
group('paginate', () {
test('first page', () {
var paginator = Paginator<int>(data);
expect(paginator.pageNumber, 1);
var r = paginator.current!;
print(r.toJson());
expect(r.total, data.length);
expect(r.itemsPerPage, 5);
expect(r.previousPage, -1);
expect(r.currentPage, 1);
expect(r.nextPage, 2);
expect(r.startIndex, 0);
expect(r.endIndex, 4);
expect(r.data, data.skip(r.startIndex!).take(r.itemsPerPage!).toList());
});
});
test('third page', () {
var paginator = Paginator<int>(data)..goToPage(3);
expect(paginator.pageNumber, 3);
var r = paginator.current!;
print(r.toJson());
expect(r.total, data.length);
expect(r.itemsPerPage, 5);
expect(r.previousPage, 2);
expect(r.currentPage, 3);
expect(r.nextPage, 4);
expect(r.startIndex, 10);
expect(r.endIndex, 14);
expect(r.data, data.skip(r.startIndex!).take(r.itemsPerPage!).toList());
paginator.back();
expect(paginator.pageNumber, 2);
});
test('last page', () {
var paginator = Paginator<int>(data);
paginator.goToPage(paginator.lastPageNumber);
var r = paginator.current!;
expect(r.total, data.length);
expect(r.itemsPerPage, 5);
expect(r.previousPage, paginator.lastPageNumber - 1);
expect(r.currentPage, paginator.lastPageNumber);
expect(r.nextPage, -1);
expect(r.startIndex, (paginator.lastPageNumber - 1) * 5);
expect(r.endIndex, r.startIndex);
expect(r.data, [data.last]);
expect(r.data, data.skip(r.startIndex!).take(r.itemsPerPage!).toList());
});
test('dump pages', () {
var paginator = Paginator<int>(data);
print('${paginator.lastPageNumber} page(s) of data:');
do {
print(' * Page #${paginator.pageNumber}: ${paginator.current!.data}');
paginator.next();
} while (paginator.canGoForward);
});
test('empty collection', () {
var paginator = Paginator([]);
var page = paginator.current!;
print(page.toJson());
expect(page.total, 0);
expect(page.previousPage, -1);
expect(page.nextPage, -1);
expect(page.currentPage, 1);
expect(page.startIndex, -1);
expect(page.endIndex, -1);
expect(page.data, isEmpty);
expect(paginator.canGoBack, isFalse);
expect(paginator.canGoForward, isFalse);
});
}