From e4969b0220de22819a7a7ef65f57e60cb518647a Mon Sep 17 00:00:00 2001 From: Tobe O Date: Tue, 6 Nov 2018 01:04:37 -0500 Subject: [PATCH] All tests passed --- .gitignore | 1 + CHANGELOG.md | 2 + analysis_options.yaml | 3 + example/main.dart | 21 ++++++ lib/angel_sembast.dart | 168 +++++++++++++++++++++++++++++++++++++++++ pubspec.yaml | 1 + test/all_test.dart | 73 ++++++++++++++++++ 7 files changed, 269 insertions(+) create mode 100644 CHANGELOG.md create mode 100644 analysis_options.yaml create mode 100644 example/main.dart create mode 100644 lib/angel_sembast.dart create mode 100644 test/all_test.dart diff --git a/.gitignore b/.gitignore index 7bf00e82..beaa4a61 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ pubspec.lock # Directory created by dartdoc # If you don't generate documentation locally you can remove this line. doc/api/ +*.db \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..bdbca561 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,2 @@ +# 1.0.0 +* First release. \ No newline at end of file diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 00000000..eae1e42a --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,3 @@ +analyzer: + strong-mode: + implicit-casts: false \ No newline at end of file diff --git a/example/main.dart b/example/main.dart new file mode 100644 index 00000000..e89f1382 --- /dev/null +++ b/example/main.dart @@ -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 = new Angel(); + var db = await databaseFactoryIo.openDatabase('todos.db'); + + app + ..logger = (new Logger('angel_sembast_example')..onRecord.listen(print)) + ..use('/api/todos', new SembastService(db, store: 'todos')) + ..shutdownHooks.add((_) => db.close()); + + var http = new AngelHttp(app); + var server = await http.startServer('127.0.0.1', 3000); + var uri = + new Uri(scheme: 'http', host: server.address.address, port: server.port); + print('angel_sembast example listening at $uri'); +} diff --git a/lib/angel_sembast.dart b/lib/angel_sembast.dart new file mode 100644 index 00000000..72861546 --- /dev/null +++ b/lib/angel_sembast.dart @@ -0,0 +1,168 @@ +import 'dart:async'; +import 'package:angel_framework/angel_framework.dart'; +import 'package:sembast/sembast.dart'; + +class SembastService extends Service> { + final Database database; + final Store 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 = + (store == null ? database.mainStore : database.getStore(store)), + super(); + + Finder _makeQuery([Map params]) { + params = new Map.from(params ?? {}); + Filter out; + var sort = []; + + // 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(new 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(new SortOrder(k.toString(), sorter == "-1")); + } else if (sorter is num) { + sort.add(new 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 = new Filter.equal(k.toString(), v); + if (out == null) + out = filter; + else + out = new Filter.or([out, filter]); + } + }); + } + } + } + + return new Finder(filter: out, sortOrders: sort); + } + + Map _jsonify(Record record) { + return new Map.from(record.value as Map) + ..['id'] = record.key.toString(); + } + + @override + Future> findOne( + [Map params, + String errorMessage = 'No record was found matching the given query.']) { + return store.findRecord(_makeQuery(params)).then(_jsonify); + } + + @override + Future>> index( + [Map params]) async { + var records = await store.findRecords(_makeQuery(params)); + return records.where((r) => r.value != null).map(_jsonify).toList(); + } + + @override + Future> read(String id, + [Map params]) async { + var record = await store.get(int.parse(id)); + + if (record == null) { + throw new AngelHttpException.notFound( + message: 'No record found for ID $id'); + } + + return (record as Map)..['id'] = id; + } + + @override + Future> create(Map data, + [Map params]) async { + return await database.transaction((txn) async { + var store = txn.getStore(this.store.name); + var key = await store.put(data) as int; + var id = key.toString(); + data = new Map.from(data)..['id'] = id; + return data; + }); + } + + @override + Future> modify(String id, Map data, + [Map params]) async { + data = new Map.from(data)..['id'] = id; + + return await database.transaction((txn) async { + var store = txn.getStore(this.store.name); + var existing = await store.get(int.parse(id)); + + data = + new Map.from(existing as Map ?? {}) + ..addAll(data) + ..['id'] = id; + + await store.put(data, int.parse(id)); + return (await store.get(int.parse(id)) as Map) + ..['id'] = id; + }); + } + + @override + Future> update(String id, Map data, + [Map params]) async { + data = new Map.from(data)..['id'] = id; + + return await database.transaction((txn) async { + var store = txn.getStore(this.store.name); + await store.put(data, int.parse(id)); + return (await store.get(int.parse(id)) as Map) + ..['id'] = id; + }); + } + + @override + Future> remove(String id, + [Map params]) async { + return database.transaction((txn) async { + var store = txn.getStore(this.store.name); + var record = await store.get(int.parse(id)) as Map; + + if (record == null) { + throw new AngelHttpException.notFound( + message: 'No record found for ID $id'); + } else { + await store.delete(id); + } + + return record..['id'] = id; + }); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index befba48f..55fb5971 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,6 @@ name: angel_sembast description: package:sembast-powered CRUD services for the Angel framework. +homepage: https://github.com/angel-dart/sembast version: 1.0.0 author: Tobe O environment: diff --git a/test/all_test.dart b/test/all_test.dart new file mode 100644 index 00000000..365fc7b7 --- /dev/null +++ b/test/all_test.dart @@ -0,0 +1,73 @@ +import 'dart:collection'; +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 = new 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], {'id': '1', 'name': 'Tobe'}); + expect(output[1], {'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${new 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(new SplayTreeMap.from(output), + new 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 database.get(id), isNull); + }); + + test('remove nonexistent', () async { + expect(() => service.remove('440'), + throwsA(const TypeMatcher())); + }); +}