Add 'packages/sembast/' from commit 'eda2acc36a69cefbe038c0a362a03b23aa4f13de'
git-subtree-dir: packages/sembast git-subtree-mainline:567ddd3897
git-subtree-split:eda2acc36a
This commit is contained in:
commit
b1a6f262ea
10 changed files with 401 additions and 0 deletions
14
packages/sembast/.gitignore
vendored
Normal file
14
packages/sembast/.gitignore
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
# See https://www.dartlang.org/guides/libraries/private-files
|
||||
|
||||
# Files and directories created by pub
|
||||
.dart_tool/
|
||||
.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/
|
||||
*.db
|
1
packages/sembast/.travis.yml
Normal file
1
packages/sembast/.travis.yml
Normal file
|
@ -0,0 +1 @@
|
|||
language: dart
|
5
packages/sembast/CHANGELOG.md
Normal file
5
packages/sembast/CHANGELOG.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
# 1.0.1
|
||||
* Fix flaw where clients could remove all records, even if `allowRemoveAll` were `false`.
|
||||
|
||||
# 1.0.0
|
||||
* First release.
|
21
packages/sembast/LICENSE
Normal file
21
packages/sembast/LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2018 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.
|
40
packages/sembast/README.md
Normal file
40
packages/sembast/README.md
Normal file
|
@ -0,0 +1,40 @@
|
|||
# sembast
|
||||
[![Pub](https://img.shields.io/pub/v/angel_sembast.svg)](https://pub.dartlang.org/packages/angel_sembast)
|
||||
[![build status](https://travis-ci.org/angel-dart/sembast.svg)](https://travis-ci.org/angel-dart/sembast)
|
||||
|
||||
package:sembast-powered CRUD services for the Angel framework.
|
||||
|
||||
# Installation
|
||||
|
||||
Add the following to your `pubspec.yaml`:
|
||||
|
||||
```yaml
|
||||
dependencies:
|
||||
angel_sembast: ^1.0.0
|
||||
```
|
||||
|
||||
# Usage
|
||||
|
||||
This library exposes one main class: `SembastService`.
|
||||
|
||||
## SembastService
|
||||
|
||||
This class interacts with a `Database` and `Store` (from `package:sembast`) and serializes data to and from Maps.
|
||||
|
||||
## 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.
|
||||
|
||||
```dart
|
||||
List queried = await MyService.index({r"query": where.id(new Finder(filter: new Filter(...))));
|
||||
```
|
||||
|
||||
Of course, you can use `package:sembast` queries. Just pass it as `query` within `params`.
|
||||
|
||||
See the tests for more usage examples.
|
4
packages/sembast/analysis_options.yaml
Normal file
4
packages/sembast/analysis_options.yaml
Normal file
|
@ -0,0 +1,4 @@
|
|||
include: package:pedantic/analysis_options.yaml
|
||||
analyzer:
|
||||
strong-mode:
|
||||
implicit-casts: false
|
21
packages/sembast/example/main.dart
Normal file
21
packages/sembast/example/main.dart
Normal file
|
@ -0,0 +1,21 @@
|
|||
import 'package:angel_framework/angel_framework.dart';
|
||||
import 'package:angel_framework/http.dart';
|
||||
import 'package:angel_sembast/angel_sembast.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:sembast/sembast_io.dart';
|
||||
|
||||
main() async {
|
||||
var app = Angel();
|
||||
var db = await databaseFactoryIo.openDatabase('todos.db');
|
||||
|
||||
app
|
||||
..logger = (Logger('angel_sembast_example')..onRecord.listen(print))
|
||||
..use('/api/todos', SembastService(db, store: 'todos'))
|
||||
..shutdownHooks.add((_) => db.close());
|
||||
|
||||
var http = AngelHttp(app);
|
||||
var server = await http.startServer('127.0.0.1', 3000);
|
||||
var uri =
|
||||
Uri(scheme: 'http', host: server.address.address, port: server.port);
|
||||
print('angel_sembast example listening at $uri');
|
||||
}
|
166
packages/sembast/lib/angel_sembast.dart
Normal file
166
packages/sembast/lib/angel_sembast.dart
Normal file
|
@ -0,0 +1,166 @@
|
|||
import 'dart:async';
|
||||
import 'package:angel_framework/angel_framework.dart';
|
||||
import 'package:sembast/sembast.dart';
|
||||
|
||||
class SembastService extends Service<String, Map<String, dynamic>> {
|
||||
final Database database;
|
||||
final StoreRef<int, Map<String, dynamic>> store;
|
||||
|
||||
/// 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;
|
||||
|
||||
SembastService(this.database,
|
||||
{String store, this.allowRemoveAll = false, this.allowQuery = true})
|
||||
: this.store = intMapStoreFactory.store(store),
|
||||
super();
|
||||
|
||||
Finder _makeQuery([Map<String, dynamic> params]) {
|
||||
params = Map<String, dynamic>.from(params ?? {});
|
||||
Filter out;
|
||||
var sort = <SortOrder>[];
|
||||
|
||||
// You can pass a Finder as 'query':
|
||||
if (params['query'] is Finder) {
|
||||
return params['query'] as Finder;
|
||||
}
|
||||
|
||||
for (var key in params.keys) {
|
||||
if (key == r'$sort' &&
|
||||
(allowQuery == true || !params.containsKey('provider'))) {
|
||||
var v = params[key];
|
||||
|
||||
if (v is! Map) {
|
||||
sort.add(SortOrder(v.toString(), true));
|
||||
} else {
|
||||
var m = v as Map;
|
||||
m.forEach((k, sorter) {
|
||||
if (sorter is SortOrder) {
|
||||
sort.add(sorter);
|
||||
} else if (sorter is String) {
|
||||
sort.add(SortOrder(k.toString(), sorter == "-1"));
|
||||
} else if (sorter is num) {
|
||||
sort.add(SortOrder(k.toString(), sorter == -1));
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (key == 'query' &&
|
||||
(allowQuery == true || !params.containsKey('provider'))) {
|
||||
var queryObj = params[key];
|
||||
|
||||
if (queryObj is Map) {
|
||||
queryObj.forEach((k, v) {
|
||||
if (k != 'provider' &&
|
||||
!const ['__requestctx', '__responsectx'].contains(k)) {
|
||||
var filter = Filter.equals(k.toString(), v);
|
||||
if (out == null) {
|
||||
out = filter;
|
||||
} else {
|
||||
out = Filter.or([out, filter]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Finder(filter: out, sortOrders: sort);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _withId(Map<String, dynamic> data, String id) =>
|
||||
Map<String, dynamic>.from(data ?? {})..['id'] = id;
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> findOne(
|
||||
[Map<String, dynamic> params,
|
||||
String errorMessage =
|
||||
'No record was found matching the given query.']) async {
|
||||
return (await store.findFirst(database, finder: _makeQuery(params)))?.value;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Map<String, dynamic>>> index(
|
||||
[Map<String, dynamic> params]) async {
|
||||
var records = await store.find(database, finder: _makeQuery(params));
|
||||
return records
|
||||
.where((r) => r.value != null)
|
||||
.map((r) => _withId(r.value, r.key.toString()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> read(String id,
|
||||
[Map<String, dynamic> params]) async {
|
||||
var record = await store.record(int.parse(id)).getSnapshot(database);
|
||||
|
||||
if (record == null) {
|
||||
throw AngelHttpException.notFound(message: 'No record found for ID $id');
|
||||
}
|
||||
|
||||
return _withId(record.value, id);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> create(Map<String, dynamic> data,
|
||||
[Map<String, dynamic> params]) async {
|
||||
return await database.transaction((txn) async {
|
||||
var key = await store.add(txn, data);
|
||||
var id = key.toString();
|
||||
return _withId(data, id);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> modify(String id, Map<String, dynamic> data,
|
||||
[Map<String, dynamic> params]) async {
|
||||
return await database.transaction((txn) async {
|
||||
var record = store.record(int.parse(id));
|
||||
data = await record.put(txn, data, merge: true);
|
||||
return _withId(data, id);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> update(String id, Map<String, dynamic> data,
|
||||
[Map<String, dynamic> params]) async {
|
||||
return await database.transaction((txn) async {
|
||||
var record = store.record(int.parse(id));
|
||||
data = await record.put(txn, data);
|
||||
return _withId(data, id);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> remove(String id,
|
||||
[Map<String, dynamic> params]) async {
|
||||
if (id == null || id == 'null') {
|
||||
// Remove everything...
|
||||
if (!(allowRemoveAll == true ||
|
||||
params?.containsKey('provider') != true)) {
|
||||
throw AngelHttpException.forbidden(
|
||||
message: 'Clients are not allowed to delete all items.');
|
||||
} else {
|
||||
await store.delete(database);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
return database.transaction((txn) async {
|
||||
var record = store.record(int.parse(id));
|
||||
var snapshot = await record.getSnapshot(txn);
|
||||
|
||||
if (snapshot == null) {
|
||||
throw AngelHttpException.notFound(
|
||||
message: 'No record found for ID $id');
|
||||
} else {
|
||||
await record.delete(txn);
|
||||
}
|
||||
|
||||
return _withId(snapshot.value, id);
|
||||
});
|
||||
}
|
||||
}
|
16
packages/sembast/pubspec.yaml
Normal file
16
packages/sembast/pubspec.yaml
Normal file
|
@ -0,0 +1,16 @@
|
|||
name: angel_sembast
|
||||
description: package:sembast-powered CRUD services for the Angel framework.
|
||||
homepage: https://github.com/angel-dart/sembast
|
||||
version: 1.0.1
|
||||
author: Tobe O <thosakwe@gmail.com>
|
||||
environment:
|
||||
sdk: ">=2.1.0-dev <3.0.0"
|
||||
dependencies:
|
||||
angel_framework: ^2.0.0-alpha
|
||||
sembast: ^1.19.0-dev.2
|
||||
dev_dependencies:
|
||||
angel_http_exception: ^1.0.0
|
||||
logging:
|
||||
pedantic: ^1.0.0
|
||||
test: ^1.0.0
|
||||
|
113
packages/sembast/test/all_test.dart
Normal file
113
packages/sembast/test/all_test.dart
Normal file
|
@ -0,0 +1,113 @@
|
|||
import 'dart:collection';
|
||||
import 'package:angel_framework/angel_framework.dart';
|
||||
import 'package:angel_http_exception/angel_http_exception.dart';
|
||||
import 'package:angel_sembast/angel_sembast.dart';
|
||||
import 'package:sembast/sembast.dart';
|
||||
import 'package:sembast/sembast_memory.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
main() async {
|
||||
Database database;
|
||||
SembastService service;
|
||||
|
||||
setUp(() async {
|
||||
database = await memoryDatabaseFactory.openDatabase('test.db');
|
||||
service = SembastService(database);
|
||||
});
|
||||
|
||||
tearDown(() => database.close());
|
||||
|
||||
test('index', () async {
|
||||
await service.create({'id': '0', 'name': 'Tobe'});
|
||||
await service.create({'id': '1', 'name': 'Osakwe'});
|
||||
|
||||
var output = await service.index();
|
||||
print(output);
|
||||
expect(output, hasLength(2));
|
||||
expect(output[0], <String, dynamic>{'id': '1', 'name': 'Tobe'});
|
||||
expect(output[1], <String, dynamic>{'id': '2', 'name': 'Osakwe'});
|
||||
});
|
||||
|
||||
test('create', () async {
|
||||
var input = {'bar': 'baz'};
|
||||
var output = await service.create(input);
|
||||
print(output);
|
||||
expect(output.keys, contains('id'));
|
||||
expect(output, containsPair('bar', 'baz'));
|
||||
});
|
||||
|
||||
test('read', () async {
|
||||
var name = 'poobah${DateTime.now().millisecondsSinceEpoch}';
|
||||
var input = await service.create({'name': name, 'bar': 'baz'});
|
||||
print(input);
|
||||
expect(await service.read(input['id'] as String), input);
|
||||
});
|
||||
|
||||
test('modify', () async {
|
||||
var input = await service.create({'bar': 'baz', 'yes': 'no'});
|
||||
var id = input['id'] as String;
|
||||
var output = await service.modify(id, {'bar': 'quux'});
|
||||
expect(SplayTreeMap.from(output),
|
||||
SplayTreeMap.from({'id': id, 'bar': 'quux', 'yes': 'no'}));
|
||||
expect(await service.read(id), output);
|
||||
});
|
||||
|
||||
test('update', () async {
|
||||
var input = await service.create({'bar': 'baz'});
|
||||
var id = input['id'] as String;
|
||||
var output = await service.update(id, {'yes': 'no'});
|
||||
expect(output, {'id': id, 'yes': 'no'});
|
||||
expect(await service.read(id), output);
|
||||
});
|
||||
|
||||
test('remove', () async {
|
||||
var input = await service.create({'bar': 'baz'});
|
||||
var id = input['id'] as String;
|
||||
expect(await service.remove(id), input);
|
||||
expect(await StoreRef.main().record(id).get(database), isNull);
|
||||
});
|
||||
|
||||
test('remove', () async {
|
||||
await service.create({'bar': 'baz'});
|
||||
await service.create({'bar': 'baz'});
|
||||
await service.create({'bar': 'baz'});
|
||||
|
||||
expect(await service.index(), isNotEmpty);
|
||||
await service.remove(null);
|
||||
expect(await service.index(), isEmpty);
|
||||
});
|
||||
|
||||
test('cannot remove all unless explicitly set', () async {
|
||||
expect(() => service.remove(null, {'provider': Providers.rest}),
|
||||
throwsA(const TypeMatcher<AngelHttpException>()));
|
||||
expect(
|
||||
() => service.remove(null, {'provider': Providers.rest}),
|
||||
throwsA(predicate((x) => x is AngelHttpException && x.statusCode == 403,
|
||||
'throws forbidden')));
|
||||
expect(() => service.remove('null', {'provider': Providers.rest}),
|
||||
throwsA(const TypeMatcher<AngelHttpException>()));
|
||||
expect(
|
||||
() => service.remove('null', {'provider': Providers.rest}),
|
||||
throwsA(predicate((x) => x is AngelHttpException && x.statusCode == 403,
|
||||
'throws forbidden')));
|
||||
});
|
||||
|
||||
test('can remove all on server side', () async {
|
||||
await service.create({'bar': 'baz'});
|
||||
await service.create({'bar': 'baz'});
|
||||
await service.create({'bar': 'baz'});
|
||||
await service.remove(null);
|
||||
expect(await service.index(), isEmpty);
|
||||
|
||||
await service.create({'bar': 'baz'});
|
||||
await service.create({'bar': 'baz'});
|
||||
await service.create({'bar': 'baz'});
|
||||
await service.remove('null');
|
||||
expect(await service.index(), isEmpty);
|
||||
});
|
||||
|
||||
test('remove nonexistent', () async {
|
||||
expect(() => service.remove('440'),
|
||||
throwsA(const TypeMatcher<AngelHttpException>()));
|
||||
});
|
||||
}
|
Loading…
Reference in a new issue