diff --git a/packages/mongo/.gitignore b/packages/mongo/.gitignore new file mode 100644 index 00000000..d95a2f2f --- /dev/null +++ b/packages/mongo/.gitignore @@ -0,0 +1,88 @@ +# See https://www.dartlang.org/tools/private-files.html + +# Files and directories created by pub +.buildlog +.packages +.project +.pub/ +build/ +**/packages/ +.dart_tool + +# 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 +### Dart template +# See https://www.dartlang.org/tools/private-files.html + +# Files and directories created by pub + +# 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) + +# Directory created by dartdoc + +# Don't commit pubspec lock file +# (Library packages only! Remove pattern if developing an application package) +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff: +.idea/workspace.xml +.idea/tasks.xml +.idea/dictionaries +.idea/vcs.xml +.idea/jsLibraryMappings.xml + +# Sensitive or high-churn files: +.idea/dataSources.ids +.idea/dataSources.xml +.idea/dataSources.local.xml +.idea/sqlDataSources.xml +.idea/dynamic.xml +.idea/uiDesigner.xml + +# Gradle: +.idea/gradle.xml +.idea/libraries + +# Mongo Explorer plugin: +.idea/mongoSettings.xml + +## File-based project format: +*.iws + +## Plugin-specific files: + +# IntelliJ +/out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties diff --git a/packages/mongo/.idea/angel_mongo.iml b/packages/mongo/.idea/angel_mongo.iml new file mode 100644 index 00000000..02bd9dfb --- /dev/null +++ b/packages/mongo/.idea/angel_mongo.iml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/mongo/.idea/modules.xml b/packages/mongo/.idea/modules.xml new file mode 100644 index 00000000..571c4566 --- /dev/null +++ b/packages/mongo/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/packages/mongo/.idea/runConfigurations/All_Tests.xml b/packages/mongo/.idea/runConfigurations/All_Tests.xml new file mode 100644 index 00000000..ac11209e --- /dev/null +++ b/packages/mongo/.idea/runConfigurations/All_Tests.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/packages/mongo/.idea/runConfigurations/Generic_Tests.xml b/packages/mongo/.idea/runConfigurations/Generic_Tests.xml new file mode 100644 index 00000000..034d37e1 --- /dev/null +++ b/packages/mongo/.idea/runConfigurations/Generic_Tests.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/packages/mongo/.idea/runConfigurations/Typed_Tests.xml b/packages/mongo/.idea/runConfigurations/Typed_Tests.xml new file mode 100644 index 00000000..0f5d6a48 --- /dev/null +++ b/packages/mongo/.idea/runConfigurations/Typed_Tests.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/packages/mongo/.idea/vcs.xml b/packages/mongo/.idea/vcs.xml new file mode 100644 index 00000000..94a25f7f --- /dev/null +++ b/packages/mongo/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/packages/mongo/.travis.yml b/packages/mongo/.travis.yml new file mode 100644 index 00000000..cf32cda7 --- /dev/null +++ b/packages/mongo/.travis.yml @@ -0,0 +1,3 @@ +language: dart +services: + - mongodb \ No newline at end of file diff --git a/packages/mongo/.vscode/tasks.json b/packages/mongo/.vscode/tasks.json new file mode 100644 index 00000000..6969f9b5 --- /dev/null +++ b/packages/mongo/.vscode/tasks.json @@ -0,0 +1,10 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "0.1.0", + "command": "test", + "isShellCommand": true, + "args": ["pub", "run", "test"], + "showOutput": "always", + "suppressTaskName": true +} \ No newline at end of file diff --git a/packages/mongo/CHANGELOG.md b/packages/mongo/CHANGELOG.md new file mode 100644 index 00000000..2a97dbec --- /dev/null +++ b/packages/mongo/CHANGELOG.md @@ -0,0 +1,12 @@ +# 2.0.3 +* Add null-coalescing check when processing queries: https://github.com/angel-dart/mongo/pull/12 + +# 2.0.2 +* Fix flaw where clients could remove all records, even if `allowRemoveAll` were `false`. + +# 2.0.1 +* Override `readMany` and `findOne`. + +# 2.0.0- +* Delete `mongo_service_typed`. +* Update for Angel 2. diff --git a/packages/mongo/LICENSE b/packages/mongo/LICENSE new file mode 100644 index 00000000..eb4ce33e --- /dev/null +++ b/packages/mongo/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 angel-dart + +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. diff --git a/packages/mongo/README.md b/packages/mongo/README.md new file mode 100644 index 00000000..de3dd1c5 --- /dev/null +++ b/packages/mongo/README.md @@ -0,0 +1,57 @@ +# angel_mongo + +[![Pub](https://img.shields.io/pub/v/angel_mongo.svg)](https://pub.dartlang.org/packages/angel_mongo) +[![build status](https://travis-ci.org/angel-dart/mongo.svg)](https://travis-ci.org/angel-dart/mongo) + +MongoDB-enabled services for the Angel framework. + +# Installation +Add the following to your `pubspec.yaml`: + +```yaml +dependencies: + angel_mongo: ^2.0.0 +``` + +# Usage +This library exposes one main class: `MongoService`. + +## 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 db = new Db('mongodb://localhost:27017/local'); + await db.open(); + + var service = app.use('/api/users', new MongoService(db.collection("users"))); + + service.afterCreated.listen((event) { + print("New user: ${event.result}"); + }); +} +``` + +## MongoService +This class interacts with a `DbCollection` (from mongo_dart) and serializing 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. + + List queried = await MyService.index({r"$query": where.id(new ObjectId.fromHexString("some hex string"}))); + +And, of course, you can use mongo_dart queries. Just pass it as `query` within `params`. + +See the tests for more usage examples. diff --git a/packages/mongo/analysis_options.yaml b/packages/mongo/analysis_options.yaml new file mode 100644 index 00000000..c230cee7 --- /dev/null +++ b/packages/mongo/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:pedantic/analysis_options.yaml +analyzer: + strong-mode: + implicit-casts: false \ No newline at end of file diff --git a/packages/mongo/example/example.dart b/packages/mongo/example/example.dart new file mode 100644 index 00000000..161c9173 --- /dev/null +++ b/packages/mongo/example/example.dart @@ -0,0 +1,15 @@ +import 'package:angel_framework/angel_framework.dart'; +import 'package:angel_mongo/angel_mongo.dart'; +import 'package:mongo_dart/mongo_dart.dart'; + +main() async { + var app = new Angel(); + Db db = new Db('mongodb://localhost:27017/local'); + await db.open(); + + var service = app.use('/api/users', new MongoService(db.collection("users"))); + + service.afterCreated.listen((event) { + print("New user: ${event.result}"); + }); +} diff --git a/packages/mongo/lib/angel_mongo.dart b/packages/mongo/lib/angel_mongo.dart new file mode 100644 index 00000000..56747813 --- /dev/null +++ b/packages/mongo/lib/angel_mongo.dart @@ -0,0 +1,3 @@ +library angel_mongo; + +export 'services.dart'; diff --git a/packages/mongo/lib/model.dart b/packages/mongo/lib/model.dart new file mode 100644 index 00000000..1c8c8ed4 --- /dev/null +++ b/packages/mongo/lib/model.dart @@ -0,0 +1,14 @@ +library angel_mongo.model; + +/// Use the `Model` class defined in `package:angel_framework/common.dart` instead. +@deprecated +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; +} diff --git a/packages/mongo/lib/mongo_service.dart b/packages/mongo/lib/mongo_service.dart new file mode 100644 index 00000000..5a2dfee6 --- /dev/null +++ b/packages/mongo/lib/mongo_service.dart @@ -0,0 +1,237 @@ +part of angel_mongo.services; + +/// Manipulates data from MongoDB as Maps. +class MongoService extends Service> { + DbCollection collection; + + /// 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; + + /// No longer used. Will be removed by `2.1.0`. + @deprecated + final bool debug; + + MongoService(DbCollection this.collection, + {this.allowRemoveAll = false, this.allowQuery = true, this.debug = true}) + : super(); + + SelectorBuilder _makeQuery([Map params_]) { + Map params = new Map.from(params_ ?? {}); + params = params..remove('provider'); + SelectorBuilder result = where.exists('_id'); + + // You can pass a SelectorBuilder as 'query'; + if (params['query'] is SelectorBuilder) { + return params['query'] as SelectorBuilder; + } + + for (var key in params.keys) { + if (key == r'$sort' || + key == r'$query' && + (allowQuery == true || !params.containsKey('provider'))) { + 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 (sorter is SelectorBuilder) { + result = result.and(sorter); + } + } + } else if (params[key] is String && key == r'$sort') { + // If they send just a string, then we'll sort + // by that, ascending + result = result.sortBy(params[key] as String); + } + } else if (key == 'query' && + (allowQuery == true || !params.containsKey('provider'))) { + var query = params[key] as Map; + query?.forEach((key, v) { + var value = v is Map ? _filterNoQuery(v) : v; + + if (!_NO_QUERY.contains(key) && + value is! RequestContext && + value is! ResponseContext) { + result = result.and(where.eq(key as String, value)); + } + }); + } + } + + return result; + } + + Map _jsonify(Map doc, + [Map params]) { + var result = {}; + + for (var key in doc.keys) { + var value = doc[key]; + if (value is ObjectId) { + result[key] = value.toHexString(); + } else if (value is! RequestContext && value is! ResponseContext) { + result[key] = value; + } + } + + return _transformId(result); + } + + @override + Future>> index( + [Map params]) async { + return await (await collection.find(_makeQuery(params))) + .map((x) => _jsonify(x, params)) + .toList(); + } + + static const String _NONCE_KEY = '__angel__mongo__nonce__key__'; + + @override + Future> create(Map data, + [Map params]) async { + var item = _removeSensitive(data); + + try { + var nonce = (await collection.db.getNonce())['nonce'] as String; + var result = await collection.findAndModify( + query: where.eq(_NONCE_KEY, nonce), + update: item, + returnNew: true, + upsert: true); + return _jsonify(result); + } catch (e, st) { + throw new AngelHttpException(e, stackTrace: st); + } + } + + @override + Future> findOne( + [Map params, + String errorMessage = + 'No record was found matching the given query.']) async { + var found = await collection.findOne(_makeQuery(params)); + + if (found == null) { + throw new AngelHttpException.notFound(message: errorMessage); + } + + return _jsonify(found, params); + } + + @override + Future> read(String id, + [Map params]) async { + ObjectId _id = _makeId(id); + var 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>> readMany(List ids, + [Map params]) async { + var q = _makeQuery(params); + q = ids.fold(q, (q, id) => q.or(where.id(_makeId(id)))); + return await (await collection.find(q)) + .map((x) => _jsonify(x, params)) + .toList(); + } + + @override + Future> modify(String id, data, + [Map params]) async { + Map target; + + try { + target = await read(id, params); + } on AngelHttpException catch (e) { + if (e.statusCode == 404) + return await create(data, params); + else + rethrow; + } + + var result = mergeMap([target, _removeSensitive(data)]); + //result['updatedAt'] = new DateTime.now().toIso8601String(); + + try { + var modified = await collection.findAndModify( + query: where.id(_makeId(id)), update: result, returnNew: true); + result = _jsonify(modified, params); + result['id'] = _makeId(id).toHexString(); + return result; + } catch (e, st) { + //printDebug(e, st, 'MODIFY'); + throw new AngelHttpException(e, stackTrace: st); + } + } + + @override + Future> update(String id, Map data, + [Map params]) async { + var result = _removeSensitive(data); + result['_id'] = _makeId(id); + /*result['createdAt'] = + target is Map ? target['createdAt'] : target.createdAt; + + if (result['createdAt'] is DateTime) + result['createdAt'] = result['createdAt'].toIso8601String(); + + result['updatedAt'] = new DateTime.now().toIso8601String();*/ + + try { + var updated = await collection.findAndModify( + query: where.id(_makeId(id)), + update: result, + returnNew: true, + upsert: true); + result = _jsonify(updated, params); + result['id'] = _makeId(id).toHexString(); + return result; + } catch (e, st) { + //printDebug(e, st, 'UPDATE'); + throw new AngelHttpException(e, stackTrace: st); + } + } + + @override + Future> remove(String id, + [Map 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 collection.remove(null); + return {}; + } + } + + // var result = await read(id, params); + + try { + var result = await collection.findAndModify( + query: where.id(_makeId(id)), remove: true); + return _jsonify(result); + } catch (e, st) { + //printDebug(e, st, 'REMOVE'); + throw new AngelHttpException(e, stackTrace: st); + } + } +} diff --git a/packages/mongo/lib/services.dart b/packages/mongo/lib/services.dart new file mode 100644 index 00000000..a44dedd4 --- /dev/null +++ b/packages/mongo/lib/services.dart @@ -0,0 +1,52 @@ +library angel_mongo.services; + +import 'dart:async'; +import 'package:angel_framework/angel_framework.dart'; +import 'package:merge_map/merge_map.dart'; +import 'package:mongo_dart/mongo_dart.dart'; + +part 'mongo_service.dart'; + +Map _transformId(Map doc) { + var result = new Map.from(doc); + result + ..['id'] = doc['_id'] + ..remove('_id'); + + return result; +} + +ObjectId _makeId(id) { + try { + return (id is ObjectId) ? id : new ObjectId.fromHexString(id.toString()); + } catch (e) { + throw new AngelHttpException.badRequest(); + } +} + +const List _sensitiveFieldNames = const [ + 'id', + '_id', + 'createdAt', + 'updatedAt' +]; + +Map _removeSensitive(Map data) { + return data.keys + .where((k) => !_sensitiveFieldNames.contains(k)) + .fold({}, (map, key) => map..[key] = data[key]); +} + +const List _NO_QUERY = const ['__requestctx', '__responsectx']; + +Map _filterNoQuery(Map data) { + return data.keys.fold({}, (map, key) { + var value = data[key]; + + if (_NO_QUERY.contains(key) || + value is RequestContext || + value is ResponseContext) return map; + if (key is! Map) return map..[key] = value; + return map..[key] = _filterNoQuery(value as Map); + }); +} diff --git a/packages/mongo/pubspec.yaml b/packages/mongo/pubspec.yaml new file mode 100644 index 00000000..4b9a9e42 --- /dev/null +++ b/packages/mongo/pubspec.yaml @@ -0,0 +1,16 @@ +name: angel_mongo +version: 2.0.3 +description: MongoDB-enabled services for the Angel framework. Well-tested. +author: Tobe O +homepage: https://github.com/angel-dart/angel_mongo +environment: + sdk: ">=2.0.0-dev <3.0.0" +dependencies: + angel_framework: ^2.0.0-alpha + json_god: ">=2.0.0-beta <3.0.0" + merge_map: ^1.0.0 + mongo_dart: ">= 0.2.7 < 1.0.0" +dev_dependencies: + http: ">= 0.11.3 < 0.12.0" + pedantic: ^1.0.0 + test: ^1.0.0 diff --git a/packages/mongo/test/generic_test.dart b/packages/mongo/test/generic_test.dart new file mode 100644 index 00000000..74d3adba --- /dev/null +++ b/packages/mongo/test/generic_test.dart @@ -0,0 +1,206 @@ +import 'package:angel_framework/angel_framework.dart'; +import 'package:angel_framework/http.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'; + +final headers = { + 'accept': 'application/json', + 'content-type': 'application/json' +}; + +final Map testGreeting = {'to': 'world'}; + +wireHooked(HookedService hooked) { + hooked.afterAll((HookedServiceEvent event) { + print("Just ${event.eventName}: ${event.result}"); + print('Params: ${event.params}'); + }); +} + +main() { + group('Generic Tests', () { + Angel app; + AngelHttp transport; + http.Client client; + Db db = new Db('mongodb://localhost:27017/angel_mongo'); + DbCollection testData; + String url; + HookedService, MongoService> greetingService; + + setUp(() async { + app = new Angel(); + transport = new AngelHttp(app); + client = new http.Client(); + await db.open(); + testData = db.collection('test_data'); + // Delete anything before we start + await testData.remove({}); + + var service = new MongoService(testData, debug: true); + greetingService = new HookedService(service); + wireHooked(greetingService); + + app.use('/api', greetingService); + + await transport.startServer('127.0.0.1', 0); + url = transport.uri.toString(); + }); + + tearDown(() async { + // Delete anything left over + await testData.remove({}); + await db.close(); + await transport.close(); + client = null; + url = null; + greetingService = null; + }); + + test('query fields mapped to filters', () async { + await greetingService.create({'foo': 'bar'}); + expect( + await greetingService.index({ + 'query': {'foo': 'not bar'} + }), + isEmpty, + ); + expect( + await greetingService.index(), + isNotEmpty, + ); + }); + + test('insert items', () async { + var response = await client.post("$url/api", + body: god.serialize(testGreeting), headers: headers); + expect(response.statusCode, isIn([200, 201])); + + response = await client.get("$url/api"); + expect(response.statusCode, isIn([200, 201])); + var users = god.deserialize(response.body, + outputType: [].runtimeType) as List; + expect(users.length, equals(1)); + }); + + test('read item', () async { + var response = await client.post("$url/api", + body: god.serialize(testGreeting), headers: headers); + expect(response.statusCode, isIn([200, 201])); + var created = god.deserialize(response.body) as Map; + + response = await client.get("$url/api/${created['id']}"); + expect(response.statusCode, isIn([200, 201])); + var read = god.deserialize(response.body) as Map; + expect(read['id'], equals(created['id'])); + expect(read['to'], equals('world')); + //expect(read['createdAt'], isNot(null)); + }); + + test('findOne', () async { + var response = await client.post("$url/api", + body: god.serialize(testGreeting), headers: headers); + expect(response.statusCode, isIn([200, 201])); + var created = god.deserialize(response.body) as Map; + + var id = new ObjectId.fromHexString(created['id'] as String); + var read = await greetingService.findOne({'query': where.id(id)}); + expect(read['id'], equals(created['id'])); + expect(read['to'], equals('world')); + //expect(read['createdAt'], isNot(null)); + }); + + test('readMany', () async { + var response = await client.post("$url/api", + body: god.serialize(testGreeting), headers: headers); + expect(response.statusCode, isIn([200, 201])); + var created = god.deserialize(response.body) as Map; + + var id = new ObjectId.fromHexString(created['id'] as String); + var read = await greetingService.readMany([id.toHexString()]); + expect(read, [created]); + //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, isIn([200, 201])); + var created = god.deserialize(response.body) as Map; + + response = await client.patch("$url/api/${created['id']}", + body: god.serialize({"to": "Mom"}), headers: headers); + var modified = god.deserialize(response.body) as Map; + expect(response.statusCode, isIn([200, 201])); + 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, isIn([200, 201])); + var created = god.deserialize(response.body) as Map; + + response = await client.post("$url/api/${created['id']}", + body: god.serialize({"to": "Updated"}), headers: headers); + var modified = god.deserialize(response.body) as Map; + expect(response.statusCode, isIn([200, 201])); + expect(modified['id'], equals(created['id'])); + expect(modified['to'], equals('Updated')); + //expect(modified['updatedAt'], isNot(null)); + }); + + test('remove item', () async { + var response = await client.post("$url/api", + body: god.serialize(testGreeting), headers: headers); + var created = god.deserialize(response.body) as Map; + + int lastCount = (await greetingService.index()).length; + + await client.delete("$url/api/${created['id']}"); + expect((await greetingService.index()).length, equals(lastCount - 1)); + }); + + test('cannot remove all unless explicitly set', () async { + var response = await client.delete('$url/api/null'); + expect(response.statusCode, 403); + }); + + test('\$sort and query parameters', () async { + // Search by where.eq + Map world = await greetingService.create({"to": "world"}); + await greetingService.create({"to": "Mom"}); + await greetingService.create({"to": "Updated"}); + + var response = await client.get("$url/api?to=world"); + print(response.body); + var queried = god.deserialize(response.body, + outputType: [].runtimeType) as List; + expect(queried.length, equals(1)); + expect(queried[0].keys.length, equals(2)); + expect(queried[0]["id"], equals(world["id"])); + expect(queried[0]["to"], equals(world["to"])); + //expect(queried[0]["createdAt"], equals(world["createdAt"])); + + /*response = await client.get("$url/api?\$sort.createdAt=-1"); + print(response.body); + queried = god.deserialize(response.body); + expect(queried[0]["id"], equals(Updated["id"])); + expect(queried[1]["id"], equals(Mom["id"])); + expect(queried[2]["id"], equals(world["id"]));*/ + + queried = await greetingService.index({ + "\$query": { + "_id": where.id(new ObjectId.fromHexString(world["id"] as String)) + } + }); + print(queried); + expect(queried.length, equals(1)); + expect(queried[0], equals(world)); + }); + }); +}