This commit is contained in:
Tobe O 2019-02-01 09:39:10 -05:00
parent c2ba916f00
commit 9bfcd93a92
12 changed files with 69 additions and 246 deletions

View file

@ -0,0 +1 @@
2.1.0

Binary file not shown.

2
CHANGELOG.md Normal file
View file

@ -0,0 +1,2 @@
# 2.0.0
* Dart2 + Angel2 update.

View file

@ -1,7 +1,6 @@
# paginate # paginate
[![version 1.0.0+3](https://img.shields.io/badge/pub-v1.0.0+3-brightgreen.svg)](https://pub.dartlang.org/packages/angel_paginate) [![Pub](https://img.shields.io/pub/v/angel_paginate.svg)](https://pub.dartlang.org/packages/angel_paginate)
[![build status](https://travis-ci.org/angel-dart/paginate.svg)](https://travis-ci.org/angel-dart/paginate) [![build status](https://travis-ci.org/angel-dart/paginate.svg)](https://travis-ci.org/angel-dart/paginate)
![coverage: 100%](https://img.shields.io/badge/coverage-100%25-green.svg)
Platform-agnostic pagination library, with custom support for the Platform-agnostic pagination library, with custom support for the
[Angel framework](https://github.com/angel-dart/angel). [Angel framework](https://github.com/angel-dart/angel).
@ -11,7 +10,7 @@ In your `pubspec.yaml` file:
```yaml ```yaml
dependencies: dependencies:
angel_paginate: ^1.0.0 angel_paginate: ^2.0.0
``` ```
# Usage # Usage
@ -58,38 +57,4 @@ The entire Paginator API is documented, so check out the DartDocs.
Paginators by default cache paginations, to improve performance as you shift through pages. 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 This can be especially helpful in a client-side application where your UX involves a fast
response time, i.e. a search page. response time, i.e. a search page.
## Use With Angel
Naturally, a library named `angel_paginate` has special provisions for the
[Angel framework](https://github.com/angel-dart/angel).
In `package:angel_paginate/server.dart`, a function called `paginate` generates
pagination service hooks for you. If the result of a hooked service event is an `Iterable`,
it will be paginated. This is convenient because it works with any data store, whether it
be MongoDB, RethinkDB, an in-memory store, or something else entirely.
```dart
configureServer(Angel app) {
var service = app.service('api/foo') as HookedService;
service.afterIndexed.listen(paginate(itemsPerPage: 10));
}
```
See `test/server_test.dart` for examples of usage with Angel.
The pagination hook also allows you to provide a `page` and/or `$limit` in the query.
If the user provides a `page` in the query, it will return a pagination of the given page.
Ex. `http://mysite.com/api/todos?page=4`
A `$limit` can be used to override the `itemsPerPage` set in the `paginate` hook. If you
would like to set a maximum on the number of items per page, you can set `maxItemsPerPage`
in the `paginate` call.
Ex. `http://mysite.com/api/todos?$limit=25`
You can use these pagination functions to provide powerful search experiences on your websites.
**NOTE**: If the paginated data is empty, expect `start_index` and `end_index`
to both be `-1`.

15
example/main.dart Normal file
View file

@ -0,0 +1,15 @@
import 'package:angel_paginate/angel_paginate.dart';
main() {
var iterable = [1, 2, 3, 4];
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)
}

View file

@ -57,8 +57,8 @@ class Paginator<T> {
nextPage: _page >= last - 1 ? -1 : _page + 2, nextPage: _page >= last - 1 ? -1 : _page + 2,
startIndex: it.isEmpty ? -1 : offset, startIndex: it.isEmpty ? -1 : offset,
endIndex: offset + it.length - 1, endIndex: offset + it.length - 1,
itemsPerPage: itemsPerPage < _items.length ? itemsPerPage : _items itemsPerPage:
.length, itemsPerPage < _items.length ? itemsPerPage : _items.length,
total: len); total: len);
} }
@ -110,7 +110,7 @@ class Paginator<T> {
/// Stores the result of a pagination. /// Stores the result of a pagination.
abstract class PaginationResult<T> { abstract class PaginationResult<T> {
factory PaginationResult.fromMap(Map<String, dynamic> map) => factory PaginationResult.fromMap(Map<String, dynamic> map) =>
new _PaginationResultImpl(map['data'], new _PaginationResultImpl((map['data'] as Iterable).cast<T>(),
currentPage: map['current_page'], currentPage: map['current_page'],
endIndex: map['end_index'], endIndex: map['end_index'],
itemsPerPage: map['items_per_page'], itemsPerPage: map['items_per_page'],
@ -147,12 +147,12 @@ class _PaginationResultImpl<T> implements PaginationResult<T> {
_PaginationResultImpl(this._data, _PaginationResultImpl(this._data,
{this.currentPage, {this.currentPage,
this.endIndex, this.endIndex,
this.itemsPerPage, this.itemsPerPage,
this.nextPage, this.nextPage,
this.previousPage, this.previousPage,
this.startIndex, this.startIndex,
this.total}); this.total});
@override @override
List<T> get data => _cachedData ?? (_cachedData = new List<T>.from(_data)); List<T> get data => _cachedData ?? (_cachedData = new List<T>.from(_data));

View file

@ -1,49 +0,0 @@
import 'package:angel_framework/angel_framework.dart';
import 'angel_paginate.dart';
export 'angel_paginate.dart';
/// Paginates the results of service events.
///
/// Users can add a `page` to the query to display a certain page, i.e. `http://foo.com/api/todos?page=5`.
///
/// Users can also add a `$limit` to the query to display more or less items than specified in [itemsPerPage] (default: `5`).
/// If [maxItemsPerPage] is set, then even if the query contains a `$limit` parameter, it will be limited to the maximum.
HookedServiceEventListener paginate<T>(
{int itemsPerPage, int maxItemsPerPage}) {
return (HookedServiceEvent e) {
if (e.isBefore) throw new UnsupportedError(
'`package:angel_paginate` can only be run as an after hook.');
if (e.result is! Iterable) return;
int page = 1,
nItems = itemsPerPage;
if (e.params.containsKey('query') && e.params['query'] is Map) {
var query = e.params['query'] as Map;
if (query.containsKey('page')) {
try {
page = int.parse(query['page']?.toString());
} catch (e) {
// Fail silently...
}
}
if (query.containsKey(r'$limit')) {
try {
var lim = int.parse(query[r'$limit']?.toString());
if (lim > 0 && (maxItemsPerPage == null || lim <= maxItemsPerPage))
nItems = lim;
} catch (e) {
// Fail silently...
}
}
}
var paginator = new Paginator(
e.result, itemsPerPage: nItems)
..goToPage(page);
e.result = paginator.current;
};
}

View file

@ -1,13 +1,13 @@
name: angel_paginate name: angel_paginate
version: 1.0.0+3 version: 2.0.0
description: Platform-agnostic pagination library, with custom support for the Angel framework. description: Platform-agnostic pagination library, with custom support for the Angel framework.
author: Tobe O <thosakwe@gmail.com> author: Tobe O <thosakwe@gmail.com>
homepage: https://github.com/angel-dart/paginate homepage: https://github.com/angel-dart/paginate
environment: environment:
sdk: ">=1.19.0" sdk: ">=2.0.0-dev <3.0.0"
dependencies: dependencies:
angel_framework: ^1.0.0 angel_framework: ^2.0.0-alpha
dev_dependencies: dev_dependencies:
angel_diagnostics: ^1.0.0 angel_test: ^2.0.0
angel_test: ^1.0.0 logging: ^0.11.0
test: ^0.12.15 test: ^1.0.0

View file

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

View file

@ -1,11 +1,11 @@
import 'package:angel_client/angel_client.dart' as c; import 'dart:convert';
import 'package:angel_diagnostics/angel_diagnostics.dart';
import 'package:angel_framework/angel_framework.dart'; import 'package:angel_framework/angel_framework.dart';
import 'package:angel_paginate/server.dart'; import 'package:angel_paginate/angel_paginate.dart';
import 'package:angel_test/angel_test.dart'; import 'package:angel_test/angel_test.dart';
import 'package:logging/logging.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
final List<Map<String, String>> DATA = [ final List<Map<String, String>> mjAlbums = [
{'billie': 'jean'}, {'billie': 'jean'},
{'off': 'the_wall'}, {'off': 'the_wall'},
{'michael': 'jackson'} {'michael': 'jackson'}
@ -13,37 +13,45 @@ final List<Map<String, String>> DATA = [
main() { main() {
TestClient client; TestClient client;
c.Service songService;
setUp(() async { setUp(() async {
var app = new Angel() var app = new Angel();
..use('/api/songs', new AnonymousService(index: ([p]) async => DATA));
var service = app.service('api/songs') as HookedService; app.get('/api/songs', (req, res) {
service.afterIndexed.listen(paginate(itemsPerPage: 2)); var p = Paginator(mjAlbums, itemsPerPage: mjAlbums.length);
await app.configure(logRequests()); p.goToPage(int.parse(req.queryParameters['page'] ?? '1'));
return p.current;
});
client = await connectTo(app); client = await connectTo(app);
songService = client.service('api/songs');
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()); tearDown(() => client.close());
test('limit exceeds size of collection', () async { test('limit exceeds size of collection', () async {
var response = await songService.index({ var response = await client.get(Uri(
'query': { path: '/api/songs',
r'$limit': DATA.length + 1 queryParameters: {r'$limit': (mjAlbums.length + 1).toString()}));
}
}); var page = new PaginationResult<Map<String, dynamic>>.fromMap(
json.decode(response.body));
var page = new PaginationResult<Map<String, String>>.fromMap(response);
print('page: ${page.toJson()}'); print('page: ${page.toJson()}');
expect(page.total, DATA.length); expect(page.total, mjAlbums.length);
expect(page.itemsPerPage, DATA.length); expect(page.itemsPerPage, mjAlbums.length);
expect(page.previousPage, -1); expect(page.previousPage, -1);
expect(page.currentPage, 1); expect(page.currentPage, 1);
expect(page.nextPage, -1); expect(page.nextPage, -1);
expect(page.startIndex, 0); expect(page.startIndex, 0);
expect(page.endIndex, DATA.length - 1); expect(page.endIndex, mjAlbums.length - 1);
expect(page.data, DATA); expect(page.data, mjAlbums);
}); });
} }

View file

@ -2,8 +2,7 @@ import 'package:angel_paginate/angel_paginate.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
// Count-down from 100, then 101 at the end... // Count-down from 100, then 101 at the end...
final List<int> DATA = new List<int>.generate(100, (i) => 100 - i) final List<int> DATA = new List<int>.generate(100, (i) => 100 - i)..add(101);
..add(101);
main() { main() {
group('cache', () { group('cache', () {
@ -68,8 +67,7 @@ main() {
}); });
test('third page', () { test('third page', () {
var paginator = new Paginator<int>(DATA) var paginator = new Paginator<int>(DATA)..goToPage(3);
..goToPage(3);
expect(paginator.pageNumber, 3); expect(paginator.pageNumber, 3);
var r = paginator.current; var r = paginator.current;
print(r.toJson()); print(r.toJson());
@ -108,7 +106,7 @@ main() {
do { do {
print(' * Page #${paginator.pageNumber}: ${paginator.current.data}'); print(' * Page #${paginator.pageNumber}: ${paginator.current.data}');
paginator.next(); paginator.next();
} while(paginator.canGoForward); } while (paginator.canGoForward);
}); });
test('empty collection', () { test('empty collection', () {

View file

@ -1,115 +0,0 @@
import 'package:angel_client/angel_client.dart' as c;
import 'package:angel_diagnostics/angel_diagnostics.dart';
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_paginate/server.dart';
import 'package:angel_test/angel_test.dart';
import 'package:test/test.dart';
final List DATA = new List.filled(50, {'foo': 'bar'}, growable: true)
..addAll(new List.filled(25, {'bar': 'baz'}));
main() {
group('no max', () {
Angel app;
TestClient client;
c.Service dataService;
setUp(() async {
app = new Angel()
..use('/data', new AnonymousService(index: ([p]) async => DATA));
await app.configure(logRequests());
var service = app.service('data') as HookedService;
service.afterIndexed.listen(paginate(itemsPerPage: 10));
client = await connectTo(app);
dataService = client.service('data');
});
tearDown(() => client.close());
test('first page', () async {
var response = await dataService
.index()
.then((m) => new PaginationResult.fromMap(m));
print(response.toJson());
expect(response.total, DATA.length);
expect(response.itemsPerPage, 10);
expect(response.startIndex, 0);
expect(response.endIndex, 9);
expect(response.previousPage, -1);
expect(response.currentPage, 1);
expect(response.nextPage, 2);
expect(response.data, DATA.take(10).toList());
});
test('third page', () async {
var response = await dataService.index({
'query': {'page': 3}
}).then((m) => new PaginationResult.fromMap(m));
print(response.toJson());
expect(response.total, DATA.length);
expect(response.itemsPerPage, 10);
expect(response.startIndex, 20);
expect(response.endIndex, 29);
expect(response.previousPage, 2);
expect(response.currentPage, 3);
expect(response.nextPage, 4);
expect(response.data, DATA.skip(20).take(10).toList());
});
test('custom limit', () async {
var response = await dataService.index({
'query': {'page': 4, r'$limit': 5}
}).then((m) => new PaginationResult.fromMap(m));
print(response.toJson());
expect(response.total, DATA.length);
expect(response.itemsPerPage, 5);
expect(response.startIndex, 15);
expect(response.endIndex, 19);
expect(response.previousPage, 3);
expect(response.currentPage, 4);
expect(response.nextPage, 5);
expect(response.data, DATA.skip(15).take(5).toList());
});
});
group('max 15', () {
Angel app;
TestClient client;
c.Service dataService;
setUp(() async {
app = new Angel()
..use('/data', new AnonymousService(index: ([p]) async => DATA));
await app.configure(logRequests());
var service = app.service('data') as HookedService;
service.afterIndexed.listen(
paginate(itemsPerPage: 10, maxItemsPerPage: 15));
client = await connectTo(app);
dataService = client.service('data');
});
tearDown(() => client.close());
test('exceed max', () async {
var response = await dataService.index({
'query': {'page': 4, r'$limit': 30}
}).then((m) => new PaginationResult.fromMap(m));
print(response.toJson());
// Should default to 10 items per page :)
expect(response.total, DATA.length);
expect(response.itemsPerPage, 10);
expect(response.startIndex, 30);
expect(response.endIndex, 39);
expect(response.previousPage, 3);
expect(response.currentPage, 4);
expect(response.nextPage, 5);
expect(response.data, DATA.skip(30).take(10).toList());
});
});
}