1.0.0
This commit is contained in:
parent
74f50141c3
commit
c8a3cd9d67
10 changed files with 408 additions and 0 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -25,3 +25,5 @@ doc/api/
|
|||
# Don't commit pubspec lock file
|
||||
# (Library packages only! Remove pattern if developing an application package)
|
||||
pubspec.lock
|
||||
|
||||
rethinkdb_data/
|
4
.travis.yml
Normal file
4
.travis.yml
Normal file
|
@ -0,0 +1,4 @@
|
|||
language: dart
|
||||
addons:
|
||||
rethinkdb: '2.3'
|
||||
before_script: 'dart test/bootstrap.dart'
|
84
README.md
84
README.md
|
@ -1,2 +1,86 @@
|
|||
# rethink
|
||||
|
||||
[![version 1.0.0](https://img.shields.io/badge/pub-1.0.0-brightgreen.svg)](https://pub.dartlang.org/packages/angel_rethink)
|
||||
[![build status](https://travis-ci.org/angel-dart/rethink.svg?branch=master)](https://travis-ci.org/angel-dart/rethink)
|
||||
|
||||
RethinkDB-enabled services for the Angel framework.
|
||||
|
||||
# Installation
|
||||
Add the following to your `pubspec.yaml`:
|
||||
|
||||
```yaml
|
||||
dependencies:
|
||||
angel_rethink: ^1.0.0
|
||||
```
|
||||
|
||||
# Usage
|
||||
This library exposes one main class: `RethinkService`. By default, these services will even
|
||||
listen to [changefeeds](https://www.rethinkdb.com/docs/changefeeds/ruby/) from the database,
|
||||
which makes them very suitable for WebSocket use.
|
||||
|
||||
However, only `CREATED`, `UPDATED` and `REMOVED` events will be fired. This is technically not
|
||||
a problem, as it lowers the numbers of events you have to handle on the client side. ;)
|
||||
|
||||
## Model
|
||||
`Model` is class with no real functionality; however, it represents a basic document, and your services should host inherited classes.
|
||||
Other Angel service providers host `Model` as well, so you will easily be able to modify your application if you ever switch databases.
|
||||
|
||||
```dart
|
||||
class User extends Model {
|
||||
String username;
|
||||
String password;
|
||||
}
|
||||
|
||||
main() async {
|
||||
var r = new RethinkDb();
|
||||
var conn = await r.connect();
|
||||
|
||||
app.use('/api/users', new RethinkService(conn, r.table('users')));
|
||||
|
||||
// Add type de/serialization if you want
|
||||
app.use('/api/users', new TypedService<User>(new RethinkService(conn, r.table('users'))));
|
||||
|
||||
// You don't have to even use a table...
|
||||
app.use('/api/pro_users', new RethinkService(conn, r.table('users').filter({'membership': 'pro'})));
|
||||
|
||||
app.service('api/users').afterCreated.listen((event) {
|
||||
print("New user: ${event.result}");
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## RethinkService
|
||||
This class interacts with a `Table` (from `package:rethinkdb_driver`) and serializes data to and from Maps.
|
||||
|
||||
## RethinkTypedService<T>
|
||||
Does the same as above, but serializes to and from a target class using `package:json_god` and its support for reflection.
|
||||
|
||||
## Querying
|
||||
You can query these services as follows:
|
||||
|
||||
/path/to/service?foo=bar
|
||||
|
||||
The above will query the database to find records where 'foo' equals 'bar'.
|
||||
|
||||
The former will sort result in ascending order of creation, and so will the latter.
|
||||
|
||||
You can use advanced queries:
|
||||
|
||||
```dart
|
||||
// Pass an actual query...
|
||||
service.index({'query': r.table('foo').filter(...)});
|
||||
|
||||
// Or, a function that creates a query from a table...
|
||||
service.index({'query': (table) => table.getAll('foo')});
|
||||
|
||||
// Or, a Map, which will be transformed into a `filter` query:
|
||||
service.index({'query': {'foo': 'bar', 'baz': 'quux'}});
|
||||
```
|
||||
|
||||
You can also apply sorting by adding a `reql` parameter on the server-side.
|
||||
|
||||
```dart
|
||||
service.index({'reql': (query) => query.sort(...)});
|
||||
```
|
||||
|
||||
See the tests for more usage examples.
|
||||
|
|
1
lib/angel_rethink.dart
Normal file
1
lib/angel_rethink.dart
Normal file
|
@ -0,0 +1 @@
|
|||
export 'src/rethink_service.dart';
|
194
lib/src/rethink_service.dart
Normal file
194
lib/src/rethink_service.dart
Normal file
|
@ -0,0 +1,194 @@
|
|||
import 'dart:async';
|
||||
import 'package:angel_framework/angel_framework.dart';
|
||||
import 'package:json_god/json_god.dart' as god;
|
||||
import 'package:rethinkdb_driver/rethinkdb_driver.dart';
|
||||
|
||||
// Extends a RethinkDB query.
|
||||
typedef RqlQuery QueryCallback(RqlQuery query);
|
||||
|
||||
/// Queries a single RethinkDB table or query.
|
||||
class RethinkService extends Service {
|
||||
/// If set to `true`, clients can remove all items by passing a `null` `id` to `remove`.
|
||||
///
|
||||
/// `false` by default.
|
||||
final bool allowRemoveAll;
|
||||
|
||||
/// If set to `true`, parameters in `req.query` are applied to the database query.
|
||||
final bool allowQuery;
|
||||
|
||||
final bool debug;
|
||||
|
||||
/// If set to `true`, then a HookedService mounted over this instance
|
||||
/// will fire events when RethinkDB pushes events.
|
||||
///
|
||||
/// Good for scaling. ;)
|
||||
final bool listenForChanges;
|
||||
|
||||
final Connection connection;
|
||||
|
||||
/// Doesn't actually have to be a table, just a RethinkDB query.
|
||||
///
|
||||
/// However, a table is the most common usecase.
|
||||
final RqlQuery table;
|
||||
|
||||
RethinkService(this.connection, this.table,
|
||||
{this.allowRemoveAll: false,
|
||||
this.allowQuery: true,
|
||||
this.debug: false,
|
||||
this.listenForChanges: true})
|
||||
: super() {}
|
||||
|
||||
RqlQuery _getQuery(RqlQuery query, Map params) {
|
||||
if (params != null)
|
||||
params['broadcast'] =
|
||||
params.containsKey('broadcast') ? params['broadcast'] : false;
|
||||
|
||||
var q = _getQueryInner(query, params);
|
||||
|
||||
if (params?.containsKey('reql') == true && params['reql'] is QueryCallback)
|
||||
q = params['reql'](q);
|
||||
|
||||
return q ?? query;
|
||||
}
|
||||
|
||||
RqlQuery _getQueryInner(RqlQuery query, Map params) {
|
||||
if (params == null || !params.containsKey('query'))
|
||||
return null;
|
||||
else {
|
||||
if (params['query'] is RqlQuery)
|
||||
return params['query'];
|
||||
else if (params['query'] is QueryCallback)
|
||||
return params['query'](table);
|
||||
else if (params['query'] is! Map || allowQuery != true)
|
||||
return query;
|
||||
else {
|
||||
Map q = params['query'];
|
||||
return q.keys.map((k) => k.toString()).fold<RqlQuery>(query,
|
||||
(out, key) {
|
||||
var val = q[key];
|
||||
|
||||
if (val is RequestContext ||
|
||||
val is ResponseContext ||
|
||||
key == 'provider' ||
|
||||
val is Providers)
|
||||
return out;
|
||||
else {
|
||||
return out.filter({k.toString(): val});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future _sendQuery(RqlQuery query) async {
|
||||
var result = await query.run(connection);
|
||||
|
||||
if (result is Cursor)
|
||||
return await result.toList();
|
||||
else if (result is Map && result['generated_keys'] is List) {
|
||||
if (result['generated_keys'].length == 1)
|
||||
return await read(result['generated_keys'].first);
|
||||
return await Future.wait(result['generated_keys'].map(read));
|
||||
} else
|
||||
return result;
|
||||
}
|
||||
|
||||
Map _serialize(data) {
|
||||
if (data is Map)
|
||||
return data;
|
||||
else
|
||||
return god.serializeObject(data);
|
||||
}
|
||||
|
||||
Map _squeeze(Map data) {
|
||||
return data.keys.fold<Map>({}, (map, k) => map..[k.toString()] = data[k]);
|
||||
}
|
||||
|
||||
void onHooked(HookedService hookedService) {
|
||||
if (listenForChanges == true) {
|
||||
listenToQuery(table, hookedService);
|
||||
}
|
||||
}
|
||||
|
||||
Future listenToQuery(RqlQuery query, HookedService hookedService) async {
|
||||
Feed feed = await query.changes({'include_types': true}).run(connection);
|
||||
|
||||
feed.listen((Map event) {
|
||||
String type = event['type']?.toString();
|
||||
var newVal = event['new_val'], oldVal = event['old_val'];
|
||||
|
||||
if (type == 'add') {
|
||||
// Create
|
||||
hookedService.fire(HookedServiceEvent.CREATED, newVal);
|
||||
} else if (type == 'change') {
|
||||
// Update
|
||||
hookedService.fire(HookedServiceEvent.UPDATED, newVal, (e) {
|
||||
e
|
||||
..id = oldVal['id']
|
||||
..data = newVal;
|
||||
});
|
||||
} else if (type == 'remove') {
|
||||
// Remove
|
||||
hookedService.fire(HookedServiceEvent.CREATED, oldVal, (e) {
|
||||
e.id = oldVal['id'];
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future index([Map params]) async {
|
||||
var query = _getQuery(table, params);
|
||||
return await _sendQuery(query);
|
||||
}
|
||||
|
||||
@override
|
||||
Future read(id, [Map params]) async {
|
||||
var query = _getQuery(table.get(id?.toString()), params);
|
||||
var found = await _sendQuery(query);
|
||||
print('Found for $id: $found');
|
||||
|
||||
if (found == null) {
|
||||
throw new AngelHttpException.notFound(
|
||||
message: 'No record found for ID $id');
|
||||
} else
|
||||
return found;
|
||||
}
|
||||
|
||||
@override
|
||||
Future create(data, [Map params]) async {
|
||||
if (table is! Table)
|
||||
throw new StateError(
|
||||
'RethinkServices can only create data within tables.');
|
||||
|
||||
var d = _serialize(data);
|
||||
var q = table as Table;
|
||||
var query = _getQuery(q.insert(_squeeze(d)), params);
|
||||
return await _sendQuery(query);
|
||||
}
|
||||
|
||||
@override
|
||||
Future modify(id, data, [Map params]) => update(id, data, params);
|
||||
|
||||
@override
|
||||
Future update(id, data, [Map params]) async {
|
||||
var query = _getQuery(table.get(id?.toString()), params).update(data);
|
||||
await _sendQuery(query);
|
||||
return await read(id, params);
|
||||
}
|
||||
|
||||
@override
|
||||
Future remove(id, [Map params]) async {
|
||||
if (id == null ||
|
||||
id == 'null' &&
|
||||
(allowRemoveAll == true ||
|
||||
params?.containsKey('provider') != true)) {
|
||||
return await _sendQuery(table.delete());
|
||||
} else {
|
||||
var prior = await read(id, params);
|
||||
var query = _getQuery(table.get(id), params).delete();
|
||||
await _sendQuery(query);
|
||||
return prior;
|
||||
}
|
||||
}
|
||||
}
|
13
pubspec.yaml
Normal file
13
pubspec.yaml
Normal file
|
@ -0,0 +1,13 @@
|
|||
name: angel_rethink
|
||||
version: 1.0.0
|
||||
description: RethinkDB-enabled services for the Angel framework.
|
||||
author: Tobe O <thosakwe@gmail.com>
|
||||
environment:
|
||||
sdk: ">=1.19.0"
|
||||
homepage: https://github.com/angel-dart/rethink
|
||||
dependencies:
|
||||
angel_framework: ^1.0.0-dev
|
||||
rethinkdb_driver: ^2.3.1
|
||||
dev_dependencies:
|
||||
angel_test: ^1.0.0-dev
|
||||
test: ^0.12.0
|
6
test/README.md
Normal file
6
test/README.md
Normal file
|
@ -0,0 +1,6 @@
|
|||
# Tests
|
||||
|
||||
The tests expect you to have installed RethinkDB. You must have a `test` database
|
||||
available, and a server ready at the default port.
|
||||
|
||||
Also, the tests expect a table named `todos`.
|
7
test/bootstrap.dart
Normal file
7
test/bootstrap.dart
Normal file
|
@ -0,0 +1,7 @@
|
|||
import 'package:rethinkdb_driver/rethinkdb_driver.dart';
|
||||
|
||||
main() async {
|
||||
var r = new Rethinkdb();
|
||||
var conn = await r.connect();
|
||||
await r.tableCreate('todos').run(conn);
|
||||
}
|
10
test/common.dart
Normal file
10
test/common.dart
Normal file
|
@ -0,0 +1,10 @@
|
|||
class Todo {
|
||||
String title;
|
||||
bool completed;
|
||||
|
||||
Todo({this.title, this.completed: false});
|
||||
|
||||
Map toJson() {
|
||||
return {'title': title, 'completed': completed == true};
|
||||
}
|
||||
}
|
87
test/generic_test.dart
Normal file
87
test/generic_test.dart
Normal file
|
@ -0,0 +1,87 @@
|
|||
import 'package:angel_client/angel_client.dart' as c;
|
||||
import 'package:angel_framework/angel_framework.dart';
|
||||
import 'package:angel_rethink/angel_rethink.dart';
|
||||
import 'package:angel_test/angel_test.dart';
|
||||
import 'package:rethinkdb_driver/rethinkdb_driver.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'common.dart';
|
||||
|
||||
main() {
|
||||
Angel app;
|
||||
TestClient client;
|
||||
Rethinkdb r;
|
||||
c.Service todoService;
|
||||
|
||||
setUp(() async {
|
||||
r = new Rethinkdb();
|
||||
var conn = await r.connect();
|
||||
|
||||
app = new Angel();
|
||||
app.use('/todos', new RethinkService(conn, r.table('todos')));
|
||||
|
||||
app.onError((e, req, res) async {
|
||||
print('Whoops: $e');
|
||||
});
|
||||
|
||||
app.fatalErrorStream.listen((e) {
|
||||
print('Whoops: ${e.error}');
|
||||
print(e.stack);
|
||||
});
|
||||
|
||||
client = await connectTo(app);
|
||||
todoService = client.service('todos');
|
||||
});
|
||||
|
||||
tearDown(() => client.close());
|
||||
|
||||
test('index', () async {
|
||||
var result = await todoService.index();
|
||||
print('Response: $result');
|
||||
expect(result, isList);
|
||||
});
|
||||
|
||||
test('create+read', () async {
|
||||
var todo = new Todo(title: 'Clean your room');
|
||||
var creation = await todoService.create(todo.toJson());
|
||||
print('Creation: $creation');
|
||||
|
||||
var id = creation['id'];
|
||||
var result = await todoService.read(id);
|
||||
|
||||
print('Response: $result');
|
||||
expect(result, isMap);
|
||||
expect(result['id'], equals(id));
|
||||
expect(result['title'], equals(todo.title));
|
||||
expect(result['completed'], equals(todo.completed));
|
||||
});
|
||||
|
||||
test('update', () async {
|
||||
var todo = new Todo(title: 'Clean your room');
|
||||
var creation = await todoService.create(todo.toJson());
|
||||
print('Creation: $creation');
|
||||
|
||||
var id = creation['id'];
|
||||
var result = await todoService.update(id, {'title': 'Eat healthy'});
|
||||
|
||||
print('Response: $result');
|
||||
expect(result, isMap);
|
||||
expect(result['id'], equals(id));
|
||||
expect(result['title'], equals('Eat healthy'));
|
||||
expect(result['completed'], equals(todo.completed));
|
||||
});
|
||||
|
||||
test('remove', () async {
|
||||
var todo = new Todo(title: 'Clean your room');
|
||||
var creation = await todoService.create(todo.toJson());
|
||||
print('Creation: $creation');
|
||||
|
||||
var id = creation['id'];
|
||||
var result = await todoService.remove(id);
|
||||
|
||||
print('Response: $result');
|
||||
expect(result, isMap);
|
||||
expect(result['id'], equals(id));
|
||||
expect(result['title'], equals(todo.title));
|
||||
expect(result['completed'], equals(todo.completed));
|
||||
});
|
||||
}
|
Loading…
Reference in a new issue