From 7d5493662323b223c47687bd39f89ad868eb5077 Mon Sep 17 00:00:00 2001 From: regiostech Date: Wed, 22 Jun 2016 14:34:28 -0400 Subject: [PATCH] Need remove, sort, docs --- lib/angel_mongo.dart | 81 ++++++++++++++++++- lib/mongo_service.dart | 99 +++++++++++++++-------- lib/mongo_service_typed.dart | 131 +++++++++++++++++++++++++++++++ pubspec.yaml | 6 +- test/generic_tests.dart | 107 +++++++++++++------------ test/typed_tests.dart | 148 +++++++++++++++++++++++++++++++++++ 6 files changed, 483 insertions(+), 89 deletions(-) create mode 100644 lib/mongo_service_typed.dart diff --git a/lib/angel_mongo.dart b/lib/angel_mongo.dart index 314a2e16..95808b28 100644 --- a/lib/angel_mongo.dart +++ b/lib/angel_mongo.dart @@ -1,10 +1,87 @@ library angel_mongo; + import 'dart:async'; +import 'dart:mirrors'; import 'package:angel_framework/angel_framework.dart'; -import 'package:json_god/json_god.dart'; +import 'package:json_god/json_god.dart' as god; import 'package:merge_map/merge_map.dart'; import 'package:mongo_dart/mongo_dart.dart'; part 'mongo_service.dart'; -final _god = new God(); +part 'mongo_service_typed.dart'; + +/// A data type that can be serialized to MongoDB. +class Model { + /// This instance's ID. + String id; + + /// The time at which this instance was created. + DateTime createdAt; + + /// The time at which this instance was last updated. + DateTime updatedAt; +} + + +Map _transformId(Map doc) { + Map result = mergeMap([doc]); + result['id'] = doc['_id']; + + return result..remove('_id'); +} + +_lastItem(DbCollection collection, Function _jsonify, [Map params]) async { + return (await (await collection + .find(where.sortBy('\$natural', descending: true))) + .toList()) + .map((x) => _jsonify(x, params)) + .first; +} + +ObjectId _makeId(id) { + try { + return (id is ObjectId) ? id : new ObjectId.fromHexString(id.toString()); + } catch (e) { + throw new AngelHttpException.BadRequest(); + } +} + +Map _removeSensitive(Map data) { + return data..remove('id')..remove('_id')..remove('createdAt')..remove( + 'updatedAt'); +} + +SelectorBuilder _makeQuery([Map params_]) { + Map params = params_ ?? {}; + params = params..remove('provider'); + SelectorBuilder result = where.exists('_id'); + + // You can pass a SelectorBuilder as 'query'; + if (params['query'] != null && params['query'] is SelectorBuilder) + return params['query']; + + for (var key in params.keys) { + if (key == r'$sort') { + if (params[key] is Map) { + // If they send a map, then we'll sort by every key in the map + for (String fieldName in params[key].keys.where((x) => x is String)) { + var sorter = params[key][fieldName]; + if (sorter is num) { + result = result.sortBy(fieldName, descending: sorter == -1); + } else if (sorter is String) { + result = result.sortBy(fieldName, descending: sorter == "-1"); + } + } + } else if (params[key] is String) { + // If they send just a string, then we'll sort + // by that, ascending + result = result.sortBy(params[key]); + } + } else if (key is String) { + result = result.and(where.eq(key, params[key])); + } + } + + return result; +} diff --git a/lib/mongo_service.dart b/lib/mongo_service.dart index c0a3bb7b..cf0f071e 100644 --- a/lib/mongo_service.dart +++ b/lib/mongo_service.dart @@ -1,29 +1,33 @@ part of angel_mongo; +/// Manipulates data from MongoDB as Maps. class MongoService extends Service { DbCollection collection; - MongoService(DbCollection this.collection); + MongoService(DbCollection this.collection):super(); - Map _jsonify(Map doc) { + Map _transformId(Map doc) { + Map result = mergeMap([doc]); + result['id'] = doc['_id']; + + return result..remove('_id'); + } + + _jsonify(Map doc, [Map params]) { Map result = {}; for (var key in doc.keys) { if (doc[key] is ObjectId) { result[key] = doc[key].toHexString(); - } else result[key] = doc[key]; + } else + result[key] = doc[key]; } - return result; - } - _lastItem() async { - return (await (await collection.find( - where.sortBy('\$natural', descending: true))).toList()) - .map(_jsonify) - .first; + return _transformId(result); } SelectorBuilder _makeQuery([Map params_]) { Map params = params_ ?? {}; + params = params..remove('provider'); SelectorBuilder result = where.exists('_id'); for (var key in params.keys) { @@ -43,9 +47,7 @@ class MongoService extends Service { // by that, ascending result = result.sortBy(params[key]); } - } - - else if (key is String) { + } else if (key is String) { result = result.and(where.eq(key, params[key])); } } @@ -56,37 +58,70 @@ class MongoService extends Service { @override Future index([Map params]) async { return await (await collection.find(_makeQuery(params))) - .map(_jsonify) + .map((x) => _jsonify(x, params)) .toList(); } @override - Future create(data, [Map params]) async { - Map item = (data is Map) ? data : _god.serializeToMap(data); - item = mergeMap([item, params]); - item['createdAt'] = new DateTime.now(); - await collection.insert(item); - return await _lastItem(); + Future create(Map data, [Map params]) async { + Map item = (data is Map) ? data : god.serializeObject(data); + item = _removeSensitive(item); + + try { + item['createdAt'] = new DateTime.now(); + await collection.insert(item); + return await _lastItem(collection, _jsonify, params); + } catch (e, st) { + throw new AngelHttpException(e, stackTrace: st); + } } @override Future read(id, [Map params]) async { - ObjectId id_; - try { - id_ = (id is ObjectId) ? id : new ObjectId.fromHexString( - id.toString()); - } catch (e) { - throw new AngelHttpException.BadRequest(); - } - - Map found = await collection.findOne( - where.id(id_).and(_makeQuery(params))); + ObjectId _id = _makeId(id); + Map found = await collection.findOne(where.id(_id).and(_makeQuery(params))); if (found == null) { throw new AngelHttpException.NotFound( - message: 'No record found for ID ${id_.toHexString()}'); + message: 'No record found for ID ${_id.toHexString()}'); } - return _jsonify(found); + return _jsonify(found, params); } + + @override + Future modify(id, Map data, [Map params]) async { + Map target = await read(id, params); + Map result = mergeMap([target, _removeSensitive(data)]); + result['updatedAt'] = new DateTime.now(); + + try { + await collection.update(where.id(_makeId(id)), result); + result = _jsonify(result, params); + result['id'] = id; + return result; + } catch (e, st) { + throw new AngelHttpException(e, stackTrace: st); + } + } + + @override + Future update(id, data, [Map params]) async { + Map target = await read(id, params); + Map result = _removeSensitive(data); + result['_id'] = _makeId(id); + result['createdAt'] = target['createdAt']; + result['updatedAt'] = new DateTime.now(); + + try { + await collection.update(where.id(_makeId(id)), result); + result = _jsonify(result, params); + result['id'] = id; + return result; + } catch (e, st) { + throw new AngelHttpException(e, stackTrace: st); + } + } + + } diff --git a/lib/mongo_service_typed.dart b/lib/mongo_service_typed.dart new file mode 100644 index 00000000..c4a11ec9 --- /dev/null +++ b/lib/mongo_service_typed.dart @@ -0,0 +1,131 @@ +part of angel_mongo; + +/// Manipulates data from MongoDB by serializing BSON from and deserializing BSON to a target class. +class MongoTypedService extends Service { + DbCollection collection; + + MongoTypedService(DbCollection this.collection):super() { + if (!reflectType(T).isAssignableTo(reflectType(Model))) + throw new Exception( + "If you specify a type for MongoService, it must be dynamic, Map, or extend from Model."); + } + + Map _transformId(Map doc) { + Map result = mergeMap([doc]); + result['id'] = doc['_id']; + + return result..remove('_id'); + } + + _jsonify(Map doc, [Map params]) { + Map result = {}; + for (var key in doc.keys) { + if (doc[key] is ObjectId) { + result[key] = doc[key].toHexString(); + } else + result[key] = doc[key]; + } + + result = _transformId(result); + + // Clients will always receive JSON. + if ((params != null && params['provider'] != null)) { + return result; + } + else { + // However, when we run server-side, we should return a T, not a Map. + Model typedResult = god.deserializeDatum(result, outputType: T); + typedResult.createdAt = result['createdAt']; + typedResult.updatedAt = result['updatedAt']; + return typedResult; + } + } + + @override + Future index([Map params]) async { + return await (await collection.find(_makeQuery(params))) + .map((x) => _jsonify(x, params)) + .toList(); + } + + @override + Future create(data, [Map params]) async { + Map item; + + try { + Model target = + (data is T) ? data : god.deserializeDatum(data, outputType: T); + item = god.serializeObject(target); + item = _removeSensitive(item); + + item['createdAt'] = new DateTime.now(); + await collection.insert(item); + return await _lastItem(collection, _jsonify, params); + } catch (e, st) { + print(e); + print(st); + throw new AngelHttpException.BadRequest(); + } + } + + @override + Future read(id, [Map params]) async { + ObjectId _id = _makeId(id); + + Map found = await collection.findOne(where.id(_id).and(_makeQuery(params))); + + if (found == null) { + throw new AngelHttpException.NotFound( + message: 'No record found for ID ${_id.toHexString()}'); + } + + return _jsonify(found, params); + } + + @override + Future modify(id, Map data, [Map params]) async { + ObjectId _id = _makeId(id); + try { + Map result = await collection.findOne( + where.id(_id).and(_makeQuery(params))); + + if (result == null) { + throw new AngelHttpException.NotFound( + message: 'No record found for ID ${_id.toHexString()}'); + } + + result = mergeMap([result, _removeSensitive(data)]); + result['_id'] = _id; + result['updatedAt'] = new DateTime.now(); + + await collection.update(where.id(_id), result); + return await read(_id, params); + } catch (e, st) { + throw new AngelHttpException(e, stackTrace: st); + } + } + + @override + Future update(id, _data, [Map params]) async { + try { + Model data = (_data is T) ? _data : god.deserializeDatum( + _data, outputType: T); + ObjectId _id = _makeId(id); + Map rawData = _removeSensitive(god.serializeObject(data)); + rawData['_id'] = _id; + rawData['createdAt'] = data.createdAt; + rawData['updatedAt'] = new DateTime.now(); + + await collection.update(where.id(_id).and(_makeQuery(params)), rawData); + var result = _jsonify(rawData, params); + + if (result is T) { + result.createdAt = data.createdAt; + result.updatedAt = rawData['updatedAt']; + } + return result; + } catch (e, st) { + throw new AngelHttpException(e, stackTrace: st); + } + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 44a65084..3c50d85f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,9 +4,9 @@ description: Core libraries for the Angel framework. author: Tobe O homepage: https://github.com/angel-dart/angel_framework dependencies: - angel_framework: ">=0.0.0-dev.17 < 0.1.0" - json_god: ">=1.0.0 <2.0.0" - mongo_dart: ">= 0.2.5+1 < 1.0.0" + angel_framework: ">=1.0.0-dev < 2.0.0" + json_god: ">=2.0.0-beta <3.0.0" + mongo_dart: ">= 0.2.7 < 1.0.0" dev_dependencies: http: ">= 0.11.3 < 0.12.0" test: ">= 0.12.13 < 0.13.0" \ No newline at end of file diff --git a/test/generic_tests.dart b/test/generic_tests.dart index 7e72b76f..4e346135 100644 --- a/test/generic_tests.dart +++ b/test/generic_tests.dart @@ -2,63 +2,53 @@ import 'dart:io'; import 'package:angel_framework/angel_framework.dart'; import 'package:angel_mongo/angel_mongo.dart'; import 'package:http/http.dart' as http; -import 'package:json_god/json_god.dart'; +import 'package:json_god/json_god.dart' as god; import 'package:mongo_dart/mongo_dart.dart'; import 'package:test/test.dart'; -class Greeting { - final String to; - const Greeting(String this.to); -} - final headers = { HttpHeaders.ACCEPT: ContentType.JSON.mimeType, HttpHeaders.CONTENT_TYPE: ContentType.JSON.mimeType }; -wireHooked(HookedService hooked) { - hooked.onCreated.listen((item) { - print("Just created: $item"); - }); -} +final Map testGreeting = {'to': 'world'}; -wireGreeting(HookedService hooked) { - hooked.onCreated.listen((item) { - print("Greeting: $item"); - }); +wireHooked(HookedService hooked) { + hooked + ..afterCreated.listen((HookedServiceEvent event) { + print("Just created: ${event.result}"); + }) + ..afterModified.listen((HookedServiceEvent event) { + print("Just modified: ${event.result}"); + }) + ..afterUpdated.listen((HookedServiceEvent event) { + print("Just updated: ${event.result}"); + }); } main() { group('angel_mongo', () { Angel app = new Angel(); http.Client client; - God god = new God(); Db db = new Db('mongodb://localhost:27017/angel_mongo'); DbCollection testData; - DbCollection testGreetings; String url; + HookedService Greetings; setUp(() async { client = new http.Client(); await db.open(); testData = db.collection('test_data'); - testGreetings = db.collection('test_greetings'); // Delete anything before we start await testData.remove(); - await testGreetings.remove(); var service = new MongoService(testData); - var hooked = new HookedService(service); - wireHooked(hooked); + Greetings = new HookedService(service); + wireHooked(Greetings); - var greetings = new MongoService(testGreetings); - var hookedGreetings = new HookedService(greetings); - wireGreeting(hookedGreetings); - - app.use('/api', hooked); - app.use('/greetings', hookedGreetings); - HttpServer server = await app.startServer( - InternetAddress.LOOPBACK_IP_V4, 0); + app.use('/api', Greetings); + HttpServer server = + await app.startServer(InternetAddress.LOOPBACK_IP_V4, 0); url = "http://${server.address.host}:${server.port}"; }); @@ -69,13 +59,12 @@ main() { await app.httpServer.close(force: true); client = null; url = null; + Greetings = null; }); test('insert items', () async { - Map testUser = {'hello': 'world'}; - - var response = await client.post( - "$url/api", body: god.serialize(testUser), headers: headers); + var response = await client.post("$url/api", + body: god.serialize(testGreeting), headers: headers); expect(response.statusCode, equals(HttpStatus.OK)); response = await client.get("$url/api"); @@ -85,42 +74,56 @@ main() { }); test('read item', () async { - Map testUser = {'hello': 'world'}; - var response = await client.post( - "$url/api", body: god.serialize(testUser), headers: headers); + var response = await client.post("$url/api", + body: god.serialize(testGreeting), headers: headers); expect(response.statusCode, equals(HttpStatus.OK)); Map created = god.deserialize(response.body); - response = await client.get("$url/api/${created['_id']}"); + response = await client.get("$url/api/${created['id']}"); expect(response.statusCode, equals(HttpStatus.OK)); Map read = god.deserialize(response.body); - expect(read['_id'], equals(created['_id'])); - expect(read['hello'], equals('world')); + expect(read['id'], equals(created['id'])); + expect(read['to'], equals('world')); expect(read['createdAt'], isNot(null)); }); test('modify item', () async { + var response = await client.post("$url/api", + body: god.serialize(testGreeting), headers: headers); + expect(response.statusCode, equals(HttpStatus.OK)); + Map created = god.deserialize(response.body); + response = await client.patch( + "$url/api/${created['id']}", body: god.serialize({"to": "Mom"}), + headers: headers); + Map modified = god.deserialize(response.body); + expect(response.statusCode, equals(HttpStatus.OK)); + expect(modified['id'], equals(created['id'])); + expect(modified['to'], equals('Mom')); + expect(modified['updatedAt'], isNot(null)); }); test('update item', () async { + var response = await client.post("$url/api", + body: god.serialize(testGreeting), headers: headers); + expect(response.statusCode, equals(HttpStatus.OK)); + Map created = god.deserialize(response.body); + response = await client.post( + "$url/api/${created['id']}", body: god.serialize({"to": "Updated"}), + headers: headers); + Map modified = god.deserialize(response.body); + expect(response.statusCode, equals(HttpStatus.OK)); + expect(modified['id'], equals(created['id'])); + expect(modified['to'], equals('Updated')); + expect(modified['updatedAt'], isNot(null)); }); - test('remove item', () async { + test('remove item', () async {}); + test(r'$sort', () async { }); - test('sort by string', () async { - - }); - - test('sort by map', () async { - - }); - - test('query parameters', () async { - - }); + test('query parameters', () async {}); }); -} \ No newline at end of file +} diff --git a/test/typed_tests.dart b/test/typed_tests.dart index e69de29b..d29eaeae 100644 --- a/test/typed_tests.dart +++ b/test/typed_tests.dart @@ -0,0 +1,148 @@ +import 'dart:io'; +import 'package:angel_framework/angel_framework.dart'; +import 'package:angel_mongo/angel_mongo.dart'; +import 'package:http/http.dart' as http; +import 'package:json_god/json_god.dart' as god; +import 'package:mongo_dart/mongo_dart.dart'; +import 'package:test/test.dart'; + +class Greeting extends Model { + String to; + + Greeting({String this.to}); +} + +final headers = { + HttpHeaders.ACCEPT: ContentType.JSON.mimeType, + HttpHeaders.CONTENT_TYPE: ContentType.JSON.mimeType +}; + +final Map testGreeting = {'to': 'world'}; + +wireHooked(HookedService hooked) { + hooked + ..afterCreated.listen((HookedServiceEvent event) { + print("Just created: ${god.serialize(event.result)}"); + }) + ..afterModified.listen((HookedServiceEvent event) { + print("Just modified: ${god.serialize(event.result)}"); + }) + ..afterUpdated.listen((HookedServiceEvent event) { + print("Just updated: ${event.result}"); + }); +} + +main() { + group('angel_mongo', () { + Angel app = new Angel(); + http.Client client; + Db db = new Db('mongodb://localhost:27017/angel_mongo'); + DbCollection testData; + String url; + Service Greetings; + + setUp(() async { + client = new http.Client(); + await db.open(); + testData = db.collection('test_greetings'); + // Delete anything before we start + await testData.remove(); + + var service = new MongoTypedService(testData); + Greetings = new HookedService(service); + wireHooked(Greetings); + + app.use('/api', Greetings); + HttpServer server = + await app.startServer(InternetAddress.LOOPBACK_IP_V4, 0); + url = "http://${server.address.host}:${server.port}"; + }); + + tearDown(() async { + // Delete anything left over + await testData.remove(); + await db.close(); + await app.httpServer.close(force: true); + client = null; + url = null; + Greetings = null; + }); + + test('insert items', () async { + var response = await client.post("$url/api", + body: god.serialize(testGreeting), headers: headers); + expect(response.statusCode, equals(HttpStatus.OK)); + + response = await client.get("$url/api"); + expect(response.statusCode, 200); + List greetings = god.deserialize(response.body); + expect(greetings.length, equals(1)); + + Greeting greeting = new Greeting(to: "Mom"); + await Greetings.create(greeting); + greetings = await (await testData.find()).toList(); + expect(greetings.length, equals(2)); + }); + + test('read item', () async { + var response = await client.post("$url/api", + body: god.serialize(testGreeting), headers: headers); + expect(response.statusCode, equals(HttpStatus.OK)); + Map created = god.deserialize(response.body); + + response = await client.get("$url/api/${created['id']}"); + expect(response.statusCode, equals(HttpStatus.OK)); + Map read = god.deserialize(response.body); + expect(read['id'], equals(created['id'])); + expect(read['to'], equals('world')); + expect(read['createdAt'], isNot(null)); + + Greeting greeting = await Greetings.read(created['id']); + expect(greeting.id, equals(created['id'])); + expect(greeting.to, equals('world')); + expect(greeting.createdAt, isNot(null)); + }); + + test('modify item', () async { + var response = await client.post("$url/api", + body: god.serialize(testGreeting), headers: headers); + expect(response.statusCode, equals(HttpStatus.OK)); + Map created = god.deserialize(response.body); + + response = await client.patch( + "$url/api/${created['id']}", body: god.serialize({"to": "Mom"}), + headers: headers); + Map modified = god.deserialize(response.body); + expect(response.statusCode, equals(HttpStatus.OK)); + expect(modified['id'], equals(created['id'])); + expect(modified['to'], equals('Mom')); + expect(modified['updatedAt'], isNot(null)); + + await Greetings.modify(created['id'], {"to": "Batman"}); + Greeting greeting = await Greetings.read(created['id']); + expect(greeting.to, equals("Batman")); + }); + + test('update item', () async { + var response = await client.post("$url/api", + body: god.serialize(testGreeting), headers: headers); + expect(response.statusCode, equals(HttpStatus.OK)); + Map created = god.deserialize(response.body); + + response = await client.post( + "$url/api/${created['id']}", body: god.serialize({"to": "Updated"}), + headers: headers); + Map modified = god.deserialize(response.body); + expect(response.statusCode, equals(HttpStatus.OK)); + expect(modified['id'], equals(created['id'])); + expect(modified['to'], equals('Updated')); + expect(modified['updatedAt'], isNot(null)); + }); + + test('remove item', () async {}); + + test(r'$sort', () async {}); + + test('query parameters', () async {}); + }); +}