Add 'packages/rethink/' from commit '6cc85e04095bf1665bf18d1f626930f0e61c0cb2'
git-subtree-dir: packages/rethink git-subtree-mainline:ed10592b5d
git-subtree-split:6cc85e0409
This commit is contained in:
commit
61c716502b
12 changed files with 512 additions and 0 deletions
31
packages/rethink/.gitignore
vendored
Normal file
31
packages/rethink/.gitignore
vendored
Normal file
|
@ -0,0 +1,31 @@
|
|||
# See https://www.dartlang.org/tools/private-files.html
|
||||
|
||||
# Files and directories created by pub
|
||||
.buildlog
|
||||
.packages
|
||||
.project
|
||||
.pub/
|
||||
build/
|
||||
**/packages/
|
||||
|
||||
# Files created by dart2js
|
||||
# (Most Dart developers will use pub build to compile Dart, use/modify these
|
||||
# rules if you intend to use dart2js directly
|
||||
# Convention is to use extension '.dart.js' for Dart compiled to Javascript to
|
||||
# differentiate from explicit Javascript files)
|
||||
*.dart.js
|
||||
*.part.js
|
||||
*.js.deps
|
||||
*.js.map
|
||||
*.info.json
|
||||
|
||||
# Directory created by dartdoc
|
||||
doc/api/
|
||||
|
||||
# Don't commit pubspec lock file
|
||||
# (Library packages only! Remove pattern if developing an application package)
|
||||
pubspec.lock
|
||||
|
||||
rethinkdb_data/
|
||||
.idea
|
||||
.dart_tool
|
4
packages/rethink/.travis.yml
Normal file
4
packages/rethink/.travis.yml
Normal file
|
@ -0,0 +1,4 @@
|
|||
language: dart
|
||||
addons:
|
||||
rethinkdb: '2.3'
|
||||
before_script: 'dart test/bootstrap.dart'
|
3
packages/rethink/CHANGELOG.md
Normal file
3
packages/rethink/CHANGELOG.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# 1.1.0
|
||||
* Moved to `package:rethinkdb_driver`
|
||||
* Fixed references to old hooked event names.
|
21
packages/rethink/LICENSE
Normal file
21
packages/rethink/LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2017 The Angel Framework
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
87
packages/rethink/README.md
Normal file
87
packages/rethink/README.md
Normal file
|
@ -0,0 +1,87 @@
|
|||
# rethink
|
||||
[![version 1.0.7](https://img.shields.io/badge/pub-1.0.7-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
|
||||
```
|
||||
|
||||
`package:rethinkdb_driver2` will be installed as well.
|
||||
|
||||
# Usage
|
||||
This library exposes one 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 `Query` (usually a table) 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
packages/rethink/lib/angel_rethink.dart
Normal file
1
packages/rethink/lib/angel_rethink.dart
Normal file
|
@ -0,0 +1 @@
|
|||
export 'src/rethink_service.dart';
|
237
packages/rethink/lib/src/rethink_service.dart
Normal file
237
packages/rethink/lib/src/rethink_service.dart
Normal file
|
@ -0,0 +1,237 @@
|
|||
import 'dart:async';
|
||||
//import 'dart:io';
|
||||
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 buildQuery(RqlQuery initialQuery, Map params) {
|
||||
if (params != null)
|
||||
params['broadcast'] = params.containsKey('broadcast')
|
||||
? params['broadcast']
|
||||
: (listenForChanges != true);
|
||||
|
||||
var q = _getQueryInner(initialQuery, params);
|
||||
|
||||
if (params?.containsKey('reql') == true && params['reql'] is QueryCallback)
|
||||
q = params['reql'](q);
|
||||
|
||||
return q ?? initialQuery;
|
||||
}
|
||||
|
||||
RqlQuery _getQueryInner(RqlQuery query, Map params) {
|
||||
if (params == null || !params.containsKey('query'))
|
||||
return query;
|
||||
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.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({key.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;
|
||||
}
|
||||
|
||||
_serialize(data) {
|
||||
if (data is Map)
|
||||
return data;
|
||||
else if (data is Iterable)
|
||||
return data.map(_serialize).toList();
|
||||
else
|
||||
return god.serializeObject(data);
|
||||
}
|
||||
|
||||
_squeeze(data) {
|
||||
if (data is Map)
|
||||
return data.keys.fold<Map>({}, (map, k) => map..[k.toString()] = data[k]);
|
||||
else if (data is Iterable)
|
||||
return data.map(_squeeze).toList();
|
||||
else
|
||||
return data;
|
||||
}
|
||||
|
||||
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.fireEvent(
|
||||
hookedService.afterCreated,
|
||||
new HookedServiceEvent(
|
||||
true, null, null, this, HookedServiceEvent.created,
|
||||
result: newVal));
|
||||
} else if (type == 'change') {
|
||||
// Update
|
||||
hookedService.fireEvent(
|
||||
hookedService.afterCreated,
|
||||
new HookedServiceEvent(
|
||||
true, null, null, this, HookedServiceEvent.updated,
|
||||
result: newVal, id: oldVal['id'], data: newVal));
|
||||
} else if (type == 'remove') {
|
||||
// Remove
|
||||
hookedService.fireEvent(
|
||||
hookedService.afterCreated,
|
||||
new HookedServiceEvent(
|
||||
true, null, null, this, HookedServiceEvent.removed,
|
||||
result: oldVal, id: oldVal['id']));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future index([Map params]) async {
|
||||
var query = buildQuery(table, params);
|
||||
return await _sendQuery(query);
|
||||
}
|
||||
|
||||
@override
|
||||
Future read(id, [Map params]) async {
|
||||
var query = buildQuery(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 AngelHttpException.methodNotAllowed();
|
||||
|
||||
var d = _serialize(data);
|
||||
var q = table as Table;
|
||||
var query = buildQuery(q.insert(_squeeze(d)), params);
|
||||
return await _sendQuery(query);
|
||||
}
|
||||
|
||||
@override
|
||||
Future modify(id, data, [Map params]) async {
|
||||
var d = _serialize(data);
|
||||
|
||||
if (d is Map && d.containsKey('id')) {
|
||||
try {
|
||||
await read(d['id'], params);
|
||||
} on AngelHttpException catch (e) {
|
||||
if (e.statusCode == 404)
|
||||
return await create(data, params);
|
||||
else
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
var query = buildQuery(table.get(id?.toString()), params).update(d);
|
||||
await _sendQuery(query);
|
||||
return await read(id, params);
|
||||
}
|
||||
|
||||
@override
|
||||
Future update(id, data, [Map params]) async {
|
||||
var d = _serialize(data);
|
||||
|
||||
if (d is Map && d.containsKey('id')) {
|
||||
try {
|
||||
await read(d['id'], params);
|
||||
} on AngelHttpException catch (e) {
|
||||
if (e.statusCode == 404)
|
||||
return await create(data, params);
|
||||
else
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
if (d is Map && !d.containsKey('id')) d['id'] = id.toString();
|
||||
var query = buildQuery(table.get(id?.toString()), params).replace(d);
|
||||
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 = buildQuery(table.get(id), params).delete();
|
||||
await _sendQuery(query);
|
||||
return prior;
|
||||
}
|
||||
}
|
||||
}
|
16
packages/rethink/pubspec.yaml
Normal file
16
packages/rethink/pubspec.yaml
Normal file
|
@ -0,0 +1,16 @@
|
|||
name: angel_rethink
|
||||
version: 1.1.0
|
||||
description: RethinkDB-enabled services for the Angel framework.
|
||||
author: Tobe O <thosakwe@gmail.com>
|
||||
environment:
|
||||
sdk: ">=1.19.0 <3.0.0"
|
||||
homepage: https://github.com/angel-dart/rethink
|
||||
dependencies:
|
||||
angel_framework: ^1.1.0
|
||||
json_god: ^2.0.0-beta
|
||||
rethinkdb_driver: ^2.3.1
|
||||
dev_dependencies:
|
||||
angel_client: ^1.1.0
|
||||
angel_test: ^1.1.0
|
||||
logging: ^0.11.3
|
||||
test: ^0.12.0
|
6
packages/rethink/test/README.md
Normal file
6
packages/rethink/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`.
|
11
packages/rethink/test/bootstrap.dart
Normal file
11
packages/rethink/test/bootstrap.dart
Normal file
|
@ -0,0 +1,11 @@
|
|||
import 'dart:io';
|
||||
import 'package:rethinkdb_driver/rethinkdb_driver.dart';
|
||||
|
||||
main() async {
|
||||
var r = new Rethinkdb();
|
||||
r.connect().then((conn) {
|
||||
r.tableCreate('todos').run(conn);
|
||||
print('Done');
|
||||
exit(0);
|
||||
});
|
||||
}
|
10
packages/rethink/test/common.dart
Normal file
10
packages/rethink/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};
|
||||
}
|
||||
}
|
85
packages/rethink/test/generic_test.dart
Normal file
85
packages/rethink/test/generic_test.dart
Normal file
|
@ -0,0 +1,85 @@
|
|||
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:logging/logging.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.errorHandler = (e, req, res) async {
|
||||
print('Whoops: $e');
|
||||
};
|
||||
|
||||
app.logger = new Logger.detached('angel')..onRecord.listen(print);
|
||||
|
||||
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('modify', () 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.modify(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