diff --git a/.analysis_options b/.analysis_options index 518eb901a..f4bbaee15 100644 --- a/.analysis_options +++ b/.analysis_options @@ -1,2 +1,4 @@ analyzer: - strong-mode: true \ No newline at end of file + strong-mode: true + exclude: + - tmp_templates/** \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 751dd43cf..6441344c0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,8 @@ before_script: - psql -c "alter user dart with password 'dart';" -U postgres - psql -c 'grant all on database dart_test to dart;' -U postgres - pub get -script: pub run test -j 1 -r expanded +script: bash ci/script.sh +after_success: bash ci/after_script.sh branches: only: - master diff --git a/CHANGELOG.md b/CHANGELOG.md index 72df4882b..6d69f111f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # aqueduct changelog +## 1.0.4 +- BREAKING CHANGE: Added new `Response.contentType` property. Adding "Content-Type" to the headers of a `Response` no longer has any effect; use this property instead. +- `ManagedDataModel`s now scan all libraries for `ManagedObject` subclasses to generate a data model. Use `ManagedDataModel.fromCurrentMirrorSystem` to create instances of `ManagedDataModel`. +- The *last* instantiated `ManagedContext` now becomes the `ManagedContext.defaultContext`; prior to this change, it was the first instantiated context. Added `ManagedContext.standalone` to opt out of setting the default context. +- @HTTPQuery parameters in HTTPController responder method will now only allow multiple keys in the query string if and only if the argument type is a List. + ## 1.0.3 - Fix to allow Windows user to use `aqueduct setup`. - Fix to CORS processing. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..6d0adc87b --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,7 @@ +### Running Tests + +A local database must exist, configured using the same script in .travis.yml. + +Tests are run with the following command: + + pub run test -j 1 diff --git a/bin/aqueduct.dart b/bin/aqueduct.dart index 4dc8bb5c9..1b87b0959 100644 --- a/bin/aqueduct.dart +++ b/bin/aqueduct.dart @@ -1,8 +1,9 @@ -import 'dart:async'; -import 'package:args/args.dart'; +import 'dart:io'; + import 'package:aqueduct/aqueduct.dart'; +import 'package:args/args.dart'; -Future main(List args) async { +main(List args) async { var templateCreator = new CLITemplateCreator(); var migrationRunner = new CLIMigrationRunner(); var setupCommand = new CLISetup(); @@ -19,16 +20,20 @@ Future main(List args) async { if (values.command == null) { print( "Invalid command, options are: ${totalParser.commands.keys.join(", ")}"); - return -1; + exitCode = 1; + return; } else if (values.command.name == "create") { - return await templateCreator.process(values.command); + exitCode = await templateCreator.process(values.command); + return; } else if (values.command.name == "db") { - return await migrationRunner.process(values.command); + exitCode = await migrationRunner.process(values.command); + return; } else if (values.command.name == "setup") { - return await setupCommand.process(values.command); + exitCode = await setupCommand.process(values.command); + return; } print( "Invalid command, options are: ${totalParser.commands.keys.join(", ")}"); - return -1; + exitCode = 1; } diff --git a/ci/after_script.sh b/ci/after_script.sh new file mode 100644 index 000000000..6bc465cce --- /dev/null +++ b/ci/after_script.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +if [[ "$TRAVIS_BRANCH" == "master" ]]; then + curl -s https://codecov.io/bash > .codecov + chmod +x .codecov + ./.codecov -f lcov.info -X xcode +fi \ No newline at end of file diff --git a/ci/script.sh b/ci/script.sh new file mode 100644 index 000000000..4510e9924 --- /dev/null +++ b/ci/script.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +pub run test -j 1 -r expanded +if [[ "$TRAVIS_BRANCH" == "master" ]]; then + pub global activate -sgit https://github.com/stablekernel/codecov_dart.git + dart_codecov_generator --report-on=lib/ --verbose --no-html +fi \ No newline at end of file diff --git a/example/templates/default/bin/start.dart b/example/templates/default/bin/start.dart index bfee4140b..f3b550d82 100644 --- a/example/templates/default/bin/start.dart +++ b/example/templates/default/bin/start.dart @@ -23,9 +23,6 @@ main() async { var signalPath = new File(".aqueductsignal"); await signalPath.writeAsString("ok"); - } on ApplicationSupervisorException catch (e, st) { - await writeError( - "IsolateSupervisorException, server failed to start: ${e.message} $st"); } catch (e, st) { await writeError("Server failed to start: $e $st"); } diff --git a/example/templates/default/lib/src/controller/identity_controller.dart b/example/templates/default/lib/controller/identity_controller.dart similarity index 92% rename from example/templates/default/lib/src/controller/identity_controller.dart rename to example/templates/default/lib/controller/identity_controller.dart index b5359a27f..a14bc386d 100644 --- a/example/templates/default/lib/src/controller/identity_controller.dart +++ b/example/templates/default/lib/controller/identity_controller.dart @@ -1,4 +1,4 @@ -part of wildfire; +import '../wildfire.dart'; class IdentityController extends HTTPController { @httpGet diff --git a/example/templates/default/lib/src/controller/register_controller.dart b/example/templates/default/lib/controller/register_controller.dart similarity index 83% rename from example/templates/default/lib/src/controller/register_controller.dart rename to example/templates/default/lib/controller/register_controller.dart index ae3b9dab4..52a4eaebe 100644 --- a/example/templates/default/lib/src/controller/register_controller.dart +++ b/example/templates/default/lib/controller/register_controller.dart @@ -1,12 +1,14 @@ -part of wildfire; +import '../wildfire.dart'; class RegisterController extends QueryController { @httpPost createUser() async { - if (query.values.username == null || query.values.password == null) { + if (query.values.email == null || query.values.password == null) { return new Response.badRequest( - body: {"error": "Username and password required."}); + body: {"error": "email and password required."}); } + var credentials = AuthorizationBasicParser + .parse(request.innerRequest.headers.value(HttpHeaders.AUTHORIZATION)); var salt = AuthServer.generateRandomSalt(); var hashedPassword = @@ -15,9 +17,6 @@ class RegisterController extends QueryController { query.values.salt = salt; var u = await query.insert(); - - var credentials = AuthorizationBasicParser - .parse(request.innerRequest.headers.value(HttpHeaders.AUTHORIZATION)); var token = await request.authorization.grantingServer.authenticate( u.username, query.values.password, diff --git a/example/templates/default/lib/src/controller/user_controller.dart b/example/templates/default/lib/controller/user_controller.dart similarity index 96% rename from example/templates/default/lib/src/controller/user_controller.dart rename to example/templates/default/lib/controller/user_controller.dart index bc775ba7b..cce562287 100644 --- a/example/templates/default/lib/src/controller/user_controller.dart +++ b/example/templates/default/lib/controller/user_controller.dart @@ -1,4 +1,4 @@ -part of wildfire; +import '../wildfire.dart'; class UserController extends QueryController { @httpGet diff --git a/example/templates/default/lib/src/model/token.dart b/example/templates/default/lib/model/token.dart similarity index 98% rename from example/templates/default/lib/src/model/token.dart rename to example/templates/default/lib/model/token.dart index ebf746f91..d05af2362 100644 --- a/example/templates/default/lib/src/model/token.dart +++ b/example/templates/default/lib/model/token.dart @@ -1,4 +1,4 @@ -part of wildfire; +import '../wildfire.dart'; class AuthCode extends ManagedObject<_AuthCode> implements _AuthCode {} diff --git a/example/templates/default/lib/src/model/user.dart b/example/templates/default/lib/model/user.dart similarity index 94% rename from example/templates/default/lib/src/model/user.dart rename to example/templates/default/lib/model/user.dart index 04cce11c1..16d73db84 100644 --- a/example/templates/default/lib/src/model/user.dart +++ b/example/templates/default/lib/model/user.dart @@ -1,4 +1,4 @@ -part of wildfire; +import '../wildfire.dart'; class User extends ManagedObject<_User> implements _User, Authenticatable { @managedTransientInputAttribute diff --git a/example/templates/default/lib/src/wildfire_sink.dart b/example/templates/default/lib/src/wildfire_sink.dart deleted file mode 100644 index 6d3f9d60b..000000000 --- a/example/templates/default/lib/src/wildfire_sink.dart +++ /dev/null @@ -1,84 +0,0 @@ -part of wildfire; - -class WildfireConfiguration extends ConfigurationItem { - WildfireConfiguration(String fileName) : super.fromFile(fileName); - - DatabaseConnectionConfiguration database; - int port; -} - -class WildfireSink extends RequestSink { - static const String ConfigurationKey = "ConfigurationKey"; - static const String LoggingTargetKey = "LoggingTargetKey"; - - WildfireSink(Map opts) : super(opts) { - configuration = opts[ConfigurationKey]; - - LoggingTarget target = opts[LoggingTargetKey]; - target?.bind(logger); - - context = contextWithConnectionInfo(configuration.database); - - authenticationServer = new AuthServer( - new WildfireAuthenticationDelegate()); - } - - ManagedContext context; - AuthServer authenticationServer; - WildfireConfiguration configuration; - - @override - void setupRouter(Router router) { - router - .route("/auth/token") - .pipe( - new Authorizer(authenticationServer, strategy: AuthStrategy.client)) - .generate(() => new AuthController(authenticationServer)); - - router - .route("/auth/code") - .pipe( - new Authorizer(authenticationServer, strategy: AuthStrategy.client)) - .generate(() => new AuthCodeController(authenticationServer)); - - router - .route("/identity") - .pipe(new Authorizer(authenticationServer)) - .generate(() => new IdentityController()); - - router - .route("/register") - .pipe( - new Authorizer(authenticationServer, strategy: AuthStrategy.client)) - .generate(() => new RegisterController()); - - router - .route("/users/[:id]") - .pipe(new Authorizer(authenticationServer)) - .generate(() => new UserController()); - } - - ManagedContext contextWithConnectionInfo( - DatabaseConnectionConfiguration database) { - var connectionInfo = configuration.database; - var dataModel = - new ManagedDataModel.fromPackageContainingType(this.runtimeType); - var psc = new PostgreSQLPersistentStore.fromConnectionInfo( - connectionInfo.username, - connectionInfo.password, - connectionInfo.host, - connectionInfo.port, - connectionInfo.databaseName); - - var ctx = new ManagedContext(dataModel, psc); - ManagedContext.defaultContext = ctx; - - return ctx; - } - - @override - Map documentSecuritySchemes( - PackagePathResolver resolver) { - return authenticationServer.documentSecuritySchemes(resolver); - } -} diff --git a/example/templates/default/lib/src/utilities/auth_delegate.dart b/example/templates/default/lib/utilities/auth_delegate.dart similarity index 98% rename from example/templates/default/lib/src/utilities/auth_delegate.dart rename to example/templates/default/lib/utilities/auth_delegate.dart index 26c7108a5..2ef7f6bff 100644 --- a/example/templates/default/lib/src/utilities/auth_delegate.dart +++ b/example/templates/default/lib/utilities/auth_delegate.dart @@ -1,6 +1,6 @@ -part of wildfire; +import '../wildfire.dart'; -class WildfireAuthenticationDelegate +class WildfireAuthDelegate implements AuthServerDelegate { Future clientForID(AuthServer server, String id) async { var clientQ = new Query()..matchOn.id = id; diff --git a/example/templates/default/lib/wildfire.dart b/example/templates/default/lib/wildfire.dart index 059507aa8..3b81a9fb7 100644 --- a/example/templates/default/lib/wildfire.dart +++ b/example/templates/default/lib/wildfire.dart @@ -6,18 +6,17 @@ /// A web server. library wildfire; -import 'dart:io'; -import 'dart:async'; -import 'package:aqueduct/aqueduct.dart'; -import 'package:scribe/scribe.dart'; +export 'dart:async'; +export 'dart:io'; export 'package:aqueduct/aqueduct.dart'; export 'package:scribe/scribe.dart'; -part 'src/model/token.dart'; -part 'src/model/user.dart'; -part 'src/wildfire_sink.dart'; -part 'src/controller/user_controller.dart'; -part 'src/controller/identity_controller.dart'; -part 'src/controller/register_controller.dart'; -part 'src/utilities/auth_delegate.dart'; +export 'wildfire_sink.dart'; +export 'model/token.dart'; +export 'model/user.dart'; +export 'controller/user_controller.dart'; +export 'controller/identity_controller.dart'; +export 'controller/register_controller.dart'; +export 'utilities/auth_delegate.dart'; + diff --git a/example/templates/default/lib/wildfire_sink.dart b/example/templates/default/lib/wildfire_sink.dart new file mode 100644 index 000000000..281e7e563 --- /dev/null +++ b/example/templates/default/lib/wildfire_sink.dart @@ -0,0 +1,104 @@ +import 'wildfire.dart'; + +/// This class handles setting up this application. +/// +/// Override methods from [RequestSink] to set up the resources your +/// application uses and the routes it exposes. +/// +/// Instances of this class are the type argument to [Application]. +/// See http://stablekernel.github.io/aqueduct/http/request_sink.html +/// for more details. +/// +/// See bin/start.dart for usage. +class WildfireSink extends RequestSink { + static const String ConfigurationKey = "ConfigurationKey"; + static const String LoggingTargetKey = "LoggingTargetKey"; + + /// [Application] creates instances of this type with this constructor. + /// + /// The options will be the values set in the spawning [Application]'s + /// [Application.configuration] [ApplicationConfiguration.configurationOptions]. + /// See bin/start.dart. + WildfireSink(Map opts) : super(opts) { + WildfireConfiguration configuration = opts[ConfigurationKey]; + + LoggingTarget target = opts[LoggingTargetKey]; + target?.bind(logger); + + context = contextWithConnectionInfo(configuration.database); + + authServer = new AuthServer( + new WildfireAuthDelegate()); + } + + ManagedContext context; + AuthServer authServer; + + /// All routes must be configured in this method. + @override + void setupRouter(Router router) { + router + .route("/auth/token") + .pipe( + new Authorizer(authServer, strategy: AuthStrategy.client)) + .generate(() => new AuthController(authServer)); + + router + .route("/auth/code") + .pipe( + new Authorizer(authServer, strategy: AuthStrategy.client)) + .generate(() => new AuthCodeController(authServer)); + + router + .route("/identity") + .pipe(new Authorizer(authServer)) + .generate(() => new IdentityController()); + + router + .route("/register") + .pipe( + new Authorizer(authServer, strategy: AuthStrategy.client)) + .generate(() => new RegisterController()); + + router + .route("/users/[:id]") + .pipe(new Authorizer(authServer)) + .generate(() => new UserController()); + } + + ManagedContext contextWithConnectionInfo( + DatabaseConnectionConfiguration connectionInfo) { + var dataModel = + new ManagedDataModel.fromCurrentMirrorSystem(); + var psc = new PostgreSQLPersistentStore.fromConnectionInfo( + connectionInfo.username, + connectionInfo.password, + connectionInfo.host, + connectionInfo.port, + connectionInfo.databaseName); + + var ctx = new ManagedContext(dataModel, psc); + ManagedContext.defaultContext = ctx; + + return ctx; + } + + @override + Map documentSecuritySchemes( + PackagePathResolver resolver) { + return authServer.documentSecuritySchemes(resolver); + } +} + +/// An instance of this class represents values from a configuration +/// file specific to this application. +/// +/// Configuration files must have key-value for the properties in this class. +/// For more documentation on configuration files, see +/// https://pub.dartlang.org/packages/safe_config. +class WildfireConfiguration extends ConfigurationItem { + WildfireConfiguration(String fileName) : super.fromFile(fileName); + + DatabaseConnectionConfiguration database; + int port; +} \ No newline at end of file diff --git a/example/templates/default/test/mock/startup.dart b/example/templates/default/test/harness/app.dart similarity index 50% rename from example/templates/default/test/mock/startup.dart rename to example/templates/default/test/harness/app.dart index a9b17e0d0..79acf74a2 100644 --- a/example/templates/default/test/mock/startup.dart +++ b/example/templates/default/test/harness/app.dart @@ -1,8 +1,18 @@ import 'package:wildfire/wildfire.dart'; -import 'package:scribe/scribe.dart'; -import 'dart:async'; +export 'package:wildfire/wildfire.dart'; +export 'package:test/test.dart'; +/// A testing harness for wildfire. +/// +/// Use instances of this class to start/stop the test wildfire server. Use [client] to execute +/// requests against the test server. This instance will create a temporary version of your +/// code's current database schema during startup. This instance will use configuration values +/// from config.yaml.src. class TestApplication { + + /// Creates an instance of this class. + /// + /// Reads configuration values from config.yaml.src. See [start] for usage. TestApplication() { configuration = new WildfireConfiguration("config.yaml.src"); configuration.database.isTemporary = true; @@ -14,6 +24,19 @@ class TestApplication { TestClient client; WildfireConfiguration configuration; + /// Starts running this test harness. + /// + /// This method will start a [LoggingServer] and an [Application] with [WildfireSink]. + /// It will also setup a temporary database connection to the database described in + /// config.yaml.src. The current declared [ManagedObject]s in this application will be + /// used to generate a temporary database schema. The [WildfireSink] instance will use + /// this temporary database. Stopping this application will remove the data from the + /// temporary database. + /// + /// An initial client ID/secret pair will be generated and added to the database + /// for the [client] to use. This value is "com.aqueduct.test"/"kilimanjaro". + /// + /// You must call [stop] on this instance when tearing down your tests. Future start() async { await logger.start(); @@ -35,13 +58,21 @@ class TestApplication { ..clientSecret = "kilimanjaro"; } + /// Stops running this application harness. + /// + /// This method must be called during test tearDown. Future stop() async { await sink.context.persistentStore?.close(); await logger?.stop(); await application?.stop(); } - static Future addClientRecord( + /// Adds a client id/secret pair to the temporary database. + /// + /// [start] must have already been called prior to executing this method. By default, + /// every application harness inserts a default client record during [start]. See [start] + /// for more details. + static Future addClientRecord( {String clientID: "com.aqueduct.test", String clientSecret: "kilimanjaro"}) async { var salt = AuthServer.generateRandomSalt(); @@ -55,9 +86,12 @@ class TestApplication { ..values.id = clientID ..values.salt = salt ..values.hashedPassword = hashedPassword; - await clientQ.insert(); + return await clientQ.insert(); } + /// Adds database tables to the temporary test database based on the declared [ManagedObject]s in this application. + /// + /// This method is executed during [start], and you shouldn't have to invoke it yourself. static Future createDatabaseSchema( ManagedContext context, Logger logger) async { var builder = new SchemaBuilder.toSchema( diff --git a/example/templates/default/test/identity_controller_test.dart b/example/templates/default/test/identity_controller_test.dart index 76b795951..4097290d5 100644 --- a/example/templates/default/test/identity_controller_test.dart +++ b/example/templates/default/test/identity_controller_test.dart @@ -1,8 +1,4 @@ -import 'dart:async'; - -import 'package:test/test.dart'; -import 'package:wildfire/wildfire.dart'; -import 'mock/startup.dart'; +import 'harness/app.dart'; Future main() async { group("Success cases", () { diff --git a/example/templates/default/test/register_test.dart b/example/templates/default/test/register_test.dart index 04060bd58..34f327a05 100644 --- a/example/templates/default/test/register_test.dart +++ b/example/templates/default/test/register_test.dart @@ -1,6 +1,4 @@ -import 'package:test/test.dart'; -import 'package:wildfire/wildfire.dart'; -import 'mock/startup.dart'; +import 'harness/app.dart'; main() { group("Success cases", () { diff --git a/example/templates/default/test/user_controller_test.dart b/example/templates/default/test/user_controller_test.dart index cc99c8fc3..670526a86 100644 --- a/example/templates/default/test/user_controller_test.dart +++ b/example/templates/default/test/user_controller_test.dart @@ -1,9 +1,5 @@ -import 'dart:async'; - -import 'package:test/test.dart'; -import 'package:wildfire/wildfire.dart'; +import 'harness/app.dart'; import 'dart:convert'; -import 'mock/startup.dart'; Future main() async { group("Success cases", () { diff --git a/lib/application/isolate_server.dart b/lib/application/isolate_server.dart deleted file mode 100644 index 05e9906b6..000000000 --- a/lib/application/isolate_server.dart +++ /dev/null @@ -1,104 +0,0 @@ -part of aqueduct; - -/// Used internally. -class ApplicationServer { - ApplicationConfiguration configuration; - HttpServer server; - RequestSink sink; - int identifier; - Logger get logger => new Logger("aqueduct"); - - ApplicationServer(this.sink, this.configuration, this.identifier) { - sink.server = this; - } - - Future start() async { - try { - sink.setupRouter(sink.router); - sink.router?.finalize(); - sink.nextController = sink.initialController; - - if (configuration.securityContext != null) { - server = await HttpServer.bindSecure(configuration.address, - configuration.port, configuration.securityContext, - requestClientCertificate: configuration.isUsingClientCertificate, - v6Only: configuration.isIpv6Only, - shared: configuration._shared); - } else { - server = await HttpServer.bind( - configuration.address, configuration.port, - v6Only: configuration.isIpv6Only, shared: configuration._shared); - } - - server.autoCompress = true; - await didOpen(); - } catch (e) { - await server?.close(force: true); - rethrow; - } - } - - Future didOpen() async { - logger.info("Server aqueduct/$identifier started."); - - server.serverHeader = "aqueduct/${this.identifier}"; - - await sink.willOpen(); - - server.map((baseReq) => new Request(baseReq)).listen((Request req) async { - logger.fine("Request received $req.", req); - await sink.willReceiveRequest(req); - sink.receive(req); - }); - - sink.didOpen(); - } -} - -/// Used internally. -class ApplicationIsolateServer extends ApplicationServer { - SendPort supervisingApplicationPort; - ReceivePort supervisingReceivePort; - - ApplicationIsolateServer( - RequestSink sink, - ApplicationConfiguration configuration, - int identifier, - this.supervisingApplicationPort) - : super(sink, configuration, identifier) { - sink.server = this; - supervisingReceivePort = new ReceivePort(); - supervisingReceivePort.listen(listener); - } - - @override - Future didOpen() async { - await super.didOpen(); - - supervisingApplicationPort.send(supervisingReceivePort.sendPort); - } - - void listener(dynamic message) { - if (message == ApplicationIsolateSupervisor._MessageStop) { - server.close(force: true).then((s) { - supervisingApplicationPort - .send(ApplicationIsolateSupervisor._MessageStop); - }); - } - } -} - -/// This method is used internally. -void isolateServerEntryPoint(ApplicationInitialServerMessage params) { - var sinkSourceLibraryMirror = - currentMirrorSystem().libraries[params.streamLibraryURI]; - var sinkTypeMirror = sinkSourceLibraryMirror.declarations[ - new Symbol(params.streamTypeName)] as ClassMirror; - - var app = sinkTypeMirror.newInstance( - new Symbol(""), [params.configuration.configurationOptions]).reflectee; - - var server = new ApplicationIsolateServer( - app, params.configuration, params.identifier, params.parentMessagePort); - server.start(); -} diff --git a/lib/aqueduct.dart b/lib/aqueduct.dart index 9108a013c..a3680a27e 100644 --- a/lib/aqueduct.dart +++ b/lib/aqueduct.dart @@ -26,88 +26,15 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND /// application: Classes in this module begin with 'Application' and are responsible for starting and stopping web servers on a number of isolates. library aqueduct; -import 'dart:async'; -import 'dart:collection'; -import 'dart:convert'; -import 'dart:io'; -import 'dart:isolate'; -import 'dart:math'; -import 'dart:mirrors'; - -import 'package:meta/meta.dart'; -import 'package:analyzer/analyzer.dart'; -import 'package:args/args.dart'; -import 'package:crypto/crypto.dart'; -import 'package:logging/logging.dart'; -import 'package:matcher/matcher.dart'; -import 'package:postgres/postgres.dart'; -import 'package:safe_config/safe_config.dart'; -import 'package:yaml/yaml.dart'; -import 'package:path/path.dart' as path_lib; - -import 'utilities/mirror_helpers.dart'; -import 'utilities/token_generator.dart'; - export 'package:logging/logging.dart'; export 'package:safe_config/safe_config.dart'; -part 'application/application.dart'; -part 'application/application_configuration.dart'; -part 'application/isolate_server.dart'; -part 'application/isolate_supervisor.dart'; -part 'auth/auth_code_controller.dart'; -part 'auth/auth_controller.dart'; -part 'auth/authentication_server.dart'; -part 'auth/authorizer.dart'; -part 'auth/authorization_parser.dart'; -part 'auth/client.dart'; -part 'auth/protocols.dart'; -part 'commands/cli_command.dart'; -part 'commands/migration_runner.dart'; -part 'commands/setup_command.dart'; -part 'commands/template_creator.dart'; -part 'db/managed/attributes.dart'; -part 'db/managed/backing.dart'; -part 'db/managed/context.dart'; -part 'db/managed/data_model.dart'; -part 'db/managed/data_model_builder.dart'; -part 'db/managed/entity.dart'; -part 'db/managed/object.dart'; -part 'db/managed/property_description.dart'; -part 'db/managed/set.dart'; -part 'db/persistent_store/persistent_store.dart'; -part 'db/persistent_store/persistent_store_query.dart'; -part 'db/postgresql/postgresql_persistent_store.dart'; -part 'db/postgresql/postgresql_schema_generator.dart'; -part 'db/query/matcher_expression.dart'; -part 'db/query/page.dart'; -part 'db/query/predicate.dart'; -part 'db/query/query.dart'; -part 'db/query/sort_descriptor.dart'; -part 'db/schema/migration.dart'; -part 'db/schema/schema.dart'; -part 'db/schema/schema_builder.dart'; -part 'db/schema/schema_column.dart'; -part 'db/schema/schema_table.dart'; -part 'http/body_decoder.dart'; -part 'http/controller_routing.dart'; -part 'http/cors_policy.dart'; -part 'http/documentable.dart'; -part 'http/http_controller.dart'; -part 'http/http_response_exception.dart'; -part 'http/query_controller.dart'; -part 'http/parameter_matching.dart'; -part 'http/request.dart'; -part 'http/request_controller.dart'; -part 'http/request_path.dart'; -part 'http/request_sink.dart'; -part 'http/resource_controller.dart'; -part 'http/response.dart'; -part 'http/route_node.dart'; -part 'http/router.dart'; -part 'http/serializable.dart'; -part 'utilities/mock_server.dart'; -part 'utilities/pbkdf2.dart'; -part 'utilities/source_generator.dart'; -part 'utilities/test_client.dart'; -part 'utilities/test_matchers.dart'; +export 'src/application/application.dart'; +export 'src/auth/auth.dart'; +export 'src/commands/cli_command.dart'; +export 'src/db/db.dart'; +export 'src/http/http.dart'; +export 'src/utilities/mock_server.dart'; +export 'src/utilities/pbkdf2.dart'; +export 'src/utilities/test_client.dart'; +export 'src/utilities/test_matchers.dart'; diff --git a/lib/db/persistent_store/persistent_store_query.dart b/lib/db/persistent_store/persistent_store_query.dart deleted file mode 100644 index e2e09766e..000000000 --- a/lib/db/persistent_store/persistent_store_query.dart +++ /dev/null @@ -1,230 +0,0 @@ -part of aqueduct; - -/// This enumeration is used internaly. -enum PersistentJoinType { leftOuter } - -/// This class is used internally to map [Query] to something a [PersistentStore] can execute. -class PersistentStoreQuery { - PersistentStoreQuery(this.rootEntity, PersistentStore store, Query q) { - confirmQueryModifiesAllInstancesOnDeleteOrUpdate = - q.confirmQueryModifiesAllInstancesOnDeleteOrUpdate; - timeoutInSeconds = q.timeoutInSeconds; - sortDescriptors = q.sortDescriptors; - resultKeys = _mappingElementsForList( - (q.resultProperties ?? rootEntity.defaultProperties), rootEntity); - - if (q._matchOn != null) { - predicate = new QueryPredicate._fromQueryIncludable(q._matchOn, store); - } else { - predicate = q.predicate; - } - - if (q._matchOn?._hasJoinElements ?? false) { - var joinElements = _joinElementsFromQueryMatchable( - q.matchOn, store, q.nestedResultProperties); - resultKeys.addAll(joinElements); - - if (q.pageDescriptor != null) { - throw new QueryException(QueryExceptionEvent.requestFailure, - message: - "Query cannot have properties that are includeInResultSet and also have a pageDescriptor."); - } - } else { - fetchLimit = q.fetchLimit; - offset = q.offset; - - pageDescriptor = _validatePageDescriptor(q.pageDescriptor); - - values = _mappingElementsForMap( - (q.valueMap ?? q.values?.backingMap), rootEntity); - } - } - - int offset = 0; - int fetchLimit = 0; - int timeoutInSeconds = 30; - bool confirmQueryModifiesAllInstancesOnDeleteOrUpdate; - ManagedEntity rootEntity; - QueryPage pageDescriptor; - QueryPredicate predicate; - List sortDescriptors; - List values; - List resultKeys; - - static ManagedPropertyDescription _propertyForName( - ManagedEntity entity, String propertyName) { - var property = entity.properties[propertyName]; - if (property == null) { - throw new QueryException(QueryExceptionEvent.internalFailure, - message: - "Property $propertyName does not exist on ${entity.tableName}"); - } - if (property is ManagedRelationshipDescription && - property.relationshipType != ManagedRelationshipType.belongsTo) { - throw new QueryException(QueryExceptionEvent.internalFailure, - message: - "Property $propertyName is a hasMany or hasOne relationship and is invalid as a result property of ${entity.tableName}, use matchOn.$propertyName.includeInResultSet = true instead."); - } - - return property; - } - - static List _mappingElementsForList( - List keys, ManagedEntity entity) { - if (!keys.contains(entity.primaryKey)) { - keys.add(entity.primaryKey); - } - - return keys.map((key) { - var property = _propertyForName(entity, key); - return new PersistentColumnMapping(property, null); - }).toList(); - } - - QueryPage _validatePageDescriptor(QueryPage page) { - if (page == null) { - return null; - } - - var prop = rootEntity.attributes[page.propertyName]; - if (prop == null) { - throw new QueryException(QueryExceptionEvent.requestFailure, - message: - "Property ${page.propertyName} in pageDescriptor does not exist on ${rootEntity.tableName}."); - } - - if (page.boundingValue != null && - !prop.isAssignableWith(page.boundingValue)) { - throw new QueryException(QueryExceptionEvent.requestFailure, - message: - "Property ${page.propertyName} in pageDescriptor has invalid type (${page.boundingValue.runtimeType})."); - } - - return page; - } - - List _mappingElementsForMap( - Map valueMap, ManagedEntity entity) { - return valueMap?.keys - ?.map((key) { - var property = entity.properties[key]; - if (property == null) { - throw new QueryException(QueryExceptionEvent.requestFailure, - message: - "Property $key in values does not exist on ${entity.tableName}"); - } - - var value = valueMap[key]; - if (property is ManagedRelationshipDescription) { - if (property.relationshipType != - ManagedRelationshipType.belongsTo) { - return null; - } - - if (value != null) { - if (value is ManagedObject) { - value = value[property.destinationEntity.primaryKey]; - } else if (value is Map) { - value = value[property.destinationEntity.primaryKey]; - } else { - throw new QueryException(QueryExceptionEvent.internalFailure, - message: - "Property $key on ${entity.tableName} in Query values must be a Map or ${MirrorSystem.getName(property.destinationEntity.instanceType.simpleName)} "); - } - } - } - - return new PersistentColumnMapping(property, value); - }) - ?.where((m) => m != null) - ?.toList(); - } - - static List _joinElementsFromQueryMatchable( - _QueryMatchableExtension matcherBackedObject, - PersistentStore store, - Map> nestedResultProperties) { - var entity = matcherBackedObject.entity; - var propertiesToJoin = matcherBackedObject._joinPropertyKeys; - - return propertiesToJoin - .map((propertyName) { - _QueryMatchableExtension inner = - matcherBackedObject._matcherMap[propertyName]; - - var relDesc = entity.relationships[propertyName]; - var predicate = new QueryPredicate._fromQueryIncludable(inner, store); - var nestedProperties = - nestedResultProperties[inner.entity.instanceType.reflectedType]; - var propertiesToFetch = - nestedProperties ?? inner.entity.defaultProperties; - - var joinElements = [ - new PersistentJoinMapping( - PersistentJoinType.leftOuter, - relDesc, - predicate, - _mappingElementsForList(propertiesToFetch, inner.entity)) - ]; - - if (inner._hasJoinElements) { - joinElements.addAll(_joinElementsFromQueryMatchable( - inner, store, nestedResultProperties)); - } - - return joinElements; - }) - .expand((l) => l) - .toList(); - } -} - -/// This class is used internally. -class PersistentColumnMapping { - PersistentColumnMapping(this.property, this.value); - PersistentColumnMapping.fromElement( - PersistentColumnMapping original, this.value) { - property = original.property; - } - - ManagedPropertyDescription property; - dynamic value; - - String toString() { - return "MappingElement on $property (Value = $value)"; - } -} - -/// This class is used internally. -class PersistentJoinMapping extends PersistentColumnMapping { - PersistentJoinMapping(this.type, ManagedPropertyDescription property, - this.predicate, this.resultKeys) - : super(property, null) { - var primaryKeyElement = this.resultKeys.firstWhere((e) { - var eProp = e.property; - if (eProp is ManagedAttributeDescription) { - return eProp.isPrimaryKey; - } - return false; - }); - - primaryKeyIndex = this.resultKeys.indexOf(primaryKeyElement); - } - - PersistentJoinMapping.fromElement( - PersistentJoinMapping original, List values) - : super.fromElement(original, values) { - type = original.type; - primaryKeyIndex = original.primaryKeyIndex; - } - - PersistentJoinType type; - ManagedPropertyDescription get joinProperty => - (property as ManagedRelationshipDescription).inverseRelationship; - QueryPredicate predicate; - List resultKeys; - - int primaryKeyIndex; - List get values => - value as List; -} diff --git a/lib/application/application.dart b/lib/src/application/application.dart similarity index 78% rename from lib/application/application.dart rename to lib/src/application/application.dart index b771ba5ea..83e93d2d8 100644 --- a/lib/application/application.dart +++ b/lib/src/application/application.dart @@ -1,4 +1,18 @@ -part of aqueduct; +import 'dart:async'; +import 'dart:io'; +import 'dart:isolate'; +import 'dart:mirrors'; + +import 'package:logging/logging.dart'; + +import '../http/http.dart'; +import 'application_configuration.dart'; +import 'application_server.dart'; +import 'isolate_application_server.dart'; +import 'isolate_supervisor.dart'; + +export 'application_configuration.dart'; +export 'application_server.dart'; /// A container for web server applications. /// @@ -9,14 +23,17 @@ class Application { /// Used internally. List supervisors = []; - /// Used internally. + /// The [ApplicationServer] managing delivering HTTP requests into this application. + /// + /// This property is only valid if this application is started with runOnMainIsolate set to true in [start]. + /// Tests may access this property to examine or directly use resources of a [RequestSink]. ApplicationServer server; /// The [RequestSink] receiving requests on the main isolate. /// /// Applications run during testing are run on the main isolate. When running in this way, /// an application will only have one [RequestSinkType] receiving HTTP requests. This property is that instance. - /// If an application is running across multiple isolates, this property will be null. See [start] for more details. + /// If an application is running across multiple isolates, this property is null. See [start] for more details. RequestSinkType get mainIsolateSink => server?.sink as RequestSinkType; /// A reference to a logger. @@ -61,8 +78,6 @@ class Application { await server.start(); } else { - configuration._shared = true; - supervisors = []; try { for (int i = 0; i < numberOfInstances; i++) { @@ -136,14 +151,26 @@ class Application { } } -/// Used internally. -class ApplicationInitialServerMessage { - String streamTypeName; - Uri streamLibraryURI; - ApplicationConfiguration configuration; - SendPort parentMessagePort; - int identifier; +/// Thrown when an application encounters an exception during startup. +/// +/// Contains the original exception that halted startup. +class ApplicationStartupException implements Exception { + ApplicationStartupException(this.originalException); - ApplicationInitialServerMessage(this.streamTypeName, this.streamLibraryURI, - this.configuration, this.identifier, this.parentMessagePort); + dynamic originalException; + + String toString() => originalException.toString(); +} + +/// An exception originating from an [Isolate] within an [Application]. +@Deprecated( + "This class will become private in 1.1. Use ApplicationStartupException in its place.") +class ApplicationSupervisorException implements Exception { + ApplicationSupervisorException(this.message); + + final String message; + + String toString() { + return "$message"; + } } diff --git a/lib/application/application_configuration.dart b/lib/src/application/application_configuration.dart similarity index 91% rename from lib/application/application_configuration.dart rename to lib/src/application/application_configuration.dart index 48aae4e57..4cf99b627 100644 --- a/lib/application/application_configuration.dart +++ b/lib/src/application/application_configuration.dart @@ -1,4 +1,6 @@ -part of aqueduct; +import 'dart:io'; +import 'application.dart'; +import '../http/request_sink.dart'; /// A set of values to configure an instance of [Application]. class ApplicationConfiguration { @@ -35,7 +37,5 @@ class ApplicationConfiguration { /// /// Allows delivery of custom configuration parameters to [RequestSink] instances /// that are attached to this application. - Map configurationOptions; - - bool _shared = false; + Map configurationOptions; } diff --git a/lib/src/application/application_server.dart b/lib/src/application/application_server.dart new file mode 100644 index 000000000..e41a3d9ef --- /dev/null +++ b/lib/src/application/application_server.dart @@ -0,0 +1,93 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:logging/logging.dart'; + +import '../http/request.dart'; +import '../http/request_sink.dart'; +import 'application.dart'; +import 'application_configuration.dart'; + +/// Represents a [RequestSink] manager being used by an [Application]. +/// +/// An Aqueduct application creates instances of this type to pair an HTTP server and an +/// instance of an application-specific [RequestSink]. +class ApplicationServer { + /// The configuration this instance used to start its [sink]. + ApplicationConfiguration configuration; + + /// The underlying [HttpServer]. + HttpServer server; + + /// The instance of [RequestSink] serving requests. + RequestSink sink; + + /// The unique identifier of this instance. + /// + /// Each instance has its own identifier, a numeric value starting at 1, to identify it + /// among other instances. + int identifier; + + /// The logger of this instance + Logger get logger => new Logger("aqueduct"); + + /// Creates an instance of this type. + /// + /// You should not need to invoke this method directly. + ApplicationServer(this.sink, this.configuration, this.identifier) { + sink.server = this; + } + + /// Starts this instance, allowing it to receive HTTP requests. + /// + /// Do not invoke this method directly, [Application] instances are responsible + /// for calling this method. + Future start({bool shareHttpServer: false}) async { + try { + sink.setupRouter(sink.router); + sink.router?.finalize(); + sink.nextController = sink.initialController; + + if (configuration.securityContext != null) { + server = await HttpServer.bindSecure(configuration.address, + configuration.port, configuration.securityContext, + requestClientCertificate: configuration.isUsingClientCertificate, + v6Only: configuration.isIpv6Only, + shared: shareHttpServer); + } else { + server = await HttpServer.bind( + configuration.address, configuration.port, + v6Only: configuration.isIpv6Only, shared: shareHttpServer); + } + + server.autoCompress = true; + await didOpen(); + } catch (e) { + await server?.close(force: true); + rethrow; + } + } + + /// Invoked when this server becomes ready receive requests. + /// + /// This method will invoke [RequestSink.open] and await for it to finish. + /// Once [RequestSink.open] completes, the underlying [server]'s HTTP requests + /// will be sent to this instance's [sink]. + /// + /// [RequestSink.didOpen] is invoked after this opening has completed. + Future didOpen() async { + logger.info("Server aqueduct/$identifier started."); + + server.serverHeader = "aqueduct/${this.identifier}"; + + await sink.willOpen(); + + server.map((baseReq) => new Request(baseReq)).listen((Request req) async { + logger.fine("Request received $req.", req); + await sink.willReceiveRequest(req); + sink.receive(req); + }); + + sink.didOpen(); + } +} diff --git a/lib/src/application/isolate_application_server.dart b/lib/src/application/isolate_application_server.dart new file mode 100644 index 000000000..1477ed324 --- /dev/null +++ b/lib/src/application/isolate_application_server.dart @@ -0,0 +1,73 @@ +import 'dart:async'; +import 'dart:isolate'; +import 'dart:mirrors'; + +import '../http/request_sink.dart'; +import 'application.dart'; +import 'application_configuration.dart'; +import 'isolate_supervisor.dart'; + +class ApplicationIsolateServer extends ApplicationServer { + SendPort supervisingApplicationPort; + ReceivePort supervisingReceivePort; + + ApplicationIsolateServer( + RequestSink sink, + ApplicationConfiguration configuration, + int identifier, + this.supervisingApplicationPort) + : super(sink, configuration, identifier) { + sink.server = this; + supervisingReceivePort = new ReceivePort(); + supervisingReceivePort.listen(listener); + } + + @override + Future didOpen() async { + await super.didOpen(); + + supervisingApplicationPort.send(supervisingReceivePort.sendPort); + } + + void listener(dynamic message) { + if (message == ApplicationIsolateSupervisor.MessageStop) { + server.close(force: true).then((s) { + supervisingApplicationPort + .send(ApplicationIsolateSupervisor.MessageStop); + }); + } + } +} + +/// This method is used internally. +void isolateServerEntryPoint(ApplicationInitialServerMessage params) { + RequestSink sink; + try { + var sinkSourceLibraryMirror = + currentMirrorSystem().libraries[params.streamLibraryURI]; + var sinkTypeMirror = sinkSourceLibraryMirror.declarations[ + new Symbol(params.streamTypeName)] as ClassMirror; + + sink = sinkTypeMirror.newInstance( + new Symbol(""), [params.configuration.configurationOptions]).reflectee; + } catch (e, st) { + params.parentMessagePort.send([e, st.toString()]); + + return; + } + + var server = new ApplicationIsolateServer( + sink, params.configuration, params.identifier, params.parentMessagePort); + server.start(shareHttpServer: true); +} + +class ApplicationInitialServerMessage { + String streamTypeName; + Uri streamLibraryURI; + ApplicationConfiguration configuration; + SendPort parentMessagePort; + int identifier; + + ApplicationInitialServerMessage(this.streamTypeName, this.streamLibraryURI, + this.configuration, this.identifier, this.parentMessagePort); +} diff --git a/lib/application/isolate_supervisor.dart b/lib/src/application/isolate_supervisor.dart similarity index 70% rename from lib/application/isolate_supervisor.dart rename to lib/src/application/isolate_supervisor.dart index 8bc6294c8..49a1ac1dc 100644 --- a/lib/application/isolate_supervisor.dart +++ b/lib/src/application/isolate_supervisor.dart @@ -1,10 +1,15 @@ -part of aqueduct; +import 'dart:async'; +import 'dart:isolate'; + +import 'package:logging/logging.dart'; + +import 'application.dart'; /// Represents the supervision of a [ApplicationIsolateServer]. /// /// You should not use this class directly. class ApplicationIsolateSupervisor { - static String _MessageStop = "_MessageStop"; + static const String MessageStop = "_MessageStop"; /// Create an isntance of [ApplicationIsolateSupervisor]. ApplicationIsolateSupervisor(this.supervisingApplication, this.isolate, @@ -25,6 +30,7 @@ class ApplicationIsolateSupervisor { /// A reference to the [Logger] used by the [supervisingApplication]. Logger logger; + bool get _isLaunching => _launchCompleter != null; SendPort _serverSendPort; Completer _launchCompleter; Completer _stopCompleter; @@ -37,14 +43,19 @@ class ApplicationIsolateSupervisor { isolate.setErrorsFatal(false); isolate.resume(isolate.pauseCapability); - return _launchCompleter.future.timeout(new Duration(seconds: 30)); + return _launchCompleter.future.timeout(new Duration(seconds: 30), + onTimeout: () { + receivePort.close(); + throw new TimeoutException("Isolate failed to launch in 30 seconds."); + }); } /// Stops the [Isolate] being supervised. Future stop() async { _stopCompleter = new Completer(); - _serverSendPort.send(_MessageStop); + _serverSendPort.send(MessageStop); await _stopCompleter.future.timeout(new Duration(seconds: 30)); + receivePort.close(); isolate.kill(); } @@ -55,18 +66,24 @@ class ApplicationIsolateSupervisor { _launchCompleter = null; _serverSendPort = message; - } else if (message == _MessageStop) { + } else if (message == MessageStop) { _stopCompleter?.complete(); _stopCompleter = null; } else if (message is List) { - var exception = new ApplicationSupervisorException(message.first); var stacktrace = new StackTrace.fromString(message.last); + _handleIsolateException(message.first, stacktrace); + } + } + + void _handleIsolateException(dynamic error, StackTrace stacktrace) { + if (_isLaunching) { + receivePort.close(); - if (_launchCompleter != null) { - _launchCompleter.completeError(exception, stacktrace); - } else { - logger.severe("Uncaught exception in isolate.", exception, stacktrace); - } + var appException = new ApplicationStartupException(error); + _launchCompleter.completeError(appException, stacktrace); + } else { + var exception = new ApplicationSupervisorException(error); + logger.severe("Uncaught exception in isolate.", exception, stacktrace); } } @@ -83,14 +100,3 @@ class ApplicationIsolateSupervisor { }); } } - -/// An exception originating from an [Isolate] within an [Application]. -class ApplicationSupervisorException implements Exception { - ApplicationSupervisorException(this.message); - - final String message; - - String toString() { - return "$message"; - } -} diff --git a/lib/src/auth/auth.dart b/lib/src/auth/auth.dart new file mode 100644 index 000000000..d7b082cd0 --- /dev/null +++ b/lib/src/auth/auth.dart @@ -0,0 +1,7 @@ +export 'auth_code_controller.dart'; +export 'auth_controller.dart'; +export 'authentication_server.dart'; +export 'authorization_parser.dart'; +export 'authorizer.dart'; +export 'client.dart'; +export 'protocols.dart'; diff --git a/lib/auth/auth_code_controller.dart b/lib/src/auth/auth_code_controller.dart similarity index 97% rename from lib/auth/auth_code_controller.dart rename to lib/src/auth/auth_code_controller.dart index 4713ad9db..32047b29c 100644 --- a/lib/auth/auth_code_controller.dart +++ b/lib/src/auth/auth_code_controller.dart @@ -1,4 +1,8 @@ -part of aqueduct; +import 'dart:async'; +import 'dart:io'; + +import '../http/http.dart'; +import 'auth.dart'; /// [RequestController] for issuing OAuth 2.0 authorization codes. class AuthCodeController extends HTTPController { diff --git a/lib/auth/auth_controller.dart b/lib/src/auth/auth_controller.dart similarity index 98% rename from lib/auth/auth_controller.dart rename to lib/src/auth/auth_controller.dart index 5c57e1d1c..abb54721a 100644 --- a/lib/auth/auth_controller.dart +++ b/lib/src/auth/auth_controller.dart @@ -1,4 +1,8 @@ -part of aqueduct; +import 'dart:async'; +import 'dart:io'; + +import '../http/http.dart'; +import 'auth.dart'; /// [RequestController] for issuing OAuth 2.0 authorization tokens. class AuthController extends HTTPController { diff --git a/lib/auth/authentication_server.dart b/lib/src/auth/authentication_server.dart similarity index 97% rename from lib/auth/authentication_server.dart rename to lib/src/auth/authentication_server.dart index 4fc992395..db95f6743 100644 --- a/lib/auth/authentication_server.dart +++ b/lib/src/auth/authentication_server.dart @@ -1,4 +1,15 @@ -part of aqueduct; +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:math'; +import 'dart:mirrors'; + +import 'package:crypto/crypto.dart'; + +import '../http/http.dart'; +import '../utilities/pbkdf2.dart'; +import '../utilities/token_generator.dart'; +import 'auth.dart'; /// A storage-agnostic authorization 'server'. /// @@ -337,7 +348,7 @@ class AuthServer< /// A utility method to generate a random base64 salt. static String generateRandomSalt({int hashLength: 32}) { - var random = new Random(new DateTime.now().millisecondsSinceEpoch); + var random = new Random.secure(); List salt = []; for (var i = 0; i < hashLength; i++) { salt.add(random.nextInt(256)); diff --git a/lib/auth/authorization_parser.dart b/lib/src/auth/authorization_parser.dart similarity index 97% rename from lib/auth/authorization_parser.dart rename to lib/src/auth/authorization_parser.dart index f36e38217..44f492b59 100644 --- a/lib/auth/authorization_parser.dart +++ b/lib/src/auth/authorization_parser.dart @@ -1,4 +1,6 @@ -part of aqueduct; +import 'dart:convert'; + +import '../http/http.dart'; /// Parses a Bearer token from an Authorization header. class AuthorizationBearerParser { diff --git a/lib/auth/authorizer.dart b/lib/src/auth/authorizer.dart similarity index 97% rename from lib/auth/authorizer.dart rename to lib/src/auth/authorizer.dart index 0e0ebf9ee..7c894a41b 100644 --- a/lib/auth/authorizer.dart +++ b/lib/src/auth/authorizer.dart @@ -1,4 +1,8 @@ -part of aqueduct; +import 'dart:async'; +import 'dart:io'; + +import '../http/http.dart'; +import 'auth.dart'; /// The type of authentication strategy to use for an [Authorizer]. enum AuthStrategy { diff --git a/lib/auth/client.dart b/lib/src/auth/client.dart similarity index 96% rename from lib/auth/client.dart rename to lib/src/auth/client.dart index 2d7622d07..10e4a7e51 100644 --- a/lib/auth/client.dart +++ b/lib/src/auth/client.dart @@ -1,5 +1,3 @@ -part of aqueduct; - /// Represents a Client ID and secret pair. class AuthClient { /// Creates an instance of [AuthClient]. diff --git a/lib/auth/protocols.dart b/lib/src/auth/protocols.dart similarity index 98% rename from lib/auth/protocols.dart rename to lib/src/auth/protocols.dart index a30234dfc..1d48d0fe8 100644 --- a/lib/auth/protocols.dart +++ b/lib/src/auth/protocols.dart @@ -1,4 +1,7 @@ -part of aqueduct; +import 'dart:async'; +import 'package:meta/meta.dart'; +import 'auth.dart'; +import '../db/managed/managed.dart'; /// An interface to represent [AuthServer.TokenType]. /// diff --git a/lib/commands/cli_command.dart b/lib/src/commands/cli_command.dart similarity index 85% rename from lib/commands/cli_command.dart rename to lib/src/commands/cli_command.dart index 9c90d5b4c..a6bd8e3fc 100644 --- a/lib/commands/cli_command.dart +++ b/lib/src/commands/cli_command.dart @@ -1,4 +1,10 @@ -part of aqueduct; +import 'dart:async'; + +import 'package:args/args.dart'; + +export 'migration_runner.dart'; +export 'setup_command.dart'; +export 'template_creator.dart'; /// A command line interface command. abstract class CLICommand { @@ -29,6 +35,7 @@ abstract class CLICommand { } finally { await cleanup(); } - return -1; + + return 1; } } diff --git a/lib/commands/migration_runner.dart b/lib/src/commands/migration_runner.dart similarity index 95% rename from lib/commands/migration_runner.dart rename to lib/src/commands/migration_runner.dart index 98b6e5560..5d8743e5b 100644 --- a/lib/commands/migration_runner.dart +++ b/lib/src/commands/migration_runner.dart @@ -1,4 +1,12 @@ -part of aqueduct; +import 'dart:async'; +import 'dart:io'; + +import 'package:yaml/yaml.dart'; +import 'package:safe_config/safe_config.dart'; +import 'package:args/args.dart'; + +import 'cli_command.dart'; +import '../db/db.dart'; /// Used internally. class CLIMigrationRunner extends CLICommand { @@ -133,7 +141,7 @@ class CLIMigrationRunner extends CLICommand { Future listVersions() async { var files = executor.migrationFiles.map((f) { var versionString = - "${executor._versionNumberFromFile(f)}".padLeft(8, "0"); + "${executor.versionNumberFromFile(f)}".padLeft(8, "0"); return " $versionString | ${f.path}"; }).join("\n"); @@ -169,7 +177,7 @@ class CLIMigrationRunner extends CLICommand { } Map versionMap = executor.migrationFiles.fold({}, (map, file) { - var versionNumber = executor._versionNumberFromFile(file); + var versionNumber = executor.versionNumberFromFile(file); map[versionNumber] = file; return map; }); diff --git a/lib/commands/setup_command.dart b/lib/src/commands/setup_command.dart similarity index 97% rename from lib/commands/setup_command.dart rename to lib/src/commands/setup_command.dart index 1889a6b2d..3356dfa81 100644 --- a/lib/commands/setup_command.dart +++ b/lib/src/commands/setup_command.dart @@ -1,4 +1,9 @@ -part of aqueduct; +import 'dart:async'; +import 'dart:io'; + +import 'package:args/args.dart'; + +import 'cli_command.dart'; class CLISetup extends CLICommand { ArgParser options = new ArgParser(allowTrailingOptions: false) diff --git a/lib/commands/template_creator.dart b/lib/src/commands/template_creator.dart similarity index 92% rename from lib/commands/template_creator.dart rename to lib/src/commands/template_creator.dart index a5d82c70a..6a06abfca 100644 --- a/lib/commands/template_creator.dart +++ b/lib/src/commands/template_creator.dart @@ -1,4 +1,11 @@ -part of aqueduct; +import 'dart:async'; +import 'dart:io'; + +import 'package:args/args.dart'; +import 'package:path/path.dart' as path_lib; + +import '../http/documentable.dart'; +import 'cli_command.dart'; /// Used internally. class CLITemplateCreator extends CLICommand { @@ -29,24 +36,24 @@ class CLITemplateCreator extends CLICommand { Future handle(ArgResults argValues) async { if (argValues["help"] == true) { print("${options.usage}"); - return 0; + return 1; } if (argValues["name"] == null) { print("No project name specified.\n\n${options.usage}"); - return -1; + return 1; } if (argValues["name"] == null || !isSnakeCase(argValues["name"])) { print( "Invalid project name ${argValues["name"]} is not snake_case).\n\n${options.usage}"); - return -1; + return 1; } var destDirectory = destinationDirectoryFromPath(argValues["name"]); if (destDirectory.existsSync()) { print("${destDirectory.path} already exists, stopping."); - return -1; + return 1; } destDirectory.createSync(); @@ -68,7 +75,7 @@ class CLITemplateCreator extends CLICommand { } if (!sourceDirectory.existsSync()) { print("Error: no template named ${argValues["template"]}"); - return -1; + return 1; } print(""); @@ -78,6 +85,8 @@ class CLITemplateCreator extends CLICommand { print("Generating project files..."); await createProjectSpecificFiles(destDirectory.path, aqueductVersion); + await replaceAqueductDependencyString(destDirectory.path, aqueductVersion); + print("Fetching project dependencies..."); Process.runSync("pub", ["get", "--no-packages-dir"], workingDirectory: destDirectory.path, runInShell: true); @@ -225,6 +234,17 @@ class CLITemplateCreator extends CLICommand { .copySync(new File(path_lib.join(directoryPath, "config.yaml")).path); } + Future replaceAqueductDependencyString( + String destDirectoryPath, String aqueductVersion) async { + var pubspecFile = + new File(path_lib.join(destDirectoryPath, "pubspec.yaml")); + var contents = pubspecFile.readAsStringSync(); + + contents = contents.replaceFirst("aqueduct: \"^1.0.0\"", aqueductVersion); + + pubspecFile.writeAsStringSync(contents); + } + void copyProjectFiles(Directory destinationDirectory, Directory sourceDirectory, String projectName) { try { diff --git a/lib/src/db/db.dart b/lib/src/db/db.dart new file mode 100644 index 000000000..0a00c4f58 --- /dev/null +++ b/lib/src/db/db.dart @@ -0,0 +1,5 @@ +export 'managed/managed.dart'; +export 'persistent_store/persistent_store.dart'; +export 'postgresql/postgresql_persistent_store.dart'; +export 'query/query.dart'; +export 'schema/schema.dart'; diff --git a/lib/db/managed/attributes.dart b/lib/src/db/managed/attributes.dart similarity index 99% rename from lib/db/managed/attributes.dart rename to lib/src/db/managed/attributes.dart index 7526c4378..36713156a 100644 --- a/lib/db/managed/attributes.dart +++ b/lib/src/db/managed/attributes.dart @@ -1,4 +1,4 @@ -part of aqueduct; +import 'managed.dart'; /// Possible values for a delete rule in a [ManagedRelationship]. enum ManagedRelationshipDeleteRule { diff --git a/lib/db/managed/backing.dart b/lib/src/db/managed/backing.dart similarity index 83% rename from lib/db/managed/backing.dart rename to lib/src/db/managed/backing.dart index 192e22a98..d6a1c8bcf 100644 --- a/lib/db/managed/backing.dart +++ b/lib/src/db/managed/backing.dart @@ -1,17 +1,9 @@ -part of aqueduct; +import 'dart:mirrors'; +import '../query/query.dart'; +import 'managed.dart'; +import '../query/matcher_internal.dart'; -abstract class _ManagedBacking { - dynamic valueForProperty(ManagedEntity entity, String propertyName); - void setValueForProperty( - ManagedEntity entity, String propertyName, dynamic value); - void removeProperty(String propertyName) { - valueMap.remove(propertyName); - } - - Map get valueMap; -} - -class _ManagedValueBacking extends _ManagedBacking { +class ManagedValueBacking extends ManagedBacking { Map valueMap = {}; dynamic valueForProperty(ManagedEntity entity, String propertyName) { @@ -44,7 +36,7 @@ class _ManagedValueBacking extends _ManagedBacking { } } -class _ManagedMatcherBacking extends _ManagedBacking { +class ManagedMatcherBacking extends ManagedBacking { Map valueMap = {}; dynamic valueForProperty(ManagedEntity entity, String propertyName) { @@ -55,7 +47,7 @@ class _ManagedMatcherBacking extends _ManagedBacking { ..entity = relDesc.destinationEntity; } else if (relDesc?.relationshipType == ManagedRelationshipType.hasOne) { valueMap[propertyName] = relDesc.destinationEntity.newInstance() - .._backing = new _ManagedMatcherBacking(); + ..backing = new ManagedMatcherBacking(); } else if (relDesc?.relationshipType == ManagedRelationshipType.belongsTo) { throw new QueryException(QueryExceptionEvent.requestFailure, @@ -74,7 +66,7 @@ class _ManagedMatcherBacking extends _ManagedBacking { return; } - if (value is _MatcherExpression) { + if (value is MatcherExpression) { var relDesc = entity.relationships[propertyName]; if (relDesc != null && @@ -94,7 +86,7 @@ class _ManagedMatcherBacking extends _ManagedBacking { } valueMap[propertyName] = - new _ComparisonMatcherExpression(value, MatcherOperator.equalTo); + new ComparisonMatcherExpression(value, MatcherOperator.equalTo); } } } diff --git a/lib/db/managed/context.dart b/lib/src/db/managed/context.dart similarity index 82% rename from lib/db/managed/context.dart rename to lib/src/db/managed/context.dart index f716ffb92..665812f5d 100644 --- a/lib/db/managed/context.dart +++ b/lib/src/db/managed/context.dart @@ -1,4 +1,9 @@ -part of aqueduct; +import 'dart:async'; + +import '../persistent_store/persistent_store_query.dart'; +import 'managed.dart'; +import '../persistent_store/persistent_store.dart'; +import '../query/query.dart'; /// Coordinates with a [ManagedDataModel] and [PersistentStore] to execute queries and /// translate between [ManagedObject] objects and database rows. @@ -14,7 +19,8 @@ class ManagedContext { /// For classes that require a [ManagedContext] - like [Query] - this is the default context when none /// is specified. /// - /// This value is set when the first [ManagedContext] instantiated in an isolate. Most applications + /// This value is set when a [ManagedContext] is instantiated in an isolate; the last context created + /// is the default context. Most applications /// will not use more than one [ManagedContext]. When running tests, you should set /// this value each time you instantiate a [ManagedContext] to ensure that a previous test isolate /// state did not set this property. @@ -22,13 +28,18 @@ class ManagedContext { /// Creates an instance of [ManagedContext] from a [ManagedDataModel] and [PersistentStore]. /// - /// If this is the first [ManagedContext] instantiated on an isolate, this instance will because the [ManagedContext.defaultContext]. + /// This instance will become the [ManagedContext.defaultContext], unless another [ManagedContext] + /// is created, in which the new context becomes the default context. ManagedContext(this.dataModel, this.persistentStore) { - if (defaultContext == null) { - defaultContext = this; - } + defaultContext = this; } + /// Creates an instance of [ManagedContext] from a [ManagedDataModel] and [PersistentStore]. + /// + /// This constructor creates an instance in the same way the default constructor does, + /// but does not set it to be the [defaultContext]. + ManagedContext.standalone(this.dataModel, this.persistentStore); + /// The persistent store that [Query]s on this context are executed on. PersistentStore persistentStore; @@ -42,22 +53,22 @@ class ManagedContext { return dataModel.entityForType(type); } - Future _executeInsertQuery(Query query) async { - var psq = new PersistentStoreQuery(query.entity, persistentStore, query); + Future executeInsertQuery(Query query) async { + var psq = query.persistentQueryForStore(persistentStore); var results = await persistentStore.executeInsertQuery(psq); return query.entity.instanceFromMappingElements(results); } - Future> _executeFetchQuery(Query query) async { - var psq = new PersistentStoreQuery(query.entity, persistentStore, query); + Future> executeFetchQuery(Query query) async { + var psq = query.persistentQueryForStore(persistentStore); var results = await persistentStore.executeFetchQuery(psq); return _coalesceAndMapRows(results, query.entity); } - Future> _executeUpdateQuery(Query query) async { - var psq = new PersistentStoreQuery(query.entity, persistentStore, query); + Future> executeUpdateQuery(Query query) async { + var psq = query.persistentQueryForStore(persistentStore); var results = await persistentStore.executeUpdateQuery(psq); return results.map((row) { @@ -65,9 +76,9 @@ class ManagedContext { }).toList(); } - Future _executeDeleteQuery(Query query) async { - return await persistentStore.executeDeleteQuery( - new PersistentStoreQuery(query.entity, persistentStore, query)); + Future executeDeleteQuery(Query query) async { + return await persistentStore + .executeDeleteQuery(query.persistentQueryForStore(persistentStore)); } List _coalesceAndMapRows( diff --git a/lib/db/managed/data_model.dart b/lib/src/db/managed/data_model.dart similarity index 54% rename from lib/db/managed/data_model.dart rename to lib/src/db/managed/data_model.dart index 10bc183f0..262febc1a 100644 --- a/lib/db/managed/data_model.dart +++ b/lib/src/db/managed/data_model.dart @@ -1,4 +1,6 @@ -part of aqueduct; +import 'dart:mirrors'; +import 'managed.dart'; +import 'data_model_builder.dart'; /// Instances of this class contain descriptions and metadata for mapping [ManagedObject]s to database rows. /// @@ -9,43 +11,63 @@ part of aqueduct; /// types that extend [ManagedObject]. class ManagedDataModel { /// Creates an instance of [ManagedDataModel] from a list of types that extend [ManagedObject]. It is preferable - /// to use [ManagedDataModel.fromPackageContainingType] over this method. + /// to use [ManagedDataModel.fromCurrentMirrorSystem] over this method. /// /// To register a class as a managed object within this data model, you must include its type in the list. Example: /// /// new DataModel([User, Token, Post]); ManagedDataModel(List instanceTypes) { - var builder = new _DataModelBuilder(this, instanceTypes); + var builder = new DataModelBuilder(this, instanceTypes); _entities = builder.entities; _persistentTypeToEntityMap = builder.persistentTypeToEntityMap; } - /// Creates an instance on [ManagedDataModel] from all of the declared [ManagedObject] subclasses declared in the same package as [type]. + /// Creates an instance of a [ManagedDataModel] from all subclasses of [ManagedObject] in all libraries visible to the calling library. + /// + /// This constructor will search every available package and file library that is visible to the library + /// that runs this constructor for subclasses of [ManagedObject]. A [ManagedEntity] will be created + /// and stored in this instance for every such class found. /// - /// This is a convenience constructor for creating a [ManagedDataModel] from an application package. It will find all subclasses of [ManagedObject] - /// in the package that [type] belongs to. Typically, you pass the [Type] of an application's [RequestSink] subclass. - ManagedDataModel.fromPackageContainingType(Type type) { - LibraryMirror libMirror = reflectType(type).owner; + /// Standard Dart libraries (prefixed with 'dart:') and URL-encoded libraries (prefixed with 'data:') are not searched. + /// + /// This is the preferred method of instantiating this type. + ManagedDataModel.fromCurrentMirrorSystem() { + var managedObjectMirror = reflectClass(ManagedObject); + var classes = currentMirrorSystem() + .libraries + .values + .where((lib) => lib.uri.scheme == "package" || lib.uri.scheme == "file") + .expand((lib) => lib.declarations.values) + .where((decl) => + decl is ClassMirror && + decl.isSubclassOf(managedObjectMirror) && + decl != managedObjectMirror) + .map((decl) => decl as ClassMirror) + .toList(); - var builder = - new _DataModelBuilder(this, _modelTypesFromLibraryMirror(libMirror)); + var builder = new DataModelBuilder( + this, classes.map((cm) => cm.reflectedType).toList()); _entities = builder.entities; _persistentTypeToEntityMap = builder.persistentTypeToEntityMap; } + /// Creates an instance on [ManagedDataModel] from all of the declared [ManagedObject] subclasses declared in the same package as [type]. + /// + /// This method now simply calls [ManagedDataModel.fromCurrentMirrorSystem]. + @deprecated + factory ManagedDataModel.fromPackageContainingType(Type type) { + return new ManagedDataModel.fromCurrentMirrorSystem(); + } + /// Creates an instance of a [ManagedDataModel] from a package on the filesystem. /// - /// This method is used by database migration tools. - ManagedDataModel.fromURI(Uri libraryURI) { - if (!libraryURI.isAbsolute) { - libraryURI = new Uri.file(libraryURI.path); - } - var libMirror = currentMirrorSystem().libraries[libraryURI]; - var builder = - new _DataModelBuilder(this, _modelTypesFromLibraryMirror(libMirror)); - _entities = builder.entities; + /// This method now simply calls [ManagedDataModel.fromCurrentMirrorSystem]. + @deprecated + factory ManagedDataModel.fromURI(Uri libraryURI) { + return new ManagedDataModel.fromCurrentMirrorSystem(); } + Iterable get entities => _entities.values; Map _entities = {}; Map _persistentTypeToEntityMap = {}; @@ -62,18 +84,6 @@ class ManagedDataModel { ManagedEntity entityForType(Type type) { return _entities[type] ?? _persistentTypeToEntityMap[type]; } - - List _modelTypesFromLibraryMirror(LibraryMirror libMirror) { - var modelMirror = reflectClass(ManagedObject); - Iterable allClasses = libMirror.declarations.values - .where((decl) => decl is ClassMirror) - .map((decl) => decl as ClassMirror); - - return allClasses - .where((m) => m.isSubclassOf(modelMirror)) - .map((m) => m.reflectedType) - .toList(); - } } /// Thrown when a [ManagedDataModel] encounters an error. diff --git a/lib/db/managed/data_model_builder.dart b/lib/src/db/managed/data_model_builder.dart similarity index 92% rename from lib/db/managed/data_model_builder.dart rename to lib/src/db/managed/data_model_builder.dart index aeff36f59..01daae49a 100644 --- a/lib/db/managed/data_model_builder.dart +++ b/lib/src/db/managed/data_model_builder.dart @@ -1,25 +1,20 @@ -part of aqueduct; +import 'dart:mirrors'; +import 'managed.dart'; +import '../../utilities/mirror_helpers.dart'; -class _DataModelBuilder { - _DataModelBuilder(ManagedDataModel dataModel, List instanceTypes) { +class DataModelBuilder { + DataModelBuilder(ManagedDataModel dataModel, List instanceTypes) { instanceTypes.forEach((type) { + var backingMirror = backingMirrorForType(type); var entity = new ManagedEntity( - dataModel, reflectClass(type), backingMirrorForType(type)); + dataModel, + tableNameForPersistentTypeMirror(backingMirror), + reflectClass(type), + backingMirror); entities[type] = entity; persistentTypeToEntityMap[entity.persistentType.reflectedType] = entity; - }); - entities.forEach((_, entity) { - entity._tableName = tableNameForEntity(entity); entity.attributes = attributeMapForEntity(entity); - entity._primaryKey = entity.attributes.values - .firstWhere((attrDesc) => attrDesc.isPrimaryKey, orElse: () => null) - ?.name; - - if (entity.primaryKey == null) { - throw new ManagedDataModelException( - "No primary key for entity ${MirrorSystem.getName(entity.persistentType.simpleName)}"); - } }); entities.forEach((_, entity) { @@ -30,13 +25,13 @@ class _DataModelBuilder { Map entities = {}; Map persistentTypeToEntityMap = {}; - String tableNameForEntity(ManagedEntity entity) { + String tableNameForPersistentTypeMirror(ClassMirror typeMirror) { var tableNameSymbol = #tableName; - if (entity.persistentType.staticMembers[tableNameSymbol] != null) { - return entity.persistentType.invoke(tableNameSymbol, []).reflectee; + if (typeMirror.staticMembers[tableNameSymbol] != null) { + return typeMirror.invoke(tableNameSymbol, []).reflectee; } - return MirrorSystem.getName(entity.persistentType.simpleName); + return MirrorSystem.getName(typeMirror.simpleName); } Map attributeMapForEntity( diff --git a/lib/db/managed/entity.dart b/lib/src/db/managed/entity.dart similarity index 93% rename from lib/db/managed/entity.dart rename to lib/src/db/managed/entity.dart index 8de3c564e..d5fb43a7b 100644 --- a/lib/db/managed/entity.dart +++ b/lib/src/db/managed/entity.dart @@ -1,4 +1,8 @@ -part of aqueduct; +import 'dart:mirrors'; +import 'managed.dart'; +import '../../http/documentable.dart'; +import '../query/query.dart'; +import '../persistent_store/persistent_store_query.dart'; /// Mapping information between a table in a database and a [ManagedObject] object. /// @@ -22,7 +26,8 @@ class ManagedEntity { /// Creates an instance of this type.. /// /// You should never call this method directly, it will be called by [ManagedDataModel]. - ManagedEntity(this.dataModel, this.instanceType, this.persistentType); + ManagedEntity( + this.dataModel, this._tableName, this.instanceType, this.persistentType); /// The type of instances represented by this entity. /// @@ -61,7 +66,20 @@ class ManagedEntity { /// transient property declared in the instance type. /// The keys are the case-sensitive name of the attribute. Values that represent a relationship to another object /// are not stored in [attributes]. - Map attributes; + + Map _attributes; + Map get attributes => _attributes; + void set attributes(Map m) { + _attributes = m; + _primaryKey = m.values + .firstWhere((attrDesc) => attrDesc.isPrimaryKey, orElse: () => null) + ?.name; + + if (_primaryKey == null) { + throw new ManagedDataModelException( + "No primary key for entity ${MirrorSystem.getName(persistentType.simpleName)}"); + } + } /// All relationship values of this entity. /// diff --git a/lib/src/db/managed/managed.dart b/lib/src/db/managed/managed.dart new file mode 100644 index 000000000..b483bbe80 --- /dev/null +++ b/lib/src/db/managed/managed.dart @@ -0,0 +1,7 @@ +export 'attributes.dart'; +export 'context.dart'; +export 'data_model.dart'; +export 'entity.dart'; +export 'object.dart'; +export 'property_description.dart'; +export 'set.dart'; diff --git a/lib/db/managed/object.dart b/lib/src/db/managed/object.dart similarity index 82% rename from lib/db/managed/object.dart rename to lib/src/db/managed/object.dart index a4df55c20..b82e6d65b 100644 --- a/lib/db/managed/object.dart +++ b/lib/src/db/managed/object.dart @@ -1,4 +1,41 @@ -part of aqueduct; +import 'dart:mirrors'; + +import '../../http/serializable.dart'; +import 'managed.dart'; +import 'query_matchable.dart'; +import 'backing.dart'; + +/// Instances of this class provide storage for [ManagedObject]s. +/// +/// A [ManagedObject] stores properties declared by its type argument in instances of this type. +/// Values are validated against the [ManagedObject.entity]. +/// +/// Instances of this type only store properties for which a value has been explicitly set. This allows +/// serialization classes to omit unset values from the serialized values. Therefore, instances of this class +/// provide behavior that can differentiate between a property being the null value and a property simply not being +/// set. (Therefore, you must use [removeProperty] instead of setting a value to null to really remove it from instances +/// of this type.) +/// +/// Aqueduct implements concrete subclasses of this class to provide behavior for property storage +/// and query-building. +abstract class ManagedBacking { + /// Retrieve a property by its entity and name. + dynamic valueForProperty(ManagedEntity entity, String propertyName); + + /// Sets a property by its entity and name. + void setValueForProperty( + ManagedEntity entity, String propertyName, dynamic value); + + /// Removes a property from this instance. + /// + /// Use this method to use any reference of a property from this instance. + void removeProperty(String propertyName) { + valueMap.remove(propertyName); + } + + /// A map of all set values of this instance. + Map get valueMap; +} /// An object whose storage is managed by an underlying [Map]. /// @@ -25,7 +62,7 @@ part of aqueduct; /// @primaryKey int id; // Persisted /// } class ManagedObject extends Object - with _QueryMatchableExtension + with QueryMatchableExtension implements HTTPSerializable, QueryMatchable { /// Used when building a [Query] to include instances of this type. /// @@ -49,25 +86,24 @@ class ManagedObject extends Object /// Not all values are fetched or populated in a [ManagedObject] instance. This value contains /// key-value pairs for the managed object that have been set, either manually /// or when fetched from a database. When [ManagedObject] is instantiated, this map is empty. - Map get backingMap => _backing.valueMap; + Map get backingMap => backing.valueMap; - _ManagedBacking _backing = new _ManagedValueBacking(); - Map get _matcherMap => backingMap; + ManagedBacking backing = new ManagedValueBacking(); /// Retrieves a value by property name from the [backingMap]. dynamic operator [](String propertyName) => - _backing.valueForProperty(entity, propertyName); + backing.valueForProperty(entity, propertyName); /// Sets a value by property name in the [backingMap]. void operator []=(String propertyName, dynamic value) { - _backing.setValueForProperty(entity, propertyName, value); + backing.setValueForProperty(entity, propertyName, value); } /// Removes a property from the [backingMap]. /// /// This will remove a value from the backing map. void removePropertyFromBackingMap(String propertyName) { - _backing.removeProperty(propertyName); + backing.removeProperty(propertyName); } /// Checks whether or not a property has been set in this instances' [backingMap]. @@ -114,7 +150,7 @@ class ManagedObject extends Object if (property is ManagedAttributeDescription) { if (!property.isTransient) { - _backing.setValueForProperty(entity, k, _valueDecoder(property, v)); + backing.setValueForProperty(entity, k, _valueDecoder(property, v)); } else { if (!property.transientStatus.isAvailableAsInput) { throw new QueryException(QueryExceptionEvent.requestFailure, @@ -134,7 +170,7 @@ class ManagedObject extends Object mirror.setField(new Symbol(k), decodedValue); } } else { - _backing.setValueForProperty(entity, k, _valueDecoder(property, v)); + backing.setValueForProperty(entity, k, _valueDecoder(property, v)); } }); } @@ -151,7 +187,7 @@ class ManagedObject extends Object Map asMap() { var outputMap = {}; - _backing.valueMap.forEach((k, v) { + backing.valueMap.forEach((k, v) { outputMap[k] = _valueEncoder(k, v); }); diff --git a/lib/db/managed/property_description.dart b/lib/src/db/managed/property_description.dart similarity index 99% rename from lib/db/managed/property_description.dart rename to lib/src/db/managed/property_description.dart index ccf3f90ac..40eae4117 100644 --- a/lib/db/managed/property_description.dart +++ b/lib/src/db/managed/property_description.dart @@ -1,4 +1,5 @@ -part of aqueduct; +import 'dart:mirrors'; +import 'managed.dart'; /// Possible data types for [ManagedEntity] attributes. enum ManagedPropertyType { diff --git a/lib/src/db/managed/query_matchable.dart b/lib/src/db/managed/query_matchable.dart new file mode 100644 index 000000000..1948fc658 --- /dev/null +++ b/lib/src/db/managed/query_matchable.dart @@ -0,0 +1,24 @@ +import 'attributes.dart'; +import '../query/query.dart'; + +export '../query/query.dart'; + +abstract class QueryMatchableExtension implements QueryMatchable { + bool get hasJoinElements { + return backingMap.values + .where((item) => item is QueryMatchable) + .any((QueryMatchable item) => item.includeInResultSet); + } + + List get joinPropertyKeys { + return backingMap.keys.where((propertyName) { + var val = backingMap[propertyName]; + var relDesc = entity.relationships[propertyName]; + + return val is QueryMatchable && + val.includeInResultSet && + (relDesc?.relationshipType == ManagedRelationshipType.hasMany || + relDesc?.relationshipType == ManagedRelationshipType.hasOne); + }).toList(); + } +} diff --git a/lib/db/managed/set.dart b/lib/src/db/managed/set.dart similarity index 91% rename from lib/db/managed/set.dart rename to lib/src/db/managed/set.dart index 279908d0d..657ff9054 100644 --- a/lib/db/managed/set.dart +++ b/lib/src/db/managed/set.dart @@ -1,4 +1,9 @@ -part of aqueduct; +import 'dart:collection'; + +import 'backing.dart'; +import '../../http/serializable.dart'; +import 'managed.dart'; +import 'query_matchable.dart'; /// Instances of this type contain zero or more instances of [ManagedObject]. /// @@ -18,7 +23,7 @@ part of aqueduct; /// User user; /// } class ManagedSet extends Object - with ListMixin, _QueryMatchableExtension + with ListMixin, QueryMatchableExtension implements QueryMatchable, HTTPSerializable { /// Creates an empty [ManagedSet]. ManagedSet() { @@ -58,7 +63,7 @@ class ManagedSet extends Object InstanceType get matchOn { if (_matchOn == null) { _matchOn = entity.newInstance() as InstanceType; - _matchOn._backing = new _ManagedMatcherBacking(); + _matchOn.backing = new ManagedMatcherBacking(); } return _matchOn; } @@ -69,7 +74,7 @@ class ManagedSet extends Object _innerValues.length = newLength; } - Map get _matcherMap => matchOn.backingMap; + Map get backingMap => matchOn.backingMap; List _innerValues; InstanceType _matchOn; diff --git a/lib/db/persistent_store/persistent_store.dart b/lib/src/db/persistent_store/persistent_store.dart similarity index 94% rename from lib/db/persistent_store/persistent_store.dart rename to lib/src/db/persistent_store/persistent_store.dart index e33e53ef8..6b95d0b00 100644 --- a/lib/db/persistent_store/persistent_store.dart +++ b/lib/src/db/persistent_store/persistent_store.dart @@ -1,4 +1,9 @@ -part of aqueduct; +import 'dart:async'; +import '../query/query.dart'; +import '../schema/schema.dart'; +import 'persistent_store_query.dart'; +import '../managed/managed.dart'; +export 'persistent_store_query.dart'; /// An interface for implementing persistent storage. /// diff --git a/lib/src/db/persistent_store/persistent_store_query.dart b/lib/src/db/persistent_store/persistent_store_query.dart new file mode 100644 index 000000000..8247419d6 --- /dev/null +++ b/lib/src/db/persistent_store/persistent_store_query.dart @@ -0,0 +1,71 @@ +import 'persistent_store.dart'; +import '../query/query.dart'; +import '../managed/managed.dart'; +import '../managed/query_matchable.dart'; + +/// This enumeration is used internaly. +enum PersistentJoinType { leftOuter } + +/// This class is used internally to map [Query] to something a [PersistentStore] can execute. +class PersistentStoreQuery { + int offset = 0; + int fetchLimit = 0; + int timeoutInSeconds = 30; + bool confirmQueryModifiesAllInstancesOnDeleteOrUpdate; + ManagedEntity entity; + QueryPage pageDescriptor; + QueryPredicate predicate; + List sortDescriptors; + List values; + List resultKeys; +} + +/// This class is used internally. +class PersistentColumnMapping { + PersistentColumnMapping(this.property, this.value); + PersistentColumnMapping.fromElement( + PersistentColumnMapping original, this.value) { + property = original.property; + } + + ManagedPropertyDescription property; + dynamic value; + + String toString() { + return "MappingElement on $property (Value = $value)"; + } +} + +/// This class is used internally. +class PersistentJoinMapping extends PersistentColumnMapping { + PersistentJoinMapping(this.type, ManagedPropertyDescription property, + this.predicate, this.resultKeys) + : super(property, null) { + var primaryKeyElement = this.resultKeys.firstWhere((e) { + var eProp = e.property; + if (eProp is ManagedAttributeDescription) { + return eProp.isPrimaryKey; + } + return false; + }); + + primaryKeyIndex = this.resultKeys.indexOf(primaryKeyElement); + } + + PersistentJoinMapping.fromElement( + PersistentJoinMapping original, List values) + : super.fromElement(original, values) { + type = original.type; + primaryKeyIndex = original.primaryKeyIndex; + } + + PersistentJoinType type; + ManagedPropertyDescription get joinProperty => + (property as ManagedRelationshipDescription).inverseRelationship; + QueryPredicate predicate; + List resultKeys; + + int primaryKeyIndex; + List get values => + value as List; +} diff --git a/lib/db/postgresql/postgresql_persistent_store.dart b/lib/src/db/postgresql/postgresql_persistent_store.dart similarity index 94% rename from lib/db/postgresql/postgresql_persistent_store.dart rename to lib/src/db/postgresql/postgresql_persistent_store.dart index 73a94657f..fad6fb35a 100644 --- a/lib/db/postgresql/postgresql_persistent_store.dart +++ b/lib/src/db/postgresql/postgresql_persistent_store.dart @@ -1,4 +1,13 @@ -part of aqueduct; +import 'package:logging/logging.dart'; +import 'package:postgres/postgres.dart'; +import 'dart:async'; +import '../managed/managed.dart'; +import '../query/query.dart'; +import '../persistent_store/persistent_store.dart'; +import '../schema/schema.dart'; +import '../persistent_store/persistent_store_query.dart'; + +import 'postgresql_schema_generator.dart'; /// A function that will create an opened instance of [PostgreSQLConnection] when executed. typedef Future PostgreSQLConnectionFunction(); @@ -8,7 +17,7 @@ typedef Future PostgreSQLConnectionFunction(); /// To interact with a PostgreSQL database, a [ManagedContext] must have an instance of this class. /// Instances of this class are configured to connect to a particular PostgreSQL database. class PostgreSQLPersistentStore extends PersistentStore - with _PostgreSQLSchemaGenerator { + with PostgreSQLSchemaGenerator { /// The logger used by instances of this class. static Logger logger = new Logger("aqueduct"); @@ -138,7 +147,7 @@ class PostgreSQLPersistentStore extends PersistentStore Future get schemaVersion async { try { var values = await execute( - "SELECT versionNumber, dateOfUpgrade FROM $_versionTableName ORDER BY dateOfUpgrade ASC") + "SELECT versionNumber, dateOfUpgrade FROM $versionTableName ORDER BY dateOfUpgrade ASC") as List>; if (values.length == 0) { return 0; @@ -164,7 +173,7 @@ class PostgreSQLPersistentStore extends PersistentStore try { await connection.transaction((ctx) async { var existingVersionRows = await ctx.query( - "SELECT versionNumber, dateOfUpgrade FROM $_versionTableName WHERE versionNumber=@v:int4", + "SELECT versionNumber, dateOfUpgrade FROM $versionTableName WHERE versionNumber=@v:int4", substitutionValues: {"v": versionNumber}); if (existingVersionRows.length > 0) { var date = existingVersionRows.first.last; @@ -177,7 +186,7 @@ class PostgreSQLPersistentStore extends PersistentStore } await ctx.execute( - "INSERT INTO $_versionTableName (versionNumber, dateOfUpgrade) VALUES ($versionNumber, '${new DateTime.now().toUtc().toIso8601String()}')"); + "INSERT INTO $versionTableName (versionNumber, dateOfUpgrade) VALUES ($versionNumber, '${new DateTime.now().toUtc().toIso8601String()}')"); }); } on PostgreSQLException catch (e) { throw _interpretException(e); @@ -200,8 +209,8 @@ class PostgreSQLPersistentStore extends PersistentStore value: (PersistentColumnMapping m) => m.value); var queryStringBuffer = new StringBuffer(); - queryStringBuffer.write( - "INSERT INTO ${q.rootEntity.tableName} ($columnsBeingInserted) "); + queryStringBuffer + .write("INSERT INTO ${q.entity.tableName} ($columnsBeingInserted) "); queryStringBuffer.write("VALUES (${valueKeysToBeInserted}) "); if (q.resultKeys != null && q.resultKeys.length > 0) { @@ -236,8 +245,8 @@ class PostgreSQLPersistentStore extends PersistentStore } }).join(","); - var queryStringBuffer = new StringBuffer( - "SELECT $columnsToFetch FROM ${q.rootEntity.tableName} "); + var queryStringBuffer = + new StringBuffer("SELECT $columnsToFetch FROM ${q.entity.tableName} "); joinElements.forEach((PersistentColumnMapping je) { PersistentJoinMapping joinElement = je; queryStringBuffer.write("${_joinStringForJoin(joinElement)} "); @@ -282,7 +291,7 @@ class PostgreSQLPersistentStore extends PersistentStore Map valueMap = null; var queryStringBuffer = new StringBuffer(); - queryStringBuffer.write("DELETE FROM ${q.rootEntity.tableName} "); + queryStringBuffer.write("DELETE FROM ${q.entity.tableName} "); if (q.predicate != null) { queryStringBuffer.write("where ${q.predicate.format} "); @@ -319,8 +328,7 @@ class PostgreSQLPersistentStore extends PersistentStore }).join(","); var queryStringBuffer = new StringBuffer(); - queryStringBuffer - .write("UPDATE ${q.rootEntity.tableName} SET $setPairString "); + queryStringBuffer.write("UPDATE ${q.entity.tableName} SET $setPairString "); if (q.predicate != null) { queryStringBuffer.write("where ${q.predicate.format} "); @@ -536,7 +544,7 @@ class PostgreSQLPersistentStore extends PersistentStore } var joinedSortDescriptors = sortDescs.map((QuerySortDescriptor sd) { - var property = q.rootEntity.properties[sd.key]; + var property = q.entity.properties[sd.key]; var columnName = "${property.entity.tableName}.${_columnNameForProperty(property)}"; var order = (sd.order == QuerySortOrder.ascending ? "ASC" : "DESC"); @@ -554,8 +562,8 @@ class PostgreSQLPersistentStore extends PersistentStore var operator = (query.pageDescriptor.order == QuerySortOrder.ascending ? ">" : "<"); var keyName = "aq_page_value"; - var typedKeyName = _typedColumnName(keyName, - query.rootEntity.properties[query.pageDescriptor.propertyName]); + var typedKeyName = _typedColumnName( + keyName, query.entity.properties[query.pageDescriptor.propertyName]); return new QueryPredicate( "${query.pageDescriptor.propertyName} ${operator} @$typedKeyName", {"$keyName": query.pageDescriptor.boundingValue}); @@ -584,7 +592,7 @@ class PostgreSQLPersistentStore extends PersistentStore Future _createVersionTableIfNecessary(bool temporary) async { var conn = await getDatabaseConnection(); - var commands = createTable(_versionTable, isTemporary: temporary); + var commands = createTable(versionTable, isTemporary: temporary); try { await conn.transaction((ctx) async { for (var cmd in commands) { diff --git a/lib/db/postgresql/postgresql_schema_generator.dart b/lib/src/db/postgresql/postgresql_schema_generator.dart similarity index 93% rename from lib/db/postgresql/postgresql_schema_generator.dart rename to lib/src/db/postgresql/postgresql_schema_generator.dart index 4bac7ba4c..332ae9511 100644 --- a/lib/db/postgresql/postgresql_schema_generator.dart +++ b/lib/src/db/postgresql/postgresql_schema_generator.dart @@ -1,7 +1,8 @@ -part of aqueduct; +import '../schema/schema.dart'; +import '../managed/managed.dart'; -class _PostgreSQLSchemaGenerator { - String get _versionTableName => "_aqueduct_version_pgsql"; +class PostgreSQLSchemaGenerator { + String get versionTableName => "_aqueduct_version_pgsql"; List createTable(SchemaTable table, {bool isTemporary: false}) { var columnString = @@ -142,7 +143,7 @@ class _PostgreSQLSchemaGenerator { return [ "ALTER TABLE ONLY ${tableName} ADD FOREIGN KEY (${_columnNameForColumn(column)}) " "REFERENCES ${column.relatedTableName} (${column.relatedColumnName}) " - "ON DELETE ${_deleteRuleStringForDeleteRule(column._deleteRule)}" + "ON DELETE ${_deleteRuleStringForDeleteRule(SchemaColumn.deleteRuleStringForDeleteRule(column.deleteRule))}" ]; } @@ -191,7 +192,7 @@ class _PostgreSQLSchemaGenerator { } String _postgreSQLTypeForColumn(SchemaColumn t) { - switch (t._type) { + switch (t.typeString) { case "integer": { if (t.autoincrement) { @@ -221,18 +222,16 @@ class _PostgreSQLSchemaGenerator { return null; } - SchemaTable get _versionTable { + SchemaTable get versionTable { return new SchemaTable.empty() - ..name = _versionTableName + ..name = versionTableName ..columns = [ (new SchemaColumn.empty() ..name = "versionNumber" - .._type = - SchemaColumn.typeStringForType(ManagedPropertyType.integer)), + ..type = ManagedPropertyType.integer), (new SchemaColumn.empty() ..name = "dateOfUpgrade" - .._type = - SchemaColumn.typeStringForType(ManagedPropertyType.datetime)), + ..type = ManagedPropertyType.datetime), ]; } } diff --git a/lib/db/query/matcher_expression.dart b/lib/src/db/query/matcher_expression.dart similarity index 72% rename from lib/db/query/matcher_expression.dart rename to lib/src/db/query/matcher_expression.dart index 5a54502da..df789e500 100644 --- a/lib/db/query/matcher_expression.dart +++ b/lib/src/db/query/matcher_expression.dart @@ -1,4 +1,5 @@ -part of aqueduct; +import 'matcher_internal.dart'; +import 'query.dart'; /// The operator in a comparison matcher. enum MatcherOperator { @@ -20,7 +21,7 @@ enum StringMatcherOperator { beginsWith, contains, endsWith } /// var query = new Query() /// ..matchOn.id = whereEqualTo(1); dynamic whereEqualTo(dynamic value) { - return new _ComparisonMatcherExpression(value, MatcherOperator.equalTo); + return new ComparisonMatcherExpression(value, MatcherOperator.equalTo); } /// Matcher for matching a column value greater than the argument in a [Query]. @@ -30,7 +31,7 @@ dynamic whereEqualTo(dynamic value) { /// var query = new Query() /// ..matchOn.salary = whereGreaterThan(60000); dynamic whereGreaterThan(dynamic value) { - return new _ComparisonMatcherExpression(value, MatcherOperator.greaterThan); + return new ComparisonMatcherExpression(value, MatcherOperator.greaterThan); } /// Matcher for matching a column value greater than or equal to the argument in a [Query]. @@ -40,7 +41,7 @@ dynamic whereGreaterThan(dynamic value) { /// var query = new Query() /// ..matchOn.salary = whereGreaterThanEqualTo(60000); dynamic whereGreaterThanEqualTo(dynamic value) { - return new _ComparisonMatcherExpression( + return new ComparisonMatcherExpression( value, MatcherOperator.greaterThanEqualTo); } @@ -51,7 +52,7 @@ dynamic whereGreaterThanEqualTo(dynamic value) { /// var query = new Query() /// ..matchOn.salary = whereLessThan(60000); dynamic whereLessThan(dynamic value) { - return new _ComparisonMatcherExpression(value, MatcherOperator.lessThan); + return new ComparisonMatcherExpression(value, MatcherOperator.lessThan); } /// Matcher for matching a column value less than or equal to the argument in a [Query]. @@ -61,7 +62,7 @@ dynamic whereLessThan(dynamic value) { /// var query = new Query() /// ..matchOn.salary = whereLessThanEqualTo(60000); dynamic whereLessThanEqualTo(dynamic value) { - return new _ComparisonMatcherExpression( + return new ComparisonMatcherExpression( value, MatcherOperator.lessThanEqualTo); } @@ -72,7 +73,7 @@ dynamic whereLessThanEqualTo(dynamic value) { /// var query = new Query() /// ..matchOn.id = whereNotEqual(60000); dynamic whereNotEqual(dynamic value) { - return new _ComparisonMatcherExpression(value, MatcherOperator.notEqual); + return new ComparisonMatcherExpression(value, MatcherOperator.notEqual); } /// Matcher for matching string properties that contain [value] in a [Query]. @@ -82,7 +83,7 @@ dynamic whereNotEqual(dynamic value) { /// var query = new Query() /// ..matchOn.title = whereContains("Director"); dynamic whereContains(String value) { - return new _StringMatcherExpression(value, StringMatcherOperator.contains); + return new StringMatcherExpression(value, StringMatcherOperator.contains); } /// Matcher for matching string properties that start with [value] in a [Query]. @@ -92,7 +93,7 @@ dynamic whereContains(String value) { /// var query = new Query() /// ..matchOn.name = whereBeginsWith("B"); dynamic whereBeginsWith(String value) { - return new _StringMatcherExpression(value, StringMatcherOperator.beginsWith); + return new StringMatcherExpression(value, StringMatcherOperator.beginsWith); } /// Matcher for matching string properties that end with [value] in a [Query]. @@ -102,7 +103,7 @@ dynamic whereBeginsWith(String value) { /// var query = new Query() /// ..matchOn.name = whereEndsWith("son"); dynamic whereEndsWith(String value) { - return new _StringMatcherExpression(value, StringMatcherOperator.endsWith); + return new StringMatcherExpression(value, StringMatcherOperator.endsWith); } /// Matcher for matching values that are within the list of [values] in a [Query]. @@ -112,7 +113,7 @@ dynamic whereEndsWith(String value) { /// var query = new Query() /// ..matchOn.department = whereIn(["Engineering", "HR"]); dynamic whereIn(Iterable values) { - return new _WithinMatcherExpression(values.toList()); + return new WithinMatcherExpression(values.toList()); } /// Matcher for matching column values where [lhs] <= value <= [rhs] in a [Query]. @@ -122,7 +123,7 @@ dynamic whereIn(Iterable values) { /// var query = new Query() /// ..matchOn.salary = whereBetween(80000, 100000); dynamic whereBetween(dynamic lhs, dynamic rhs) { - return new _RangeMatcherExpression(lhs, rhs, true); + return new RangeMatcherExpression(lhs, rhs, true); } /// Matcher for matching column values where matched value is less than [lhs] or greater than [rhs] in a [Query]. @@ -132,7 +133,7 @@ dynamic whereBetween(dynamic lhs, dynamic rhs) { /// var query = new Query() /// ..matchOn.salary = whereOutsideOf(80000, 100000); dynamic whereOutsideOf(dynamic lhs, dynamic rhs) { - return new _RangeMatcherExpression(lhs, rhs, false); + return new RangeMatcherExpression(lhs, rhs, false); } /// Matcher for matching [ManagedRelationship] property in a [Query]. @@ -146,7 +147,7 @@ dynamic whereOutsideOf(dynamic lhs, dynamic rhs) { /// var q = new Query() /// ..matchOn.user = whereRelatedByValue(userPrimaryKey); dynamic whereRelatedByValue(dynamic foreignKeyValue) { - return new _ComparisonMatcherExpression( + return new ComparisonMatcherExpression( foreignKeyValue, MatcherOperator.equalTo); } @@ -156,7 +157,7 @@ dynamic whereRelatedByValue(dynamic foreignKeyValue) { /// /// var q = new Query() /// ..matchOn.manager = whereNull; -const dynamic whereNull = const _NullMatcherExpression(true); +const dynamic whereNull = const NullMatcherExpression(true); /// Matcher for matching everything but null in a [Query]. /// @@ -164,42 +165,7 @@ const dynamic whereNull = const _NullMatcherExpression(true); /// /// var q = new Query() /// ..matchOn.manager = whereNotNull; -const dynamic whereNotNull = const _NullMatcherExpression(false); - -abstract class _MatcherExpression {} - -class _ComparisonMatcherExpression implements _MatcherExpression { - const _ComparisonMatcherExpression(this.value, this.operator); - - final dynamic value; - final MatcherOperator operator; -} - -class _RangeMatcherExpression implements _MatcherExpression { - const _RangeMatcherExpression(this.lhs, this.rhs, this.within); - - final bool within; - final dynamic lhs, rhs; -} - -class _NullMatcherExpression implements _MatcherExpression { - const _NullMatcherExpression(this.shouldBeNull); - - final bool shouldBeNull; -} - -class _WithinMatcherExpression implements _MatcherExpression { - _WithinMatcherExpression(this.values); - - List values; -} - -class _StringMatcherExpression implements _MatcherExpression { - _StringMatcherExpression(this.value, this.operator); - - StringMatcherOperator operator; - String value; -} +const dynamic whereNotNull = const NullMatcherExpression(false); /// Thrown when a [Query] matcher is invalid. class PredicateMatcherException implements Exception { diff --git a/lib/src/db/query/matcher_internal.dart b/lib/src/db/query/matcher_internal.dart new file mode 100644 index 000000000..b025bb298 --- /dev/null +++ b/lib/src/db/query/matcher_internal.dart @@ -0,0 +1,36 @@ +import 'query.dart'; + +abstract class MatcherExpression {} + +class ComparisonMatcherExpression implements MatcherExpression { + const ComparisonMatcherExpression(this.value, this.operator); + + final dynamic value; + final MatcherOperator operator; +} + +class RangeMatcherExpression implements MatcherExpression { + const RangeMatcherExpression(this.lhs, this.rhs, this.within); + + final bool within; + final dynamic lhs, rhs; +} + +class NullMatcherExpression implements MatcherExpression { + const NullMatcherExpression(this.shouldBeNull); + + final bool shouldBeNull; +} + +class WithinMatcherExpression implements MatcherExpression { + WithinMatcherExpression(this.values); + + List values; +} + +class StringMatcherExpression implements MatcherExpression { + StringMatcherExpression(this.value, this.operator); + + StringMatcherOperator operator; + String value; +} diff --git a/lib/db/query/page.dart b/lib/src/db/query/page.dart similarity index 99% rename from lib/db/query/page.dart rename to lib/src/db/query/page.dart index 0daf42f23..b217052d1 100644 --- a/lib/db/query/page.dart +++ b/lib/src/db/query/page.dart @@ -1,4 +1,4 @@ -part of aqueduct; +import 'query.dart'; /// A description of a page of results to be applied to a [Query]. /// diff --git a/lib/db/query/predicate.dart b/lib/src/db/query/predicate.dart similarity index 86% rename from lib/db/query/predicate.dart rename to lib/src/db/query/predicate.dart index 0a8fbc771..2ce570c10 100644 --- a/lib/db/query/predicate.dart +++ b/lib/src/db/query/predicate.dart @@ -1,4 +1,7 @@ -part of aqueduct; +import 'query.dart'; +import '../managed/managed.dart'; +import '../persistent_store/persistent_store.dart'; +import '../query/matcher_internal.dart'; /// A predicate contains instructions for filtering rows when performing a [Query]. /// @@ -29,10 +32,10 @@ class QueryPredicate { /// The [format] and [parameters] of this predicate. QueryPredicate(this.format, this.parameters); - factory QueryPredicate._fromQueryIncludable( + factory QueryPredicate.fromQueryIncludable( QueryMatchable obj, PersistentStore persistentStore) { var entity = obj.entity; - var attributeKeys = obj._matcherMap.keys.where((propertyName) { + var attributeKeys = obj.backingMap.keys.where((propertyName) { var desc = entity.properties[propertyName]; if (desc is ManagedRelationshipDescription) { return desc.relationshipType == ManagedRelationshipType.belongsTo; @@ -43,19 +46,19 @@ class QueryPredicate { return QueryPredicate.andPredicates(attributeKeys.map((queryKey) { var desc = entity.properties[queryKey]; - var matcher = obj._matcherMap[queryKey]; + var matcher = obj.backingMap[queryKey]; - if (matcher is _ComparisonMatcherExpression) { + if (matcher is ComparisonMatcherExpression) { return persistentStore.comparisonPredicate( desc, matcher.operator, matcher.value); - } else if (matcher is _RangeMatcherExpression) { + } else if (matcher is RangeMatcherExpression) { return persistentStore.rangePredicate( desc, matcher.lhs, matcher.rhs, matcher.within); - } else if (matcher is _NullMatcherExpression) { + } else if (matcher is NullMatcherExpression) { return persistentStore.nullPredicate(desc, matcher.shouldBeNull); - } else if (matcher is _WithinMatcherExpression) { + } else if (matcher is WithinMatcherExpression) { return persistentStore.containsPredicate(desc, matcher.values); - } else if (matcher is _StringMatcherExpression) { + } else if (matcher is StringMatcherExpression) { return persistentStore.stringPredicate( desc, matcher.operator, matcher.value); } diff --git a/lib/db/query/query.dart b/lib/src/db/query/query.dart similarity index 84% rename from lib/db/query/query.dart rename to lib/src/db/query/query.dart index 0d9285efc..3029b5920 100644 --- a/lib/db/query/query.dart +++ b/lib/src/db/query/query.dart @@ -1,4 +1,18 @@ -part of aqueduct; +import 'dart:async'; + +import '../managed/managed.dart'; +import '../managed/backing.dart'; +import 'page.dart'; +import 'predicate.dart'; +import 'sort_descriptor.dart'; +import '../persistent_store/persistent_store.dart'; +import '../persistent_store/persistent_store_query.dart'; +import 'query_mapping.dart'; + +export 'matcher_expression.dart'; +export 'page.dart'; +export 'predicate.dart'; +export 'sort_descriptor.dart'; /// Contains information for building and executing a database operation. /// @@ -48,7 +62,7 @@ class Query { InstanceType get matchOn { if (_matchOn == null) { _matchOn = entity.newInstance() as InstanceType; - _matchOn._backing = new _ManagedMatcherBacking(); + _matchOn.backing = new ManagedMatcherBacking(); } return _matchOn; } @@ -125,6 +139,45 @@ class Query { InstanceType _valueObject; + PersistentStoreQuery persistentQueryForStore(PersistentStore store) { + var psq = new PersistentStoreQuery() + ..confirmQueryModifiesAllInstancesOnDeleteOrUpdate = + confirmQueryModifiesAllInstancesOnDeleteOrUpdate + ..entity = entity + ..timeoutInSeconds = timeoutInSeconds + ..sortDescriptors = sortDescriptors + ..resultKeys = mappingElementsForList( + (resultProperties ?? entity.defaultProperties), entity); + + if (_matchOn != null) { + psq.predicate = new QueryPredicate.fromQueryIncludable(_matchOn, store); + } else { + psq.predicate = predicate; + } + + if (_matchOn?.hasJoinElements ?? false) { + if (pageDescriptor != null) { + throw new QueryException(QueryExceptionEvent.requestFailure, + message: + "Query cannot have properties that are includeInResultSet and also have a pageDescriptor."); + } + + var joinElements = joinElementsFromQueryMatchable( + matchOn, store, nestedResultProperties); + psq.resultKeys.addAll(joinElements); + } else { + psq.fetchLimit = fetchLimit; + psq.offset = offset; + + psq.pageDescriptor = validatePageDescriptor(entity, pageDescriptor); + + psq.values = + mappingElementsForMap((valueMap ?? values?.backingMap), entity); + } + + return psq; + } + /// A list of properties to be fetched by this query. /// /// Each [InstanceType] will have these properties set when this query is executed. Each property must be @@ -151,7 +204,7 @@ class Query { /// ..values.name = "Joe"; /// var newUser = await q.insert(); Future insert() async { - return await context._executeInsertQuery(this); + return await context.executeInsertQuery(this); } /// Updates [InstanceType]s in the underlying database. @@ -167,7 +220,7 @@ class Query { /// ..values.name = "Joe"; /// var usersNamedFredNowNamedJoe = await q.update(); Future> update() async { - return await context._executeUpdateQuery(this); + return await context.executeUpdateQuery(this); } /// Updates an [InstanceType] in the underlying database. @@ -175,7 +228,7 @@ class Query { /// This method works the same as [update], except it may only update one row in the underlying database. If this method /// ends up modifying multiple rows, an exception is thrown. Future updateOne() async { - var results = await context._executeUpdateQuery(this); + var results = await context.executeUpdateQuery(this); if (results.length == 1) { return results.first; } else if (results.length == 0) { @@ -195,7 +248,7 @@ class Query { /// var allUsers = q.fetch(); /// Future> fetch() async { - return await context._executeFetchQuery(this); + return await context.executeFetchQuery(this); } /// Fetches a single [InstanceType] from the database. @@ -204,7 +257,7 @@ class Query { Future fetchOne() async { fetchLimit = 1; - var results = await context._executeFetchQuery(this); + var results = await context.executeFetchQuery(this); if (results.length == 1) { return results.first; } else if (results.length > 1) { @@ -228,7 +281,7 @@ class Query { /// ..matchOn.id = whereEqualTo(1); /// var deletedCount = await q.delete(); Future delete() async { - return await context._executeDeleteQuery(this); + return await context.executeDeleteQuery(this); } } @@ -275,31 +328,10 @@ enum QueryExceptionEvent { requestFailure } -/// Used internally. abstract class QueryMatchable { ManagedEntity entity; bool includeInResultSet; - Map get _matcherMap; -} - -abstract class _QueryMatchableExtension implements QueryMatchable { - bool get _hasJoinElements { - return _matcherMap.values - .where((item) => item is QueryMatchable) - .any((QueryMatchable item) => item.includeInResultSet); - } - - List get _joinPropertyKeys { - return _matcherMap.keys.where((propertyName) { - var val = _matcherMap[propertyName]; - var relDesc = entity.relationships[propertyName]; - - return val is QueryMatchable && - val.includeInResultSet && - (relDesc?.relationshipType == ManagedRelationshipType.hasMany || - relDesc?.relationshipType == ManagedRelationshipType.hasOne); - }).toList(); - } + Map get backingMap; } diff --git a/lib/src/db/query/query_mapping.dart b/lib/src/db/query/query_mapping.dart new file mode 100644 index 000000000..4503be2c2 --- /dev/null +++ b/lib/src/db/query/query_mapping.dart @@ -0,0 +1,135 @@ +import 'dart:mirrors'; + +import 'query.dart'; +import '../managed/managed.dart'; +import '../persistent_store/persistent_store.dart'; +import '../managed/query_matchable.dart'; +import '../persistent_store/persistent_store_query.dart'; + +ManagedPropertyDescription _propertyForName( + ManagedEntity entity, String propertyName) { + var property = entity.properties[propertyName]; + if (property == null) { + throw new QueryException(QueryExceptionEvent.internalFailure, + message: + "Property $propertyName does not exist on ${entity.tableName}"); + } + if (property is ManagedRelationshipDescription && + property.relationshipType != ManagedRelationshipType.belongsTo) { + throw new QueryException(QueryExceptionEvent.internalFailure, + message: + "Property $propertyName is a hasMany or hasOne relationship and is invalid as a result property of ${entity + .tableName}, use matchOn.$propertyName.includeInResultSet = true instead."); + } + + return property; +} + +List mappingElementsForList( + List keys, ManagedEntity entity) { + if (!keys.contains(entity.primaryKey)) { + keys.add(entity.primaryKey); + } + + return keys.map((key) { + var property = _propertyForName(entity, key); + return new PersistentColumnMapping(property, null); + }).toList(); +} + +QueryPage validatePageDescriptor(ManagedEntity entity, QueryPage page) { + if (page == null) { + return null; + } + + var prop = entity.attributes[page.propertyName]; + if (prop == null) { + throw new QueryException(QueryExceptionEvent.requestFailure, + message: + "Property ${page.propertyName} in pageDescriptor does not exist on ${entity.tableName}."); + } + + if (page.boundingValue != null && + !prop.isAssignableWith(page.boundingValue)) { + throw new QueryException(QueryExceptionEvent.requestFailure, + message: + "Property ${page.propertyName} in pageDescriptor has invalid type (${page.boundingValue.runtimeType})."); + } + + return page; +} + +List mappingElementsForMap( + Map valueMap, ManagedEntity entity) { + return valueMap?.keys + ?.map((key) { + var property = entity.properties[key]; + if (property == null) { + throw new QueryException(QueryExceptionEvent.requestFailure, + message: + "Property $key in values does not exist on ${entity.tableName}"); + } + + var value = valueMap[key]; + if (property is ManagedRelationshipDescription) { + if (property.relationshipType != ManagedRelationshipType.belongsTo) { + return null; + } + + if (value != null) { + if (value is ManagedObject) { + value = value[property.destinationEntity.primaryKey]; + } else if (value is Map) { + value = value[property.destinationEntity.primaryKey]; + } else { + throw new QueryException(QueryExceptionEvent.internalFailure, + message: + "Property $key on ${entity.tableName} in Query values must be a Map or ${MirrorSystem.getName( + property.destinationEntity.instanceType.simpleName)} "); + } + } + } + + return new PersistentColumnMapping(property, value); + }) + ?.where((m) => m != null) + ?.toList(); +} + +List joinElementsFromQueryMatchable( + QueryMatchableExtension matcherBackedObject, + PersistentStore store, + Map> nestedResultProperties) { + var entity = matcherBackedObject.entity; + var propertiesToJoin = matcherBackedObject.joinPropertyKeys; + + return propertiesToJoin + .map((propertyName) { + QueryMatchableExtension inner = + matcherBackedObject.backingMap[propertyName]; + + var relDesc = entity.relationships[propertyName]; + var predicate = new QueryPredicate.fromQueryIncludable(inner, store); + var nestedProperties = + nestedResultProperties[inner.entity.instanceType.reflectedType]; + var propertiesToFetch = + nestedProperties ?? inner.entity.defaultProperties; + + var joinElements = [ + new PersistentJoinMapping( + PersistentJoinType.leftOuter, + relDesc, + predicate, + mappingElementsForList(propertiesToFetch, inner.entity)) + ]; + + if (inner.hasJoinElements) { + joinElements.addAll(joinElementsFromQueryMatchable( + inner, store, nestedResultProperties)); + } + + return joinElements; + }) + .expand((l) => l) + .toList(); +} diff --git a/lib/db/query/sort_descriptor.dart b/lib/src/db/query/sort_descriptor.dart similarity index 97% rename from lib/db/query/sort_descriptor.dart rename to lib/src/db/query/sort_descriptor.dart index fbb0a78af..f0d50a06d 100644 --- a/lib/db/query/sort_descriptor.dart +++ b/lib/src/db/query/sort_descriptor.dart @@ -1,5 +1,3 @@ -part of aqueduct; - /// Order value for [QuerySortDescriptor]s and [QueryPage]s. enum QuerySortOrder { /// Ascending order. Example: 1, 2, 3, 4, ... diff --git a/lib/db/schema/migration.dart b/lib/src/db/schema/migration.dart similarity index 89% rename from lib/db/schema/migration.dart rename to lib/src/db/schema/migration.dart index 6fc5a3d4e..dcd27045f 100644 --- a/lib/db/schema/migration.dart +++ b/lib/src/db/schema/migration.dart @@ -1,4 +1,12 @@ -part of aqueduct; +import 'dart:async'; +import 'dart:io'; +import 'dart:mirrors'; + +import '../managed/managed.dart'; +import '../persistent_store/persistent_store.dart'; +import 'schema.dart'; +import '../../utilities/source_generator.dart'; +import '../postgresql/postgresql_persistent_store.dart'; /// Thrown when [Migration] encounters an error. class MigrationException { @@ -77,7 +85,9 @@ class MigrationExecutor { return m; } catch (e) { throw new MigrationException( - "Migration files must have the following format: Version_Name.migration.dart, where Version must be an integer (optionally prefixed with 0s, e.g. '00000002') and '_Name' is optional. Offender: ${fse.uri}"); + "Migration files must have the following format: Version_Name.migration.dart," + "where Version must be an integer (optionally prefixed with 0s, e.g. '00000002')" + " and '_Name' is optional. Offender: ${fse.uri}"); } }); @@ -99,10 +109,9 @@ class MigrationExecutor { "Migration directory doesn't contain any migrations, nothing to validate."); } - var generator = new _SourceGenerator( + var generator = new SourceGenerator( (List args, Map values) async { - var dataModel = new ManagedDataModel.fromURI( - new Uri(scheme: "package", path: args[0])); + var dataModel = new ManagedDataModel.fromCurrentMirrorSystem(); var schema = new Schema.fromDataModel(dataModel); return schema.asMap(); @@ -114,7 +123,7 @@ class MigrationExecutor { "dart:async" ]); - var executor = new _IsolateExecutor(generator, [libraryName], + var executor = new IsolateExecutor(generator, [libraryName], packageConfigURI: projectDirectoryPath.resolve(".packages")); var projectSchema = new Schema.fromMap(await executor.execute( workingDirectory: projectDirectoryPath) as Map); @@ -142,7 +151,7 @@ class MigrationExecutor { var files = migrationFiles; if (!files.isEmpty) { // For now, just make a new empty one... - var newVersionNumber = _versionNumberFromFile(files.last) + 1; + var newVersionNumber = versionNumberFromFile(files.last) + 1; var contents = SchemaBuilder.sourceForSchemaUpgrade( new Schema.empty(), new Schema.empty(), newVersionNumber); var file = new File.fromUri(migrationFileDirectory.resolve( @@ -152,10 +161,9 @@ class MigrationExecutor { return file; } - var generator = new _SourceGenerator( + var generator = new SourceGenerator( (List args, Map values) async { - var dataModel = new ManagedDataModel.fromURI( - new Uri(scheme: "package", path: args[0])); + var dataModel = new ManagedDataModel.fromCurrentMirrorSystem(); var schema = new Schema.fromDataModel(dataModel); return SchemaBuilder.sourceForSchemaUpgrade( @@ -168,7 +176,7 @@ class MigrationExecutor { "dart:async" ]); - var executor = new _IsolateExecutor(generator, [libraryName], + var executor = new IsolateExecutor(generator, [libraryName], packageConfigURI: projectDirectoryPath.resolve(".packages")); var contents = await executor.execute(workingDirectory: projectDirectoryPath); @@ -211,7 +219,7 @@ class MigrationExecutor { /////// - int _versionNumberFromFile(File file) { + int versionNumberFromFile(File file) { var fileName = file.uri.pathSegments.last; var migrationName = fileName.split(".").first; return int.parse(migrationName.split("_").first); @@ -221,7 +229,7 @@ class MigrationExecutor { var files = migrationFiles; var latestMigrationFile = files.last; var latestMigrationVersionNumber = - _versionNumberFromFile(latestMigrationFile); + versionNumberFromFile(latestMigrationFile); List migrationFilesToRun = []; List migrationFilesToGetToCurrent = []; @@ -229,7 +237,7 @@ class MigrationExecutor { migrationFilesToRun = files; } else if (latestMigrationVersionNumber > aroundVersion) { var indexOfCurrent = files.indexOf( - files.firstWhere((f) => _versionNumberFromFile(f) == aroundVersion)); + files.firstWhere((f) => versionNumberFromFile(f) == aroundVersion)); migrationFilesToGetToCurrent = files.sublist(0, indexOfCurrent + 1); migrationFilesToRun = files.sublist(indexOfCurrent + 1); } else { @@ -241,7 +249,7 @@ class MigrationExecutor { Future _executeUpgradeForFile(File file, Schema schema, {bool dryRun: false}) async { - var generator = new _SourceGenerator( + var generator = new SourceGenerator( (List args, Map values) async { var inputSchema = new Schema.fromMap(values["schema"] as Map); @@ -289,8 +297,8 @@ class MigrationExecutor { "dart:mirrors" ], additionalContents: file.readAsStringSync()); - var executor = new _IsolateExecutor(generator, [ - "${_versionNumberFromFile(file)}" + var executor = new IsolateExecutor(generator, [ + "${versionNumberFromFile(file)}" ], message: { "dryRun": dryRun, "schema": schema.asMap(), diff --git a/lib/db/schema/schema.dart b/lib/src/db/schema/schema.dart similarity index 94% rename from lib/db/schema/schema.dart rename to lib/src/db/schema/schema.dart index 9ff1f98f5..348e7dc81 100644 --- a/lib/db/schema/schema.dart +++ b/lib/src/db/schema/schema.dart @@ -1,4 +1,11 @@ -part of aqueduct; +import '../managed/managed.dart'; + +import 'schema_table.dart'; + +export 'migration.dart'; +export 'schema_builder.dart'; +export 'schema_column.dart'; +export 'schema_table.dart'; /// Thrown when a [Schema] encounters an error. class SchemaException implements Exception { @@ -14,9 +21,8 @@ class Schema { Schema(this.tables); Schema.fromDataModel(ManagedDataModel dataModel) { - tables = dataModel._entities.values - .map((e) => new SchemaTable.fromEntity(e)) - .toList(); + tables = + dataModel.entities.map((e) => new SchemaTable.fromEntity(e)).toList(); } Schema.from(Schema otherSchema) { diff --git a/lib/db/schema/schema_builder.dart b/lib/src/db/schema/schema_builder.dart similarity index 94% rename from lib/db/schema/schema_builder.dart rename to lib/src/db/schema/schema_builder.dart index 6aa50bb9a..0375ff13b 100644 --- a/lib/db/schema/schema_builder.dart +++ b/lib/src/db/schema/schema_builder.dart @@ -1,4 +1,5 @@ -part of aqueduct; +import 'schema.dart'; +import '../persistent_store/persistent_store.dart'; /// Used during migration to modify a schema. class SchemaBuilder { @@ -135,9 +136,9 @@ class SchemaBuilder { var newColumn = new SchemaColumn.from(existingColumn); modify(newColumn); - if (existingColumn._type != newColumn._type) { + if (existingColumn.type != newColumn.type) { throw new SchemaException( - "May not change column (${existingColumn.name}) type (${existingColumn._type} -> ${newColumn._type})"); + "May not change column (${existingColumn.name}) type (${existingColumn.typeString} -> ${newColumn.typeString})"); } if (existingColumn.autoincrement != newColumn.autoincrement) { @@ -171,7 +172,7 @@ class SchemaBuilder { "May not change column (${existingColumn.name}) to be nullable without unencodedInitialValue."); } - table._replaceColumn(existingColumn, newColumn); + table.replaceColumn(existingColumn, newColumn); if (store != null) { if (existingColumn.isIndexed != newColumn.isIndexed) { @@ -195,7 +196,7 @@ class SchemaBuilder { commands.addAll(store.alterColumnDefaultValue(table, newColumn)); } - if (existingColumn._deleteRule != newColumn._deleteRule) { + if (existingColumn.deleteRule != newColumn.deleteRule) { commands.addAll(store.alterColumnDeleteRule(table, newColumn)); } } @@ -248,14 +249,13 @@ class SchemaBuilder { var builder = new StringBuffer(); if (column.relatedTableName != null) { builder.write( - '${spaceOffset}new SchemaColumn.relationship("${column.name}", ${SchemaColumn.typeFromTypeString(column._type)}'); + '${spaceOffset}new SchemaColumn.relationship("${column.name}", ${column.type}'); builder.write(", relatedTableName: \"${column.relatedTableName}\""); builder.write(", relatedColumnName: \"${column.relatedColumnName}\""); - builder.write( - ", rule: ${SchemaColumn.deleteRuleForDeleteRuleString(column._deleteRule)}"); + builder.write(", rule: ${column.deleteRule}"); } else { builder.write( - '${spaceOffset}new SchemaColumn("${column.name}", ${SchemaColumn.typeFromTypeString(column._type)}'); + '${spaceOffset}new SchemaColumn("${column.name}", ${column.type}'); if (column.isPrimaryKey) { builder.write(", isPrimaryKey: true"); } else { diff --git a/lib/db/schema/schema_column.dart b/lib/src/db/schema/schema_column.dart similarity index 98% rename from lib/db/schema/schema_column.dart rename to lib/src/db/schema/schema_column.dart index 510d511c2..36ae75748 100644 --- a/lib/db/schema/schema_column.dart +++ b/lib/src/db/schema/schema_column.dart @@ -1,4 +1,7 @@ -part of aqueduct; +import 'dart:mirrors'; + +import '../managed/managed.dart'; +import 'schema.dart'; /// Represents a database column for a [SchemaTable]. /// @@ -80,6 +83,8 @@ class SchemaColumn { String name; String _type; + String get typeString => _type; + ManagedPropertyType get type => typeFromTypeString(_type); void set type(ManagedPropertyType t) { _type = typeStringForType(t); diff --git a/lib/db/schema/schema_table.dart b/lib/src/db/schema/schema_table.dart similarity index 96% rename from lib/db/schema/schema_table.dart rename to lib/src/db/schema/schema_table.dart index 62b9ba488..35bb1541c 100644 --- a/lib/db/schema/schema_table.dart +++ b/lib/src/db/schema/schema_table.dart @@ -1,4 +1,5 @@ -part of aqueduct; +import 'schema.dart'; +import '../managed/managed.dart'; /// Represents a database table for a [Schema]. /// @@ -116,7 +117,7 @@ class SchemaTable { columns.remove(column); } - void _replaceColumn(SchemaColumn existingColumn, SchemaColumn newColumn) { + void replaceColumn(SchemaColumn existingColumn, SchemaColumn newColumn) { existingColumn = columnForName(existingColumn.name); if (existingColumn == null) { throw new SchemaException( diff --git a/lib/http/body_decoder.dart b/lib/src/http/body_decoder.dart similarity index 98% rename from lib/http/body_decoder.dart rename to lib/src/http/body_decoder.dart index 6d303658f..a8910f0d2 100644 --- a/lib/http/body_decoder.dart +++ b/lib/src/http/body_decoder.dart @@ -1,4 +1,6 @@ -part of aqueduct; +import 'dart:async'; +import 'dart:io'; +import 'dart:convert'; /// A decoding method for decoding a stream of bytes from an HTTP request body into a String. /// diff --git a/lib/http/controller_routing.dart b/lib/src/http/controller_routing.dart similarity index 63% rename from lib/http/controller_routing.dart rename to lib/src/http/controller_routing.dart index 7fed5259d..4826b76e8 100644 --- a/lib/http/controller_routing.dart +++ b/lib/src/http/controller_routing.dart @@ -1,4 +1,5 @@ -part of aqueduct; +import 'http_controller_internal.dart'; +import 'http_controller.dart'; /// Indicates that an [HTTPController] method is triggered by an HTTP GET method. /// @@ -38,3 +39,30 @@ class HTTPMethod { /// Case-insensitive. final String method; } + +/// Marks a controller HTTPHeader or HTTPQuery property as required. +const HTTPRequiredParameter requiredHTTPParameter = + const HTTPRequiredParameter(); + +class HTTPRequiredParameter { + const HTTPRequiredParameter(); +} + +/// Specifies the route path variable for the associated controller method argument. +class HTTPPath extends HTTPParameter { + const HTTPPath(String segment) : super(segment); +} + +/// Metadata indicating a parameter to a controller's method should be set from +/// the HTTP header indicated by the [header] field. The [header] value is case- +/// insensitive. +class HTTPHeader extends HTTPParameter { + const HTTPHeader(String header) : super(header); +} + +/// Metadata indicating a parameter to a controller's method should be set from +/// the query value (or form-encoded body) from the indicated [key]. The [key] +/// value is case-sensitive. +class HTTPQuery extends HTTPParameter { + const HTTPQuery(String key) : super(key); +} diff --git a/lib/http/cors_policy.dart b/lib/src/http/cors_policy.dart similarity index 99% rename from lib/http/cors_policy.dart rename to lib/src/http/cors_policy.dart index 79f35c19b..9db4e3024 100644 --- a/lib/http/cors_policy.dart +++ b/lib/src/http/cors_policy.dart @@ -1,4 +1,5 @@ -part of aqueduct; +import 'dart:io'; +import 'http.dart'; /// Describes a CORS policy for a [RequestController]. /// diff --git a/lib/http/documentable.dart b/lib/src/http/documentable.dart similarity index 98% rename from lib/http/documentable.dart rename to lib/src/http/documentable.dart index 1fb3cb51c..1bec614ef 100644 --- a/lib/http/documentable.dart +++ b/lib/src/http/documentable.dart @@ -1,4 +1,7 @@ -part of aqueduct; +import 'dart:io'; +import 'dart:mirrors'; + +import 'package:path/path.dart' as path_lib; Map _stripNull(Map m) { var outMap = {}; @@ -418,19 +421,6 @@ enum APIParameterLocation { query, header, path, formData, cookie, body } /// Represents a parameter in the OpenAPI specification. class APIParameter { - static APIParameterLocation _parameterLocationFromHTTPParameter( - _HTTPParameter p) { - if (p is HTTPPath) { - return APIParameterLocation.path; - } else if (p is HTTPQuery) { - return APIParameterLocation.query; - } else if (p is HTTPHeader) { - return APIParameterLocation.header; - } - - return null; - } - static String parameterLocationStringForType( APIParameterLocation parameterLocation) { switch (parameterLocation) { diff --git a/lib/src/http/http.dart b/lib/src/http/http.dart new file mode 100644 index 000000000..59d529639 --- /dev/null +++ b/lib/src/http/http.dart @@ -0,0 +1,15 @@ +export 'body_decoder.dart'; +export 'controller_routing.dart'; +export 'cors_policy.dart'; +export 'documentable.dart'; +export 'http_controller.dart'; +export 'http_response_exception.dart'; +export 'query_controller.dart'; +export 'request.dart'; +export 'request_controller.dart'; +export 'request_path.dart'; +export 'request_sink.dart'; +export 'resource_controller.dart'; +export 'response.dart'; +export 'router.dart'; +export 'serializable.dart'; diff --git a/lib/http/http_controller.dart b/lib/src/http/http_controller.dart similarity index 83% rename from lib/http/http_controller.dart rename to lib/src/http/http_controller.dart index 98ce47f11..15f5178a9 100644 --- a/lib/http/http_controller.dart +++ b/lib/src/http/http_controller.dart @@ -1,4 +1,11 @@ -part of aqueduct; +import 'dart:async'; +import 'dart:io'; +import 'dart:mirrors'; + +import 'package:analyzer/analyzer.dart'; + +import 'http.dart'; +import 'http_controller_internal.dart'; /// Base class for HTTP web service controller. /// @@ -46,11 +53,10 @@ abstract class HTTPController extends RequestController { _applicationWWWFormURLEncodedContentType ]; - /// The content type of responses from this [HTTPController]. + /// The default content type of responses from this [HTTPController]. /// - /// This type will automatically be written to this response's - /// HTTP header. Defaults to "application/json". This value determines how the body data returned from this controller - /// in a [Response] is encoded. + /// If the [Response.contentType] has not explicitly been set by a responder method in this controller, the controller will set + /// that property with this value. Defaults to "application/json". ContentType responseContentType = ContentType.JSON; /// The HTTP request body object, after being decoded. @@ -73,12 +79,6 @@ abstract class HTTPController extends RequestController { /// handled by the appropriate responder method. void didDecodeRequestBody(dynamic decodedObject) {} - /// Executed prior to [Response] being sent, but after the responder method has been executed. - /// - /// This method is used to post-process a response before it is finally sent. By default, does nothing. - /// This method will have no impact on when or how the [Response] is sent, is is simply informative. - void willSendResponse(Response response) {} - bool _requestContentTypeIsSupported(Request req) { var incomingContentType = request.innerRequest.headers.contentType; return acceptedContentTypes.firstWhere((ct) { @@ -89,7 +89,7 @@ abstract class HTTPController extends RequestController { } Future _process() async { - var controllerCache = _HTTPControllerCache.cacheForType(runtimeType); + var controllerCache = HTTPControllerCache.cacheForType(runtimeType); var mapper = controllerCache.mapperForRequest(request); if (mapper == null) { return new Response( @@ -130,8 +130,8 @@ abstract class HTTPController extends RequestController { request.innerRequest.headers, queryParameters); var missingParameters = [orderedParameters, controllerProperties.values] .expand((p) => p) - .where((p) => p is _HTTPControllerMissingParameter) - .map((p) => p as _HTTPControllerMissingParameter) + .where((p) => p is HTTPControllerMissingParameter) + .map((p) => p as HTTPControllerMissingParameter) .toList(); if (missingParameters.length > 0) { return _missingRequiredParameterResponseIfNecessary(missingParameters); @@ -149,8 +149,9 @@ abstract class HTTPController extends RequestController { .reflectee as Future; var response = await eventualResponse; - response.headers[HttpHeaders.CONTENT_TYPE] = responseContentType; - willSendResponse(response); + if (!response.hasExplicitlySetContentType) { + response.contentType = responseContentType; + } return response; } @@ -172,14 +173,15 @@ abstract class HTTPController extends RequestController { } return response; - } on _InternalControllerException catch (e) { - return e.response; + } on InternalControllerException catch (e) { + var response = e.response; + return response; } } @override List documentOperations(PackagePathResolver resolver) { - var controllerCache = _HTTPControllerCache.cacheForType(runtimeType); + var controllerCache = HTTPControllerCache.cacheForType(runtimeType); var reflectedType = reflect(this).type; var uri = reflectedType.location.sourceUri; var fileUnit = parseDartFile(resolver.resolve(uri)); @@ -233,8 +235,8 @@ abstract class HTTPController extends RequestController { cachedMethod.optionalParameters.values, controllerCache.propertyCache.values ].expand((i) => i.toList()).map((param) { - var paramLocation = APIParameter - ._parameterLocationFromHTTPParameter(param.httpParameter); + var paramLocation = + _parameterLocationFromHTTPParameter(param.httpParameter); if (usesFormEncodedData && paramLocation == APIParameterLocation.query) { paramLocation = APIParameterLocation.formData; @@ -264,7 +266,7 @@ abstract class HTTPController extends RequestController { var symbol = APIOperation.symbolForID(operation.id, this); if (symbol != null) { - var controllerCache = _HTTPControllerCache.cacheForType(runtimeType); + var controllerCache = HTTPControllerCache.cacheForType(runtimeType); var methodMirror = reflect(this).type.declarations[symbol]; if (controllerCache.hasRequiredParametersForMethod(methodMirror)) { @@ -280,39 +282,14 @@ abstract class HTTPController extends RequestController { } } -class _InternalControllerException implements Exception { - final String message; - final int statusCode; - final HttpHeaders additionalHeaders; - final String responseMessage; - - _InternalControllerException(this.message, this.statusCode, - {HttpHeaders additionalHeaders: null, String responseMessage: null}) - : this.additionalHeaders = additionalHeaders, - this.responseMessage = responseMessage; - - Response get response { - var headerMap = {}; - additionalHeaders?.forEach((k, _) { - headerMap[k] = additionalHeaders.value(k); - }); - - var bodyMap = null; - if (responseMessage != null) { - bodyMap = {"error": responseMessage}; - } - return new Response(statusCode, headerMap, bodyMap); - } -} - Response _missingRequiredParameterResponseIfNecessary( - List<_HTTPControllerMissingParameter> params) { + List params) { var missingHeaders = params - .where((p) => p.type == _HTTPControllerMissingParameterType.header) + .where((p) => p.type == HTTPControllerMissingParameterType.header) .map((p) => p.externalName) .toList(); var missingQueryParameters = params - .where((p) => p.type == _HTTPControllerMissingParameterType.query) + .where((p) => p.type == HTTPControllerMissingParameterType.query) .map((p) => p.externalName) .toList(); @@ -332,3 +309,15 @@ Response _missingRequiredParameterResponseIfNecessary( return new Response.badRequest(body: {"error": missings.toString()}); } + +APIParameterLocation _parameterLocationFromHTTPParameter(HTTPParameter p) { + if (p is HTTPPath) { + return APIParameterLocation.path; + } else if (p is HTTPQuery) { + return APIParameterLocation.query; + } else if (p is HTTPHeader) { + return APIParameterLocation.header; + } + + return null; +} diff --git a/lib/http/parameter_matching.dart b/lib/src/http/http_controller_internal.dart similarity index 56% rename from lib/http/parameter_matching.dart rename to lib/src/http/http_controller_internal.dart index 445bffb57..57feee6a2 100644 --- a/lib/http/parameter_matching.dart +++ b/lib/src/http/http_controller_internal.dart @@ -1,69 +1,72 @@ -part of aqueduct; +import 'dart:io'; +import 'dart:mirrors'; + +import 'controller_routing.dart'; +import 'request.dart'; +import 'response.dart'; + +class InternalControllerException implements Exception { + final String message; + final int statusCode; + final HttpHeaders additionalHeaders; + final String responseMessage; + + InternalControllerException(this.message, this.statusCode, + {HttpHeaders additionalHeaders: null, String responseMessage: null}) + : this.additionalHeaders = additionalHeaders, + this.responseMessage = responseMessage; + + Response get response { + var headerMap = {}; + additionalHeaders?.forEach((k, _) { + headerMap[k] = additionalHeaders.value(k); + }); + + var bodyMap = null; + if (responseMessage != null) { + bodyMap = {"error": responseMessage}; + } + return new Response(statusCode, headerMap, bodyMap); + } +} /// Parent class for annotations used for optional parameters in controller methods -abstract class _HTTPParameter { - const _HTTPParameter(this.externalName); +abstract class HTTPParameter { + const HTTPParameter(this.externalName); /// The name of the variable in the HTTP request. final String externalName; } -/// Marks a controller HTTPHeader or HTTPQuery property as required. -const HTTPRequiredParameter requiredHTTPParameter = - const HTTPRequiredParameter(); - -class HTTPRequiredParameter { - const HTTPRequiredParameter(); -} - -/// Specifies the route path variable for the associated controller method argument. -class HTTPPath extends _HTTPParameter { - const HTTPPath(String segment) : super(segment); -} - -/// Metadata indicating a parameter to a controller's method should be set from -/// the HTTP header indicated by the [header] field. The [header] value is case- -/// insensitive. -class HTTPHeader extends _HTTPParameter { - const HTTPHeader(String header) : super(header); -} - -/// Metadata indicating a parameter to a controller's method should be set from -/// the query value (or form-encoded body) from the indicated [key]. The [key] -/// value is case-sensitive. -class HTTPQuery extends _HTTPParameter { - const HTTPQuery(String key) : super(key); -} - -class _HTTPControllerCache { - static Map controllerCache = {}; - static _HTTPControllerCache cacheForType(Type t) { +class HTTPControllerCache { + static Map controllerCache = {}; + static HTTPControllerCache cacheForType(Type t) { var cacheItem = controllerCache[t]; if (cacheItem != null) { return cacheItem; } - controllerCache[t] = new _HTTPControllerCache(t); + controllerCache[t] = new HTTPControllerCache(t); return controllerCache[t]; } - _HTTPControllerCache(Type controllerType) { + HTTPControllerCache(Type controllerType) { var allDeclarations = reflectClass(controllerType).declarations; allDeclarations.values .where((decl) => decl is VariableMirror) .where( - (decl) => decl.metadata.any((im) => im.reflectee is _HTTPParameter)) + (decl) => decl.metadata.any((im) => im.reflectee is HTTPParameter)) .forEach((decl) { - _HTTPControllerCachedParameter param; + HTTPControllerCachedParameter param; var isRequired = allDeclarations[decl.simpleName] .metadata .any((im) => im.reflectee is HTTPRequiredParameter); if (isRequired) { hasControllerRequiredParameter = true; - param = new _HTTPControllerCachedParameter(decl, isRequired: true); + param = new HTTPControllerCachedParameter(decl, isRequired: true); } else { - param = new _HTTPControllerCachedParameter(decl, isRequired: false); + param = new HTTPControllerCachedParameter(decl, isRequired: false); } propertyCache[param.symbol] = param; @@ -72,17 +75,17 @@ class _HTTPControllerCache { allDeclarations.values .where((decl) => decl is MethodMirror) .where((decl) => decl.metadata.any((im) => im.reflectee is HTTPMethod)) - .map((decl) => new _HTTPControllerCachedMethod(decl)) - .forEach((_HTTPControllerCachedMethod method) { - var key = _HTTPControllerCachedMethod.generateRequestMethodKey( + .map((decl) => new HTTPControllerCachedMethod(decl)) + .forEach((HTTPControllerCachedMethod method) { + var key = HTTPControllerCachedMethod.generateRequestMethodKey( method.httpMethod.method, method.pathParameters.length); methodCache[key] = method; }); } - Map methodCache = {}; - Map propertyCache = {}; + Map methodCache = {}; + Map propertyCache = {}; bool hasControllerRequiredParameter = false; bool hasRequiredParametersForMethod(MethodMirror mm) { @@ -92,8 +95,8 @@ class _HTTPControllerCache { if (mm is MethodMirror && mm.metadata.any((im) => im.reflectee is HTTPMethod)) { - _HTTPControllerCachedMethod method = new _HTTPControllerCachedMethod(mm); - var key = _HTTPControllerCachedMethod.generateRequestMethodKey( + HTTPControllerCachedMethod method = new HTTPControllerCachedMethod(mm); + var key = HTTPControllerCachedMethod.generateRequestMethodKey( method.httpMethod.method, method.pathParameters.length); return methodCache[key] @@ -104,8 +107,8 @@ class _HTTPControllerCache { return false; } - _HTTPControllerCachedMethod mapperForRequest(Request req) { - var key = _HTTPControllerCachedMethod.generateRequestMethodKey( + HTTPControllerCachedMethod mapperForRequest(Request req) { + var key = HTTPControllerCachedMethod.generateRequestMethodKey( req.innerRequest.method, req.path.orderedVariableNames.length); return methodCache[key]; @@ -113,7 +116,7 @@ class _HTTPControllerCache { Map propertiesFromRequest( HttpHeaders headers, Map> queryParameters) { - return _parseParametersFromRequest(propertyCache, headers, queryParameters); + return parseParametersFromRequest(propertyCache, headers, queryParameters); } List allowedMethodsForArity(int arity) { @@ -124,66 +127,66 @@ class _HTTPControllerCache { } } -class _HTTPControllerCachedMethod { +class HTTPControllerCachedMethod { static String generateRequestMethodKey(String httpMethod, int arity) { return "${httpMethod.toLowerCase()}/$arity"; } - _HTTPControllerCachedMethod(MethodMirror mirror) { + HTTPControllerCachedMethod(MethodMirror mirror) { httpMethod = mirror.metadata.firstWhere((m) => m.reflectee is HTTPMethod).reflectee; methodSymbol = mirror.simpleName; positionalParameters = mirror.parameters .where((pm) => !pm.isOptional) - .map((pm) => new _HTTPControllerCachedParameter(pm, isRequired: true)) + .map((pm) => new HTTPControllerCachedParameter(pm, isRequired: true)) .toList(); optionalParameters = new Map.fromIterable( mirror.parameters.where((pm) => pm.isOptional).map( - (pm) => new _HTTPControllerCachedParameter(pm, isRequired: false)), - key: (_HTTPControllerCachedParameter p) => p.symbol, + (pm) => new HTTPControllerCachedParameter(pm, isRequired: false)), + key: (HTTPControllerCachedParameter p) => p.symbol, value: (p) => p); } Symbol methodSymbol; HTTPMethod httpMethod; - List<_HTTPControllerCachedParameter> positionalParameters = []; - Map optionalParameters = {}; - List<_HTTPControllerCachedParameter> get pathParameters => + List positionalParameters = []; + Map optionalParameters = {}; + List get pathParameters => positionalParameters.where((p) => p.httpParameter is HTTPPath).toList(); List positionalParametersFromRequest( Request req, Map> queryParameters) { return positionalParameters.map((param) { if (param.httpParameter is HTTPPath) { - return _convertParameterWithMirror( + return convertParameterWithMirror( req.path.variables[param.name], param.typeMirror); } else if (param.httpParameter is HTTPQuery) { - return _convertParameterListWithMirror( + return convertParameterListWithMirror( queryParameters[param.name], param.typeMirror) ?? - new _HTTPControllerMissingParameter( - _HTTPControllerMissingParameterType.query, param.name); + new HTTPControllerMissingParameter( + HTTPControllerMissingParameterType.query, param.name); } else if (param.httpParameter is HTTPHeader) { - return _convertParameterListWithMirror( + return convertParameterListWithMirror( req.innerRequest.headers[param.name], param.typeMirror) ?? - new _HTTPControllerMissingParameter( - _HTTPControllerMissingParameterType.header, param.name); + new HTTPControllerMissingParameter( + HTTPControllerMissingParameterType.header, param.name); } }).toList(); } Map optionalParametersFromRequest( HttpHeaders headers, Map> queryParameters) { - return _parseParametersFromRequest( + return parseParametersFromRequest( optionalParameters, headers, queryParameters); } } -class _HTTPControllerCachedParameter { - _HTTPControllerCachedParameter(VariableMirror mirror, +class HTTPControllerCachedParameter { + HTTPControllerCachedParameter(VariableMirror mirror, {this.isRequired: false}) { symbol = mirror.simpleName; httpParameter = mirror.metadata - .firstWhere((im) => im.reflectee is _HTTPParameter, orElse: () => null) + .firstWhere((im) => im.reflectee is HTTPParameter, orElse: () => null) ?.reflectee; typeMirror = mirror.type; } @@ -191,21 +194,21 @@ class _HTTPControllerCachedParameter { Symbol symbol; String get name => httpParameter.externalName; TypeMirror typeMirror; - _HTTPParameter httpParameter; + HTTPParameter httpParameter; bool isRequired; } -enum _HTTPControllerMissingParameterType { header, query } +enum HTTPControllerMissingParameterType { header, query } -class _HTTPControllerMissingParameter { - _HTTPControllerMissingParameter(this.type, this.externalName); +class HTTPControllerMissingParameter { + HTTPControllerMissingParameter(this.type, this.externalName); - _HTTPControllerMissingParameterType type; + HTTPControllerMissingParameterType type; String externalName; } -Map _parseParametersFromRequest( - Map mappings, +Map parseParametersFromRequest( + Map mappings, HttpHeaders headers, Map> queryParameters) { return mappings.keys.fold({}, (m, sym) { @@ -214,17 +217,17 @@ Map _parseParametersFromRequest( var paramType = null; if (mapper.httpParameter is HTTPQuery) { - paramType = _HTTPControllerMissingParameterType.query; + paramType = HTTPControllerMissingParameterType.query; value = queryParameters[mapper.httpParameter.externalName]; } else if (mapper.httpParameter is HTTPHeader) { - paramType = _HTTPControllerMissingParameterType.header; + paramType = HTTPControllerMissingParameterType.header; value = headers[mapper.httpParameter.externalName]; } if (value != null) { - m[sym] = _convertParameterListWithMirror(value, mapper.typeMirror); + m[sym] = convertParameterListWithMirror(value, mapper.typeMirror); } else if (mapper.isRequired) { - m[sym] = new _HTTPControllerMissingParameter( + m[sym] = new HTTPControllerMissingParameter( paramType, mapper.httpParameter.externalName); } @@ -232,7 +235,7 @@ Map _parseParametersFromRequest( }); } -dynamic _convertParameterListWithMirror( +dynamic convertParameterListWithMirror( List parameterValues, TypeMirror typeMirror) { if (parameterValues == null) { return null; @@ -241,14 +244,19 @@ dynamic _convertParameterListWithMirror( if (typeMirror.isSubtypeOf(reflectType(List))) { return parameterValues .map((str) => - _convertParameterWithMirror(str, typeMirror.typeArguments.first)) + convertParameterWithMirror(str, typeMirror.typeArguments.first)) .toList(); } else { - return _convertParameterWithMirror(parameterValues.first, typeMirror); + if (parameterValues.length > 1) { + throw new InternalControllerException( + "Duplicate value for parameter", HttpStatus.BAD_REQUEST, + responseMessage: "Duplicate parameter for non-List parameter type"); + } + return convertParameterWithMirror(parameterValues.first, typeMirror); } } -dynamic _convertParameterWithMirror( +dynamic convertParameterWithMirror( String parameterValue, TypeMirror typeMirror) { if (parameterValue == null) { return null; @@ -270,7 +278,7 @@ dynamic _convertParameterWithMirror( typeMirror.invoke(parseDecl.simpleName, [parameterValue]); return reflValue.reflectee; } catch (e) { - throw new _InternalControllerException( + throw new InternalControllerException( "Invalid value for parameter type", HttpStatus.BAD_REQUEST, responseMessage: "URI parameter is wrong type"); } @@ -278,7 +286,7 @@ dynamic _convertParameterWithMirror( } // If we get here, then it wasn't a string and couldn't be parsed, and we should throw? - throw new _InternalControllerException( + throw new InternalControllerException( "Invalid path parameter type, types must be String or implement parse", HttpStatus.INTERNAL_SERVER_ERROR, responseMessage: "URI parameter is wrong type"); diff --git a/lib/http/http_response_exception.dart b/lib/src/http/http_response_exception.dart similarity index 77% rename from lib/http/http_response_exception.dart rename to lib/src/http/http_response_exception.dart index 4bedffa10..238bf38f9 100644 --- a/lib/http/http_response_exception.dart +++ b/lib/src/http/http_response_exception.dart @@ -1,4 +1,8 @@ -part of aqueduct; +import 'dart:io'; + +import 'response.dart'; +import 'request_controller.dart'; +import 'request.dart'; /// An exception for early-exiting a [RequestController] to respond to a request. /// @@ -18,6 +22,6 @@ class HTTPResponseException implements Exception { final int statusCode; /// A [Response] object derived from this exception. - Response get response => new Response(statusCode, - {HttpHeaders.CONTENT_TYPE: ContentType.JSON}, {"error": message}); + Response get response => new Response(statusCode, null, {"error": message}) + ..contentType = ContentType.JSON; } diff --git a/lib/http/query_controller.dart b/lib/src/http/query_controller.dart similarity index 97% rename from lib/http/query_controller.dart rename to lib/src/http/query_controller.dart index eabe8699a..e0c6fb146 100644 --- a/lib/http/query_controller.dart +++ b/lib/src/http/query_controller.dart @@ -1,4 +1,8 @@ -part of aqueduct; +import 'dart:async'; +import 'dart:mirrors'; + +import '../db/db.dart'; +import 'http.dart'; /// A partial class for implementing an [HTTPController] that has a few conveniences /// for executing [Query]s. diff --git a/lib/http/request.dart b/lib/src/http/request.dart similarity index 86% rename from lib/http/request.dart rename to lib/src/http/request.dart index 9c5aa9fd9..30c3a8a48 100644 --- a/lib/http/request.dart +++ b/lib/src/http/request.dart @@ -1,4 +1,10 @@ -part of aqueduct; +import 'dart:async'; +import 'dart:io'; + +import 'package:logging/logging.dart'; + +import '../auth/auth.dart'; +import 'http.dart'; /// A single HTTP request. /// @@ -135,51 +141,25 @@ class Request implements RequestControllerEvent { void respond(Response responseObject) { respondDate = new DateTime.now().toUtc(); + var encodedBody = responseObject.encodedBody; + response.statusCode = responseObject.statusCode; if (responseObject.headers != null) { responseObject.headers.forEach((k, v) { - if (v is ContentType) { - response.headers.add(HttpHeaders.CONTENT_TYPE, v.toString()); - } else { - response.headers.add(k, v); - } + response.headers.add(k, v); }); } - if (responseObject.body != null) { - _encodeBody(responseObject); + if (encodedBody != null) { + response.headers + .add(HttpHeaders.CONTENT_TYPE, responseObject.contentType.toString()); + response.write(encodedBody); } response.close(); } - void _encodeBody(Response respObj) { - var contentTypeValue = respObj.headers["Content-Type"]; - if (contentTypeValue == null) { - contentTypeValue = ContentType.JSON; - response.headers.contentType = ContentType.JSON; - } else if (contentTypeValue is String) { - contentTypeValue = ContentType.parse(contentTypeValue); - } - - ContentType contentType = contentTypeValue; - var topLevel = Response._encoders[contentType.primaryType]; - if (topLevel == null) { - throw new RequestException( - "No encoder for $contentTypeValue, add with Request.addEncoder()."); - } - - var encoder = topLevel[contentType.subType]; - if (encoder == null) { - throw new RequestException( - "No encoder for $contentTypeValue, add with Request.addEncoder()."); - } - - var encodedValue = encoder(respObj.body); - response.write(encodedValue); - } - String toString() { return "${innerRequest.method} ${this.innerRequest.uri} (${this.receivedDate.millisecondsSinceEpoch})"; } diff --git a/lib/http/request_controller.dart b/lib/src/http/request_controller.dart similarity index 76% rename from lib/http/request_controller.dart rename to lib/src/http/request_controller.dart index da7506b66..08a69822a 100644 --- a/lib/http/request_controller.dart +++ b/lib/src/http/request_controller.dart @@ -1,4 +1,11 @@ -part of aqueduct; +import 'dart:async'; +import 'dart:mirrors'; +import 'dart:io'; + +import 'package:logging/logging.dart'; + +import 'http.dart'; +import '../db/db.dart'; /// The unifying protocol for [Request] and [Response] classes. /// @@ -50,7 +57,7 @@ class RequestController extends Object with APIDocumentable { RequestController pipe(RequestController n) { var typeMirror = reflect(n).type; if (_requestControllerTypeRequiresInstantion(typeMirror)) { - throw new ApplicationSupervisorException( + throw new RequestControllerException( "RequestController subclass ${typeMirror.reflectedType} instances cannot be reused. Rewrite as .generate(() => new ${typeMirror.reflectedType}())"); } this.nextController = n; @@ -133,16 +140,19 @@ class RequestController extends Object with APIDocumentable { if (policy != null) { if (!policy.validatePreflightRequest(req.innerRequest)) { - req.respond(new Response.forbidden()); + var response = new Response.forbidden(); + _sendResponse(req, response); logger.info(req.toDebugString(includeHeaders: true)); } else { - req.respond(policy.preflightResponse(req)); + var response = policy.preflightResponse(req); + _sendResponse(req, response); logger.info(req.toDebugString()); } return; } else { // If we don't have a policy, then a preflight request makes no sense. - req.respond(new Response.forbidden()); + var response = new Response.forbidden(); + _sendResponse(req, response); logger.info(req.toDebugString(includeHeaders: true)); return; } @@ -153,13 +163,14 @@ class RequestController extends Object with APIDocumentable { if (result is Request && nextController != null) { nextController.receive(req); } else if (result is Response) { - _applyCORSHeadersIfNecessary(req, result); - req.respond(result); + _sendResponse(req, result, includeCORSHeaders: true); logger.info(req.toDebugString()); } } catch (any, stacktrace) { - _handleError(req, any, stacktrace); + try { + _handleError(req, any, stacktrace); + } catch (_) {} } } @@ -179,62 +190,72 @@ class RequestController extends Object with APIDocumentable { return req; } + /// Executed prior to [Response] being sent. + /// + /// This method is used to post-process [response] just before it is sent. By default, does nothing. + /// The [response] may be altered prior to being sent. This method will be executed for all requests, + /// including server errors. + void willSendResponse(Response response) {} + + void _sendResponse(Request request, Response response, {bool includeCORSHeaders: false}) { + if (includeCORSHeaders) { + applyCORSHeadersIfNecessary(request, response); + } + willSendResponse(response); + request.respond(response); + } + void _handleError(Request request, dynamic caughtValue, StackTrace trace) { - try { - if (caughtValue is HTTPResponseException) { - var response = caughtValue.response; - _applyCORSHeadersIfNecessary(request, response); - request.respond(response); - - logger.info( - "${request.toDebugString(includeHeaders: true, includeBody: true)}"); - } else if (caughtValue is QueryException && - caughtValue.event != QueryExceptionEvent.internalFailure) { - // Note that if the event is an internal failure, this code is skipped and the 500 handler is executed. - var statusCode = 500; - switch (caughtValue.event) { - case QueryExceptionEvent.requestFailure: - statusCode = 400; - break; - case QueryExceptionEvent.internalFailure: - statusCode = 500; - break; - case QueryExceptionEvent.connectionFailure: - statusCode = 503; - break; - case QueryExceptionEvent.conflict: - statusCode = 409; - break; - } + if (caughtValue is HTTPResponseException) { + var response = caughtValue.response; + _sendResponse(request, response, includeCORSHeaders: true); + + logger.info( + "${request.toDebugString(includeHeaders: true, includeBody: true)}"); + } else if (caughtValue is QueryException && + caughtValue.event != QueryExceptionEvent.internalFailure) { + // Note that if the event is an internal failure, this code is skipped and the 500 handler is executed. + var statusCode = 500; + switch (caughtValue.event) { + case QueryExceptionEvent.requestFailure: + statusCode = 400; + break; + case QueryExceptionEvent.internalFailure: + statusCode = 500; + break; + case QueryExceptionEvent.connectionFailure: + statusCode = 503; + break; + case QueryExceptionEvent.conflict: + statusCode = 409; + break; + } - var response = - new Response(statusCode, null, {"error": caughtValue.toString()}); - _applyCORSHeadersIfNecessary(request, response); - request.respond(response); - - logger.info( - "${request.toDebugString(includeHeaders: true, includeBody: true)}"); - } else { - var body = null; - if (includeErrorDetailsInServerErrorResponses) { - body = { - "error": "${this.runtimeType}: $caughtValue.", - "stacktrace": trace.toString() - }; - } + var response = + new Response(statusCode, null, {"error": caughtValue.toString()}); + _sendResponse(request, response, includeCORSHeaders: true); + + logger.info( + "${request.toDebugString(includeHeaders: true, includeBody: true)}"); + } else { + var body = null; + if (includeErrorDetailsInServerErrorResponses) { + body = { + "error": "${this.runtimeType}: $caughtValue.", + "stacktrace": trace.toString() + }; + } - var response = new Response.serverError( - headers: {HttpHeaders.CONTENT_TYPE: ContentType.JSON}, body: body); + var response = new Response.serverError(body: body) + ..contentType = ContentType.JSON; - _applyCORSHeadersIfNecessary(request, response); - request.respond(response); + _sendResponse(request, response, includeCORSHeaders: true); - logger.severe( - "${request.toDebugString(includeHeaders: true, includeBody: true)}", - caughtValue, - trace); - } - } catch (_) {} + logger.severe( + "${request.toDebugString(includeHeaders: true, includeBody: true)}", + caughtValue, + trace); + } } RequestController _lastRequestController() { @@ -245,7 +266,7 @@ class RequestController extends Object with APIDocumentable { return controller; } - void _applyCORSHeadersIfNecessary(Request req, Response resp) { + void applyCORSHeadersIfNecessary(Request req, Response resp) { if (req.isCORSRequest && !req.isPreflightRequest) { var lastPolicyController = _lastRequestController(); var p = lastPolicyController.policy; @@ -320,3 +341,14 @@ class _RequestControllerGenerator extends RequestController { PackagePathResolver resolver) => instantiate().documentSecuritySchemes(resolver); } + +/// Thrown when [RequestController] throws an exception. +/// +/// +class RequestControllerException { + RequestControllerException(this.message); + + String message; + + String toString() => "RequestControllerException: $message"; +} diff --git a/lib/http/request_path.dart b/lib/src/http/request_path.dart similarity index 82% rename from lib/http/request_path.dart rename to lib/src/http/request_path.dart index 46856a4c1..97e1b6c7a 100644 --- a/lib/http/request_path.dart +++ b/lib/src/http/request_path.dart @@ -1,4 +1,5 @@ -part of aqueduct; +import 'http.dart'; +import 'route_node.dart'; /// The HTTP request path decomposed into variables and segments based on a [RouteSpecification]. /// @@ -164,98 +165,6 @@ class RouteSpecification extends Object with APIDocumentable { String toString() => segments.join("/"); } -/// Used internally. -class RouteSegment { - RouteSegment(String segment) { - if (segment == "*") { - isRemainingMatcher = true; - return; - } - - var regexIndex = segment.indexOf("("); - if (regexIndex != -1) { - var regexText = segment.substring(regexIndex + 1, segment.length - 1); - matcher = new RegExp(regexText); - - segment = segment.substring(0, regexIndex); - } - - if (segment.startsWith(":")) { - variableName = segment.substring(1, segment.length); - } else if (regexIndex == -1) { - literal = segment; - } - } - - RouteSegment.direct( - {String literal: null, - String variableName: null, - String expression: null, - bool matchesAnything: false}) { - this.literal = literal; - this.variableName = variableName; - this.isRemainingMatcher = matchesAnything; - if (expression != null) { - this.matcher = new RegExp(expression); - } - } - - String literal; - String variableName; - RegExp matcher; - - bool get isLiteralMatcher => - !isRemainingMatcher && !isVariable && !hasRegularExpression; - bool get hasRegularExpression => matcher != null; - bool get isVariable => variableName != null; - bool isRemainingMatcher = false; - - bool matches(String pathSegment) { - if (isLiteralMatcher) { - return pathSegment == literal; - } - - if (hasRegularExpression) { - if (matcher.firstMatch(pathSegment) == null) { - return false; - } - } - - if (isVariable) { - return true; - } - - return false; - } - - operator ==(dynamic other) { - if (other is! RouteSegment) { - return false; - } - - return literal == other.literal && - variableName == other.variableName && - isRemainingMatcher == other.isRemainingMatcher && - matcher?.pattern == other.matcher?.pattern; - } - - String toString() { - if (isLiteralMatcher) { - return literal; - } - - if (isVariable) { - return variableName; - } - - if (hasRegularExpression) { - return "(${matcher.pattern})"; - } - - return "*"; - } -} - /// Utility method to take Route syntax into one or more full paths. /// /// This method strips away optionals in the route syntax, yielding an individual path for every combination of the route syntax. diff --git a/lib/http/request_sink.dart b/lib/src/http/request_sink.dart similarity index 97% rename from lib/http/request_sink.dart rename to lib/src/http/request_sink.dart index 84b8f87a8..8e1f0c349 100644 --- a/lib/http/request_sink.dart +++ b/lib/src/http/request_sink.dart @@ -1,4 +1,11 @@ -part of aqueduct; +import 'dart:io'; +import 'dart:async'; + +import 'request.dart'; +import 'request_controller.dart'; +import 'documentable.dart'; +import 'router.dart'; +import '../application/application.dart'; /// Instances of this class are responsible for setting up routing and resources used by an [Application]. /// diff --git a/lib/http/resource_controller.dart b/lib/src/http/resource_controller.dart similarity index 99% rename from lib/http/resource_controller.dart rename to lib/src/http/resource_controller.dart index f497a51e0..e577baf8a 100644 --- a/lib/http/resource_controller.dart +++ b/lib/src/http/resource_controller.dart @@ -1,4 +1,8 @@ -part of aqueduct; +import 'dart:async'; +import 'dart:io'; + +import '../db/db.dart'; +import 'http.dart'; /// A [RequestController] for performing CRUD operations on [ManagedObject] instances. /// diff --git a/lib/http/response.dart b/lib/src/http/response.dart similarity index 62% rename from lib/http/response.dart rename to lib/src/http/response.dart index 946afc008..39b48c80f 100644 --- a/lib/http/response.dart +++ b/lib/src/http/response.dart @@ -1,14 +1,30 @@ -part of aqueduct; +import 'dart:io'; +import 'dart:convert'; +import 'http.dart'; /// Represents the information in an HTTP response. /// /// This object can be used to write an HTTP response and contains conveniences /// for creating these objects. class Response implements RequestControllerEvent { + /// The default value of a [contentType]. + /// + /// If no [contentType] is set for an instance, this is the value used. By default, this value is + /// [ContentType.JSON]. + static ContentType defaultContentType = ContentType.JSON; + /// Adds an HTTP Response Body encoder to list of available encoders for all [Request]s. /// - /// By default, 'application/json' and 'text/plain' are implemented. If you wish to add another encoder - /// to your application, use this method. The [encoder] must take one argument of any type, and return a value + /// When the [contentType] of an instance is set, an encoder function is applied to the data. This method + /// adds an encoder function for [type]. + /// + /// By default, 'application/json' and 'text/*' are available. A [Response] with "application/json" [contentType] + /// will be encoded by invoking [JSON.decode] on the instance's [body]. The default encoder for [ContentType]s whose primary type is "text" will invoke [toString] + /// on the instance's [body]. + /// + /// [type] can have a '*' [ContentType.subType] that matches all subtypes for a primary type. + /// + /// An [encoder] must take one argument of any type, and return a value /// that will become the HTTP response body. /// /// The return value is written to the response with [IOSink.write] and so it must either be a [String] or its [toString] @@ -27,7 +43,7 @@ class Response implements RequestControllerEvent { "application": { "json": (v) => JSON.encode(v), }, - "text": {"plain": (Object v) => v.toString()} + "text": {"*": (Object v) => v.toString()} }; /// An object representing the body of the [Response], which will be encoded when used to [Request.respond]. @@ -59,14 +75,57 @@ class Response implements RequestControllerEvent { dynamic _body; + /// Returns the encoded [body] according to [contentType]. + /// + /// If there is no [body] present, this property is null. This property will use the encoders available through [addEncoder]. If + /// no encoder is found, [toString] is called on the body. + dynamic get encodedBody { + if (_body == null) { + return null; + } + + var encoder = null; + var topLevel = _encoders[contentType.primaryType]; + if (topLevel != null) { + encoder = topLevel[contentType.subType] ?? topLevel["*"]; + } + + if (encoder == null) { + throw new HTTPResponseException( + 500, "Could not encode body as ${contentType.toString()}."); + } + + return encoder(_body); + } + /// Map of headers to send in this response. /// - /// Where the key is the Header name and value is the Header value. + /// Where the key is the Header name and value is the Header value. Values may be any type and by default will have [toString] invoked + /// on them. For [DateTime] values, the value will be converted into an HTTP date format. For [List] values, each value will be + /// have [toString] invoked on it and the resulting outputs will be joined together with the "," character. + /// + /// Adding a Content-Type header through this property has no effect. Use [contentType] instead. Map headers; /// The HTTP status code of this response. int statusCode; + /// The content type of the body of this response. + /// + /// Defaults to [defaultContentType]. This response's body will be encoded according to this value. + /// The Content-Type header of the HTTP response will always be set according to this value. + ContentType get contentType => _contentType ?? defaultContentType; + void set contentType(ContentType t) { + _contentType = t; + } + + ContentType _contentType; + + /// Whether or nor this instance has explicitly has its [contentType] property. + /// + /// This value indicates whether or not [contentType] has been set, or is still using its default value. + bool get hasExplicitlySetContentType => _contentType != null; + /// The default constructor. /// /// There exist convenience constructors for common response status codes diff --git a/lib/http/route_node.dart b/lib/src/http/route_node.dart similarity index 55% rename from lib/http/route_node.dart rename to lib/src/http/route_node.dart index dc52b26d3..c9fcacb16 100644 --- a/lib/http/route_node.dart +++ b/lib/src/http/route_node.dart @@ -1,7 +1,76 @@ -part of aqueduct; +import 'http.dart'; -class _RouteNode { - _RouteNode(List specs, +class RouteSegment { + RouteSegment(String segment) { + if (segment == "*") { + isRemainingMatcher = true; + return; + } + + var regexIndex = segment.indexOf("("); + if (regexIndex != -1) { + var regexText = segment.substring(regexIndex + 1, segment.length - 1); + matcher = new RegExp(regexText); + + segment = segment.substring(0, regexIndex); + } + + if (segment.startsWith(":")) { + variableName = segment.substring(1, segment.length); + } else if (regexIndex == -1) { + literal = segment; + } + } + + RouteSegment.direct( + {String literal: null, + String variableName: null, + String expression: null, + bool matchesAnything: false}) { + this.literal = literal; + this.variableName = variableName; + this.isRemainingMatcher = matchesAnything; + if (expression != null) { + this.matcher = new RegExp(expression); + } + } + + String literal; + String variableName; + RegExp matcher; + + bool get isLiteralMatcher => + !isRemainingMatcher && !isVariable && !hasRegularExpression; + bool get hasRegularExpression => matcher != null; + bool get isVariable => variableName != null; + bool isRemainingMatcher = false; + + bool operator ==(dynamic other) { + return literal == other.literal && + variableName == other.variableName && + isRemainingMatcher == other.isRemainingMatcher && + matcher?.pattern == other.matcher?.pattern; + } + + String toString() { + if (isLiteralMatcher) { + return literal; + } + + if (isVariable) { + return variableName; + } + + if (hasRegularExpression) { + return "(${matcher.pattern})"; + } + + return "*"; + } +} + +class RouteNode { + RouteNode(List specs, {int level: 0, RegExp matcher: null}) { patternMatcher = matcher; @@ -23,9 +92,8 @@ class _RouteNode { var literalMatcher = (RouteSpecification spec) => spec.segments[level].literal == segmentLiteral; - literalChildren[segmentLiteral] = new _RouteNode( - specs.where(literalMatcher).toList(), - level: level + 1); + literalChildren[segmentLiteral] = + new RouteNode(specs.where(literalMatcher).toList(), level: level + 1); specs.removeWhere(literalMatcher); }); @@ -33,7 +101,7 @@ class _RouteNode { (rps) => rps.segments[level].isRemainingMatcher, orElse: () => null); if (anyMatcher != null) { - anyMatcherChildNode = new _RouteNode.withSpecification(anyMatcher); + anyMatcherChildNode = new RouteNode.withSpecification(anyMatcher); specs.removeWhere((rps) => rps.segments[level].isRemainingMatcher); } @@ -49,23 +117,23 @@ class _RouteNode { "Cannot disambiguate from the following routes, as one of them will match anything: $matchingSpecs"); } - return new _RouteNode(matchingSpecs, + return new RouteNode(matchingSpecs, level: level + 1, matcher: matchingSpecs.first.segments[level].matcher); }).toList(); } - _RouteNode.withSpecification(this.specification); + RouteNode.withSpecification(this.specification); bool matchingAnything = false; RegExp patternMatcher; RequestController get controller => specification?.controller; RouteSpecification specification; - List<_RouteNode> patternMatchChildren = []; - Map literalChildren = {}; - _RouteNode anyMatcherChildNode; + List patternMatchChildren = []; + Map literalChildren = {}; + RouteNode anyMatcherChildNode; - _RouteNode nodeForPathSegments(List requestSegments) { + RouteNode nodeForPathSegments(List requestSegments) { if (requestSegments.isEmpty) { return this; } diff --git a/lib/http/router.dart b/lib/src/http/router.dart similarity index 96% rename from lib/http/router.dart rename to lib/src/http/router.dart index 471b35efd..c86d62e38 100644 --- a/lib/http/router.dart +++ b/lib/src/http/router.dart @@ -1,4 +1,7 @@ -part of aqueduct; +import 'dart:async'; + +import 'http.dart'; +import 'route_node.dart'; /// A router to split requests based on their URI path. /// @@ -23,7 +26,7 @@ class Router extends RequestController { } List _routeControllers = []; - _RouteNode _rootRouteNode; + RouteNode _rootRouteNode; /// A string to be prepended to the beginning of every route this [Router] manages. /// @@ -81,7 +84,7 @@ class Router extends RequestController { /// you must call this method after all routes have been added to build a tree of routes for optimized route finding. void finalize() { _rootRouteNode = - new _RouteNode(_routeControllers.expand((rh) => rh.patterns).toList()); + new RouteNode(_routeControllers.expand((rh) => rh.patterns).toList()); } /// Routers override this method to throw an exception. Use [route] instead. @@ -146,7 +149,7 @@ class Router extends RequestController { void _handleUnhandledRequest(Request req) { var response = new Response.notFound(); - _applyCORSHeadersIfNecessary(req, response); + applyCORSHeadersIfNecessary(req, response); req.respond(response); logger.info("${req.toDebugString()}"); } @@ -169,10 +172,12 @@ class RouteController extends RequestController { final List patterns; } +/// Thrown when a [Router] encounters an exception. class RouterException implements Exception { - final String message; RouterException(this.message); + final String message; + String toString() { return "RouterException: $message"; } diff --git a/lib/http/serializable.dart b/lib/src/http/serializable.dart similarity index 96% rename from lib/http/serializable.dart rename to lib/src/http/serializable.dart index e5c6d044d..df54bfe88 100644 --- a/lib/http/serializable.dart +++ b/lib/src/http/serializable.dart @@ -1,5 +1,3 @@ -part of aqueduct; - /// Interface for serializable instances to be returned as the HTTP response body. /// /// Implementers of this interface may be the 'body' argument in a [Response]. diff --git a/lib/utilities/mirror_helpers.dart b/lib/src/utilities/mirror_helpers.dart similarity index 90% rename from lib/utilities/mirror_helpers.dart rename to lib/src/utilities/mirror_helpers.dart index 7bbf5a5ff..afc6a6012 100644 --- a/lib/utilities/mirror_helpers.dart +++ b/lib/src/utilities/mirror_helpers.dart @@ -1,6 +1,8 @@ import 'dart:mirrors'; -import '../aqueduct.dart'; +import '../db/managed/object.dart'; +import '../db/managed/set.dart'; +import '../db/managed/attributes.dart'; bool doesVariableMirrorRepresentRelationship(VariableMirror mirror) { var modelMirror = reflectType(ManagedObject); diff --git a/lib/utilities/mock_server.dart b/lib/src/utilities/mock_server.dart similarity index 96% rename from lib/utilities/mock_server.dart rename to lib/src/utilities/mock_server.dart index fcf80358e..6d56434e9 100644 --- a/lib/utilities/mock_server.dart +++ b/lib/src/utilities/mock_server.dart @@ -1,4 +1,9 @@ -part of aqueduct; +import 'dart:async'; +import 'dart:io'; +import 'dart:convert'; + +import '../http/request.dart'; +import '../http/response.dart'; /// This class is used as a utility for testing. /// @@ -112,14 +117,14 @@ class MockHTTPRequest { /// await nestMockServer.close(); /// }); class MockHTTPServer extends MockServer { - static final int _mockConnectionFailureStatusCode = -1; + static const int _mockConnectionFailureStatusCode = -1; /// Used to simulate a failed request. /// /// Pass this value to [queueResponse] to simulate a 'no response' failure on the next request made to this instance. /// The next request made to this instance will simply not be responded to. /// This is useful in debugging for determining how your code responds to not being able to reach a third party server. - static final Response mockConnectionFailureResponse = + static Response mockConnectionFailureResponse = new Response(_mockConnectionFailureStatusCode, {}, null); MockHTTPServer(this.port) : super(); diff --git a/lib/utilities/pbkdf2.dart b/lib/src/utilities/pbkdf2.dart similarity index 97% rename from lib/utilities/pbkdf2.dart rename to lib/src/utilities/pbkdf2.dart index 35956b7c6..288a6e243 100644 --- a/lib/utilities/pbkdf2.dart +++ b/lib/src/utilities/pbkdf2.dart @@ -1,5 +1,3 @@ -part of aqueduct; - /* Based on implementation found here: https://github.com/jamesots/pbkdf2, which contains the following license: Copyright 2014 James Ots @@ -17,6 +15,10 @@ part of aqueduct; limitations under the License. */ +import 'dart:math'; + +import 'package:crypto/crypto.dart'; + /// Instances of this type perform one-way cryptographic hashing using the PBKDF2 algorithm. class PBKDF2 { Hash hashAlgorithm; diff --git a/lib/utilities/source_generator.dart b/lib/src/utilities/source_generator.dart similarity index 89% rename from lib/utilities/source_generator.dart rename to lib/src/utilities/source_generator.dart index 3ba29a706..9cbc62bab 100644 --- a/lib/utilities/source_generator.dart +++ b/lib/src/utilities/source_generator.dart @@ -1,14 +1,18 @@ -part of aqueduct; +import 'dart:async'; +import 'dart:io'; +import 'dart:isolate'; +import 'token_generator.dart'; +import 'dart:mirrors'; -class _SourceGenerator { +class SourceGenerator { static String generate(Function closure, {List imports: const [], String additionalContents}) { - var gen = new _SourceGenerator(closure, + var gen = new SourceGenerator(closure, imports: imports, additionalContents: additionalContents); return gen.source; } - _SourceGenerator(this.closure, + SourceGenerator(this.closure, {this.imports: const [], this.additionalContents}); Function closure; @@ -45,11 +49,11 @@ class _SourceGenerator { } } -class _IsolateExecutor { - _IsolateExecutor(this.generator, this.arguments, +class IsolateExecutor { + IsolateExecutor(this.generator, this.arguments, {this.message, this.packageConfigURI}); - _SourceGenerator generator; + SourceGenerator generator; Map message; List arguments; Uri packageConfigURI; diff --git a/lib/utilities/test_client.dart b/lib/src/utilities/test_client.dart similarity index 98% rename from lib/utilities/test_client.dart rename to lib/src/utilities/test_client.dart index 1486f1f27..2bae8866f 100644 --- a/lib/utilities/test_client.dart +++ b/lib/src/utilities/test_client.dart @@ -1,4 +1,8 @@ -part of aqueduct; +import 'dart:io'; +import 'dart:async'; +import 'dart:convert'; +import '../application/application.dart'; +import '../application/application_configuration.dart'; /// Instances of this class are used during testing to make testing an HTTP server more convenient. /// diff --git a/lib/utilities/test_matchers.dart b/lib/src/utilities/test_matchers.dart similarity index 97% rename from lib/utilities/test_matchers.dart rename to lib/src/utilities/test_matchers.dart index 38a530ed4..1bdc90347 100644 --- a/lib/utilities/test_matchers.dart +++ b/lib/src/utilities/test_matchers.dart @@ -1,4 +1,5 @@ -part of aqueduct; +import 'package:matcher/matcher.dart'; +import 'test_client.dart'; /// Validates that expected result is a [num]. const Matcher isNumber = const isInstanceOf(); @@ -378,18 +379,17 @@ class _PartialMapMatcher extends Matcher { for (var matchKey in map.keys) { var matchValue = map[matchKey]; var value = item[matchKey]; - - if (value != null && matchValue is _NotPresentMatcher) { - var extra = matchState["extra"]; - if (extra == null) { - extra = []; - matchState["extra"] = extra; + if (matchValue is _NotPresentMatcher) { + if (item.containsKey(matchKey)) { + var extra = matchState["extra"]; + if (extra == null) { + extra = []; + matchState["extra"] = extra; + } + extra = matchKey; + return false; } - extra = matchKey; - return false; - } - - if (value == null) { + } else if (value == null && !matchValue.matches(value, matchState)) { var missing = matchState["missing"]; if (missing == null) { missing = []; diff --git a/lib/utilities/token_generator.dart b/lib/src/utilities/token_generator.dart similarity index 80% rename from lib/utilities/token_generator.dart rename to lib/src/utilities/token_generator.dart index e2fdd79e9..38bdae54c 100644 --- a/lib/utilities/token_generator.dart +++ b/lib/src/utilities/token_generator.dart @@ -1,9 +1,5 @@ import 'dart:math'; -/// A utility to generate a random string of [length]. -/// -/// Will use characters A-Za-z0-9. -/// String randomStringOfLength(int length) { var possibleCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; diff --git a/pubspec.yaml b/pubspec.yaml index 045845346..c391f4437 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: aqueduct -version: 1.0.3 +version: 1.0.4 description: A fully featured server-side framework built for productivity and testability. author: stable|kernel homepage: https://github.com/stablekernel/aqueduct @@ -21,6 +21,7 @@ dependencies: dev_dependencies: test: any http: ">=0.11.3+7 <0.12.0" + coverage: any executables: aqueduct: aqueduct diff --git a/test/auth/auth_code_controller_test.dart b/test/auth/auth_code_controller_test.dart index ec72beee9..7e757b166 100644 --- a/test/auth/auth_code_controller_test.dart +++ b/test/auth/auth_code_controller_test.dart @@ -5,16 +5,18 @@ import '../helpers.dart'; void main() { Application application = new Application(); + ManagedContext ctx = null; TestClient client = new TestClient.onPort(8080) ..clientID = "com.stablekernel.app3" ..clientSecret = "mckinley"; tearDownAll(() async { await application?.stop(); + await ctx?.persistentStore?.close(); }); setUpAll(() async { - await contextWithModels([TestUser, Token, AuthCode]); + ctx = await contextWithModels([TestUser, Token, AuthCode]); await application.start(runOnMainIsolate: true); await createUsers(2); diff --git a/test/auth/auth_controller_test.dart b/test/auth/auth_controller_test.dart index d2ca303b9..9ba1e626d 100644 --- a/test/auth/auth_controller_test.dart +++ b/test/auth/auth_controller_test.dart @@ -10,17 +10,23 @@ void main() { TestClient client = new TestClient.onPort(8080) ..clientID = "com.stablekernel.app1" ..clientSecret = "kilimanjaro"; - - var authenticationServer = - new AuthServer(new AuthDelegate(context)); - var router = new Router(); - router - .route("/auth/token") - .generate(() => new AuthController(authenticationServer)); - router.finalize(); + AuthServer authenticationServer; + Router router; + + setUpAll(() { + authenticationServer = + new AuthServer(new AuthDelegate(context)); + + router = new Router(); + router + .route("/auth/token") + .generate(() => new AuthController(authenticationServer)); + router.finalize(); + }); tearDownAll(() async { await server?.close(force: true); + await context?.persistentStore?.close(); }); setUp(() async { diff --git a/test/base/body_decoder_test.dart b/test/base/body_decoder_test.dart index 60557e7cc..18e1c8b6f 100644 --- a/test/base/body_decoder_test.dart +++ b/test/base/body_decoder_test.dart @@ -7,18 +7,21 @@ import 'dart:convert'; void main() { group("Default decoders", () { HttpServer server; + setUp(() async { server = await HttpServer.bind(InternetAddress.ANY_IP_V4, 8123); }); tearDown(() async { - await server?.close(); + await server?.close(force: true); }); test("application/json decoder works on valid json", () async { - http.post("http://localhost:8123", - headers: {"Content-Type": "application/json"}, - body: JSON.encode({"a": "val"})); + http + .post("http://localhost:8123", + headers: {"Content-Type": "application/json"}, + body: JSON.encode({"a": "val"})) + .catchError((err) => null); var request = await server.first; var body = await HTTPBodyDecoder.decode(request); @@ -27,9 +30,11 @@ void main() { test("application/x-form-url-encoded decoder works on valid form", () async { - http.post("http://localhost:8123", - headers: {"Content-Type": "application/x-www-form-urlencoded"}, - body: "a=b&c=2"); + http + .post("http://localhost:8123", + headers: {"Content-Type": "application/x-www-form-urlencoded"}, + body: "a=b&c=2") + .catchError((err) => null); var request = await server.first; var body = await HTTPBodyDecoder.decode(request); @@ -40,8 +45,11 @@ void main() { }); test("Any text decoder works on text", () async { - http.post("http://localhost:8123", - headers: {"Content-Type": "text/plain"}, body: "foobar"); + http + .post("http://localhost:8123", + headers: {"Content-Type": "text/plain"}, body: "foobar") + .catchError((err) => null); + var request = await server.first; var body = await HTTPBodyDecoder.decode(request); @@ -49,8 +57,12 @@ void main() { }); test("No found decoder for primary type returns binary", () async { - http.post("http://localhost:8123", - headers: {"Content-Type": "notarealthing/nothing"}, body: "foobar"); + http + .post("http://localhost:8123", + headers: {"Content-Type": "notarealthing/nothing"}, + body: "foobar") + .catchError((err) => null); + ; var request = await server.first; var body = await HTTPBodyDecoder.decode(request); @@ -61,7 +73,8 @@ void main() { var req = await new HttpClient() .openUrl("POST", Uri.parse("http://localhost:8123")); req.add("foobar".codeUnits); - req.close(); + req.close().catchError((err) => null); + var request = await server.first; expect(request.headers.contentType, isNull); @@ -71,9 +84,11 @@ void main() { }); test("Decoder that matches primary type but not subtype fails", () async { - http.post("http://localhost:8123", - headers: {"Content-Type": "application/notarealthing"}, - body: "a=b&c=2"); + http + .post("http://localhost:8123", + headers: {"Content-Type": "application/notarealthing"}, + body: "a=b&c=2") + .catchError((err) => null); var request = await server.first; try { @@ -84,8 +99,10 @@ void main() { }); test("Failed decoding throws exception", () async { - http.post("http://localhost:8123", - headers: {"Content-Type": "application/json"}, body: "{a=b&c=2"); + http + .post("http://localhost:8123", + headers: {"Content-Type": "application/json"}, body: "{a=b&c=2") + .catchError((err) => null); var request = await server.first; try { @@ -115,13 +132,15 @@ void main() { }); tearDown(() async { - await server?.close(); + await server?.close(force: true); }); test("Added decoder works when content-type matches", () async { - http.post("http://localhost:8123", - headers: {"Content-Type": "application/thingy"}, - body: "this doesn't matter"); + http + .post("http://localhost:8123", + headers: {"Content-Type": "application/thingy"}, + body: "this doesn't matter") + .catchError((err) => null); var request = await server.first; var body = await HTTPBodyDecoder.decode(request); @@ -129,9 +148,12 @@ void main() { }); test("Added decoder that matches any subtype works", () async { - http.post("http://localhost:8123", - headers: {"Content-Type": "somethingelse/whatever"}, - body: "this doesn't matter"); + http + .post("http://localhost:8123", + headers: {"Content-Type": "somethingelse/whatever"}, + body: "this doesn't matter") + .catchError((err) => null); + ; var request = await server.first; var body = await HTTPBodyDecoder.decode(request); @@ -147,13 +169,16 @@ void main() { }); tearDown(() async { - await server?.close(); + await server?.close(force: true); }); test("Subsequent decodes do not re-process body", () async { - http.post("http://localhost:8123", - headers: {"Content-Type": "application/json"}, - body: JSON.encode({"a": "val"})); + http + .post("http://localhost:8123", + headers: {"Content-Type": "application/json"}, + body: JSON.encode({"a": "val"})) + .catchError((err) => null); + var request = new Request(await server.first); var b1 = await request.decodeBody(); diff --git a/test/base/client_test_test.dart b/test/base/client_test_test.dart index ac042377a..dc965b4eb 100644 --- a/test/base/client_test_test.dart +++ b/test/base/client_test_test.dart @@ -1,9 +1,8 @@ import 'package:aqueduct/aqueduct.dart'; import 'package:test/test.dart'; import 'dart:io'; -import 'dart:async'; -Future main() async { +main() { test("Client can expect array of JSON", () async { TestClient client = new TestClient.onPort(8080); HttpServer server = diff --git a/test/base/controller_test.dart b/test/base/controller_test.dart index 4287a8990..b217886f3 100644 --- a/test/base/controller_test.dart +++ b/test/base/controller_test.dart @@ -12,8 +12,10 @@ import '../helpers.dart'; void main() { HttpServer server; - ManagedDataModel dm = new ManagedDataModel([TestModel]); - ManagedContext _ = new ManagedContext(dm, new DefaultPersistentStore()); + setUpAll(() { + new ManagedContext( + new ManagedDataModel([TestModel]), new DefaultPersistentStore()); + }); tearDown(() async { await server?.close(force: true); @@ -259,6 +261,33 @@ void main() { expect(resp.body, '"false"'); }); + test("Content-Type defaults to application/json", () async { + server = await enableController("/a", TController); + var resp = await http.get("http://localhost:4040/a"); + expect(resp.statusCode, 200); + expect(ContentType.parse(resp.headers["content-type"]).primaryType, + "application"); + expect(ContentType.parse(resp.headers["content-type"]).subType, "json"); + }); + + test("Content-Type can be set adjusting responseContentType", () async { + server = await enableController("/a", ContentTypeController); + var resp = + await http.get("http://localhost:4040/a?opt=responseContentType"); + expect(resp.statusCode, 200); + expect(resp.headers["content-type"], "text/plain"); + expect(resp.body, "body"); + }); + + test("Content-Type set directly on Response overrides responseContentType", + () async { + server = await enableController("/a", ContentTypeController); + var resp = await http.get("http://localhost:4040/a?opt=direct"); + expect(resp.statusCode, 200); + expect(resp.headers["content-type"], "text/plain"); + expect(resp.body, "body"); + }); + group("Annotated HTTP parameters", () { test("are supplied correctly", () async { server = await enableController("/a", HTTPParameterController); @@ -402,6 +431,55 @@ void main() { expect(JSON.decode(resp.body)["error"], contains("Table")); expect(JSON.decode(resp.body)["error"], contains("Shaqs")); }); + + test("May only be one query parameter if arg type is not List", + () async { + server = await enableController("/a", DuplicateParamController); + var resp = await http + .get("http://localhost:4040/a?list=a&list=b&single=x&single=y"); + + expect(resp.statusCode, 400); + + expect(JSON.decode(resp.body)["error"], + "Duplicate parameter for non-List parameter type"); + }); + + test("Can be more than one query parameters for arg type that is List", + () async { + server = await enableController("/a", DuplicateParamController); + var resp = + await http.get("http://localhost:4040/a?list=a&list=b&single=x"); + + expect(resp.statusCode, 200); + + expect(JSON.decode(resp.body), { + "list": ["a", "b"], + "single": "x" + }); + }); + + test("Can be exactly one query parameter for arg type that is List", + () async { + server = await enableController("/a", DuplicateParamController); + var resp = await http.get("http://localhost:4040/a?list=a&single=x"); + + expect(resp.statusCode, 200); + + expect(JSON.decode(resp.body), { + "list": ["a"], + "single": "x" + }); + }); + + test("Missing required List query parameter still returns 400", + () async { + server = await enableController("/a", DuplicateParamController); + var resp = await http.get("http://localhost:4040/a?single=x"); + + expect(resp.statusCode, 400); + + expect(JSON.decode(resp.body)["error"], contains("list")); + }); }); } @@ -421,6 +499,7 @@ class FilteringController extends HTTPController { } class TController extends HTTPController { + TController() {} @httpGet Future getAll() async { return new Response.ok("getAll"); @@ -578,6 +657,27 @@ class ModelEncodeController extends HTTPController { } } +class ContentTypeController extends HTTPController { + @httpGet + getThing(@HTTPQuery("opt") String opt) async { + if (opt == "responseContentType") { + responseContentType = new ContentType("text", "plain"); + return new Response.ok("body"); + } else if (opt == "direct") { + return new Response.ok("body") + ..contentType = new ContentType("text", "plain"); + } + } +} + +class DuplicateParamController extends HTTPController { + @httpGet + Future getThing(@HTTPQuery("list") List list, + @HTTPQuery("single") String single) async { + return new Response.ok({"list": list, "single": single}); + } +} + Future enableController(String pattern, Type controller) async { var router = new Router(); router.route(pattern).generate( diff --git a/test/base/cors_test.dart b/test/base/cors_test.dart index db472bb24..bd0184021 100644 --- a/test/base/cors_test.dart +++ b/test/base/cors_test.dart @@ -192,6 +192,7 @@ void main() { await (new HttpClient().open("OPTIONS", "localhost", 8000, "opts")); req.headers.set("Authorization", "Bearer auth"); var resp = await req.close(); + await resp.drain(); expect(resp.statusCode, 200); expectThatNoCORSProcessingOccurred(resp); @@ -203,6 +204,7 @@ void main() { var req = await (new HttpClient().open("OPTIONS", "localhost", 8000, "opts")); var resp = await req.close(); + await resp.drain(); expect(resp.statusCode, 401); expectThatNoCORSProcessingOccurred(resp); @@ -213,6 +215,7 @@ void main() { var req = await (new HttpClient().open("OPTIONS", "localhost", 8000, "foobar")); var resp = await req.close(); + await resp.drain(); expect(resp.statusCode, 404); expectThatNoCORSProcessingOccurred(resp); @@ -224,6 +227,7 @@ void main() { var req = await (new HttpClient() .open("OPTIONS", "localhost", 8000, "nopolicy")); var resp = await req.close(); + await resp.drain(); expect(resp.statusCode, 405); expectThatNoCORSProcessingOccurred(resp); diff --git a/test/base/default_resource_controller_test.dart b/test/base/default_resource_controller_test.dart index b6128be3a..31b9aabae 100644 --- a/test/base/default_resource_controller_test.dart +++ b/test/base/default_resource_controller_test.dart @@ -6,7 +6,7 @@ import '../helpers.dart'; void main() { group("Standard operations", () { - Application app = new Application(); + var app = new Application(); app.configuration.port = 8080; var client = new TestClient.onPort(app.configuration.port); List allObjects = []; @@ -26,6 +26,7 @@ void main() { }); tearDownAll(() async { + await app.mainIsolateSink.context.persistentStore.close(); await app.stop(); }); @@ -82,7 +83,7 @@ void main() { }); group("Standard operation failure cases", () { - Application app = new Application(); + var app = new Application(); app.configuration.port = 8080; var client = new TestClient.onPort(8080); @@ -91,6 +92,7 @@ void main() { }); tearDownAll(() async { + await app.mainIsolateSink.context.persistentStore.close(); await app.stop(); }); @@ -114,8 +116,7 @@ void main() { }); group("Objects that don't exist", () { - Application app = null; - app = new Application(); + var app = new Application(); app.configuration.port = 8080; var client = new TestClient.onPort(8080); @@ -124,6 +125,7 @@ void main() { }); tearDownAll(() async { + await app.mainIsolateSink.context.persistentStore.close(); await app.stop(); }); @@ -150,8 +152,7 @@ void main() { }); group("Extended GET requests", () { - Application app = null; - app = new Application(); + var app = new Application(); app.configuration.port = 8080; var client = new TestClient.onPort(8080); List allObjects = []; @@ -171,6 +172,7 @@ void main() { }); tearDownAll(() async { + await app.mainIsolateSink.context.persistentStore.close(); await app.stop(); }); diff --git a/test/base/isolate_application_test.dart b/test/base/isolate_application_test.dart index 5cdab0662..c16a45083 100644 --- a/test/base/isolate_application_test.dart +++ b/test/base/isolate_application_test.dart @@ -82,39 +82,35 @@ main() { () async { var crashingApp = new Application(); - var succeeded = false; try { crashingApp.configuration.configurationOptions = { "crashIn": "constructor" }; await crashingApp.start(); - succeeded = true; - } catch (e) { - expect(e.message, "TestException: constructor"); + expect(true, false); + } on ApplicationStartupException catch (e) { + expect(e.toString(), contains("TestException: constructor")); } - expect(succeeded, false); try { crashingApp.configuration.configurationOptions = { "crashIn": "addRoutes" }; await crashingApp.start(); - succeeded = true; - } catch (e) { - expect(e.message, "TestException: addRoutes"); + expect(true, false); + } on ApplicationStartupException catch (e) { + expect(e.toString(), contains("TestException: addRoutes")); } - expect(succeeded, false); try { crashingApp.configuration.configurationOptions = { "crashIn": "willOpen" }; await crashingApp.start(); - succeeded = true; - } catch (e) { - expect(e.message, "TestException: willOpen"); + expect(true, false); + } on ApplicationStartupException catch (e) { + expect(e.toString(), contains("TestException: willOpen")); } - expect(succeeded, false); crashingApp.configuration.configurationOptions = {"crashIn": "dontCrash"}; await crashingApp.start(); @@ -132,14 +128,12 @@ main() { var conflictingApp = new Application(); conflictingApp.configuration.port = 8080; - var successful = false; try { await conflictingApp.start(); - successful = true; - } catch (e) { - expect(e, new isInstanceOf()); + expect(true, false); + } on ApplicationStartupException catch (e) { + expect(e, new isInstanceOf()); } - expect(successful, false); await server.close(force: true); await conflictingApp.stop(); diff --git a/test/base/pattern_test.dart b/test/base/pattern_test.dart index adc85cb55..41640c7a6 100644 --- a/test/base/pattern_test.dart +++ b/test/base/pattern_test.dart @@ -2,6 +2,8 @@ import "package:test/test.dart"; import "dart:core"; import 'package:aqueduct/aqueduct.dart'; +import '../../lib/src/http/route_node.dart'; + void main() { group("Pattern splitting", () { test("No optionals, no expressions", () { diff --git a/test/base/pipeline_test.dart b/test/base/pipeline_test.dart index 2f88b5ea7..b2002fc25 100644 --- a/test/base/pipeline_test.dart +++ b/test/base/pipeline_test.dart @@ -9,9 +9,11 @@ void main() { try { await app.start(); expect(true, false); - } on ApplicationSupervisorException catch (e) { - expect(e.message, - "RequestController subclass FailingController instances cannot be reused. Rewrite as .generate(() => new FailingController())"); + } on ApplicationStartupException catch (e) { + expect( + e.toString(), + contains( + "RequestController subclass FailingController instances cannot be reused. Rewrite as .generate(() => new FailingController())")); } }); } diff --git a/test/base/recovery_test.dart b/test/base/recovery_test.dart index b5d8735c4..c993a9d19 100644 --- a/test/base/recovery_test.dart +++ b/test/base/recovery_test.dart @@ -30,7 +30,7 @@ main() { var errorMessage = await app.logger.onRecord.first; expect(errorMessage.message, contains("Uncaught exception")); expect( - errorMessage.error.toString(), contains("method not found: 'foo'")); + errorMessage.error.toString(), contains("foo")); expect(errorMessage.stackTrace, isNotNull); // And then we should make sure everything is working just fine. @@ -51,7 +51,7 @@ main() { logMessages.forEach((errorMessage) { expect(errorMessage.message, contains("Uncaught exception")); expect( - errorMessage.error.toString(), contains("method not found: 'foo'")); + errorMessage.error.toString(), contains("foo")); expect(errorMessage.stackTrace, isNotNull); }); diff --git a/test/base/request_controller_test.dart b/test/base/request_controller_test.dart index 7c257a3ab..c65849808 100644 --- a/test/base/request_controller_test.dart +++ b/test/base/request_controller_test.dart @@ -37,7 +37,7 @@ void main() { await next.receive(req); - // We'll get here only if delivery succeeds, evne tho the response must be an error + // We'll get here only if delivery succeeds, even tho the response must be an error ensureExceptionIsCapturedByDeliver.complete(true); }); @@ -147,6 +147,215 @@ void main() { expect(resp.headers["content-type"], startsWith("application/json")); expect(JSON.decode(resp.body), {"name": "Bob"}); }); + + test( + "Responding to request with no content-type, but does have a body, defaults to application/json", + () async { + server = await HttpServer.bind(InternetAddress.LOOPBACK_IP_V4, 8080); + server.map((req) => new Request(req)).listen((req) async { + var next = new RequestController(); + next.listen((req) async { + return new Response.ok({"a": "b"}); + }); + await next.receive(req); + }); + + var resp = await http.get("http://localhost:8080"); + expect(resp.headers["content-type"], startsWith("application/json")); + expect(JSON.decode(resp.body), {"a": "b"}); + }); + + test( + "Responding to a request with no explicit content-type and has a body that cannot be encoded to JSON will throw 500", + () async { + server = await HttpServer.bind(InternetAddress.LOOPBACK_IP_V4, 8080); + server.map((req) => new Request(req)).listen((req) async { + var next = new RequestController(); + next.listen((req) async { + return new Response.ok(new DateTime.now()); + }); + await next.receive(req); + }); + + var resp = await http.get("http://localhost:8080"); + expect(resp.statusCode, 500); + expect(resp.headers["content-type"], "text/plain; charset=utf-8"); + expect(resp.body, ""); + }); + + test( + "Responding to request with no explicit content-type, but does not have a body, defaults to plaintext Content-Type header", + () async { + server = await HttpServer.bind(InternetAddress.LOOPBACK_IP_V4, 8080); + server.map((req) => new Request(req)).listen((req) async { + var next = new RequestController(); + next.listen((req) async { + return new Response.ok(null); + }); + await next.receive(req); + }); + var resp = await http.get("http://localhost:8080"); + expect(resp.statusCode, 200); + expect(resp.headers["content-length"], "0"); + expect(resp.headers["content-type"], "text/plain; charset=utf-8"); + expect(resp.body, ""); + }); + + test("Using an encoder that doesn't exist returns a 500", () async { + server = await HttpServer.bind(InternetAddress.LOOPBACK_IP_V4, 8080); + server.map((req) => new Request(req)).listen((req) async { + var next = new RequestController(); + next.listen((req) async { + return new Response.ok(1234) + ..contentType = new ContentType("foo", "bar", charset: "utf-8"); + }); + await next.receive(req); + }); + var resp = await http.get("http://localhost:8080"); + var contentType = ContentType.parse(resp.headers["content-type"]); + expect(resp.statusCode, 500); + expect(contentType.primaryType, "application"); + expect(contentType.subType, "json"); + expect(JSON.decode(resp.body), + {"error": "Could not encode body as foo/bar; charset=utf-8."}); + }); + + test( + "Using an encoder other than the default correctly encodes and sets content-type", + () async { + server = await HttpServer.bind(InternetAddress.LOOPBACK_IP_V4, 8080); + server.map((req) => new Request(req)).listen((req) async { + var next = new RequestController(); + next.listen((req) async { + return new Response.ok(1234) + ..contentType = new ContentType("text", "plain"); + }); + await next.receive(req); + }); + var resp = await http.get("http://localhost:8080"); + expect(resp.statusCode, 200); + expect(resp.headers["content-type"], "text/plain"); + expect(resp.body, "1234"); + }); + + test("A decoder with a match-all subtype will be used when matching", + () async { + Response.addEncoder(new ContentType("b", "*"), (s) => s); + server = await HttpServer.bind(InternetAddress.LOOPBACK_IP_V4, 8080); + server.map((req) => new Request(req)).listen((req) async { + var next = new RequestController(); + next.listen((req) async { + return new Response.ok("hello") + ..contentType = new ContentType("b", "bar", charset: "utf-8"); + }); + await next.receive(req); + }); + var resp = await http.get("http://localhost:8080"); + expect(resp.statusCode, 200); + expect(resp.headers["content-type"], "b/bar; charset=utf-8"); + expect(resp.body, "hello"); + }); + + test( + "A decoder with a subtype always trumps a decoder that matches any subtype", + () async { + Response.addEncoder(new ContentType("a", "*"), (s) => s); + Response.addEncoder(new ContentType("a", "html"), (s) { + return "$s"; + }); + server = await HttpServer.bind(InternetAddress.LOOPBACK_IP_V4, 8080); + server.map((req) => new Request(req)).listen((req) async { + var next = new RequestController(); + next.listen((req) async { + return new Response.ok("hello") + ..contentType = new ContentType("a", "html", charset: "utf-8"); + }); + await next.receive(req); + }); + var resp = await http.get("http://localhost:8080"); + expect(resp.statusCode, 200); + expect(resp.headers["content-type"], "a/html; charset=utf-8"); + expect(resp.body, "hello"); + }); + + test("Using an encoder that blows up during encoded returns 500 safely", + () async { + Response.addEncoder(new ContentType("foo", "bar"), (s) { + throw new Exception("uhoh"); + }); + server = await HttpServer.bind(InternetAddress.LOOPBACK_IP_V4, 8080); + server.map((req) => new Request(req)).listen((req) async { + var next = new RequestController(); + next.listen((req) async { + return new Response.ok("hello") + ..contentType = new ContentType("foo", "bar", charset: "utf-8"); + }); + await next.receive(req); + }); + var resp = await http.get("http://localhost:8080"); + expect(resp.statusCode, 500); + }); + + test("willSendResponse is always called prior to Response being sent for preflight requests", () async { + server = await HttpServer.bind(InternetAddress.LOOPBACK_IP_V4, 8080); + server.map((req) => new Request(req)).listen((req) async { + var next = new RequestController(); + next.generate(() => new Always200Controller()); + await next.receive(req); + }); + + // Invalid preflight + var req = await (new HttpClient().open("OPTIONS", "localhost", 8080, "")); + req.headers.set("Origin", "http://foobar.com"); + req.headers.set("Access-Control-Request-Method", "POST"); + req.headers + .set("Access-Control-Request-Headers", "accept, authorization"); + var resp = await req.close(); + + expect(resp.statusCode, 200); + expect(JSON.decode((new String.fromCharCodes(await resp.first))), {"statusCode" : 403}); + + // valid preflight + req = await (new HttpClient().open("OPTIONS", "localhost", 8080, "")); + req.headers.set("Origin", "http://somewhere.com"); + req.headers.set("Access-Control-Request-Method", "POST"); + req.headers + .set("Access-Control-Request-Headers", "accept, authorization"); + resp = await req.close(); + + expect(resp.statusCode, 200); + expect(resp.headers.value("access-control-allow-methods"), "POST, PUT, DELETE, GET"); + expect(JSON.decode((new String.fromCharCodes(await resp.first))), {"statusCode" : 200}); + }); + + test("willSendResponse is always called prior to Response being sent for normal requests", () async { + server = await HttpServer.bind(InternetAddress.LOOPBACK_IP_V4, 8080); + server.map((req) => new Request(req)).listen((req) async { + var next = new RequestController(); + next.generate(() => new Always200Controller()); + await next.receive(req); + }); + + // normal response + var resp = await http.get("http://localhost:8080"); + expect(resp.statusCode, 200); + expect(JSON.decode(resp.body), {"statusCode" : 100}); + + // httpresponseexception + resp = await http.get("http://localhost:8080?q=http_response_exception"); + expect(resp.statusCode, 200); + expect(JSON.decode(resp.body), {"statusCode" : 400}); + + // query exception + resp = await http.get("http://localhost:8080?q=query_exception"); + expect(resp.statusCode, 200); + expect(JSON.decode(resp.body), {"statusCode" : 503}); + + // any other exception (500) + resp = await http.get("http://localhost:8080?q=server_error"); + expect(resp.statusCode, 200); + expect(JSON.decode(resp.body), {"statusCode" : 500}); + }); } class SomeObject implements HTTPSerializable { @@ -156,3 +365,31 @@ class SomeObject implements HTTPSerializable { return {"name": name}; } } + +class Always200Controller extends RequestController { + Always200Controller() { + policy.allowedOrigins = ["http://somewhere.com"]; + } + @override + Future processRequest(Request req) async { + var q = req.innerRequest.uri.queryParameters["q"]; + if (q == "http_response_exception") { + throw new HTTPResponseException(400, "ok"); + } else if (q == "query_exception") { + throw new QueryException(QueryExceptionEvent.connectionFailure); + } else if (q == "server_error") { + throw new FormatException("whocares"); + } + return new Response(100, null, null); + } + + @override + void willSendResponse(Response resp) { + var originalMap = { + "statusCode" : resp.statusCode + }; + resp.statusCode = 200; + resp.body = originalMap; + resp.contentType = ContentType.JSON; + } +} \ No newline at end of file diff --git a/test/command/create_test.dart b/test/command/create_test.dart new file mode 100644 index 000000000..030947cf0 --- /dev/null +++ b/test/command/create_test.dart @@ -0,0 +1,112 @@ +import 'package:test/test.dart'; +import 'dart:io'; +import 'package:path/path.dart' as path_lib; + +Directory testTemplateDirectory = new Directory("tmp_templates"); + +void main() { + setUp(() { + testTemplateDirectory.createSync(); + }); + + tearDown(() { + testTemplateDirectory.deleteSync(recursive: true); + }); + + group("Project naming", () { + test("Appropriately named project gets created correctly", () { + var res = runWith(["-n", "test_project"]); + expect(res.exitCode, 0); + + expect( + new Directory( + path_lib.join(testTemplateDirectory.path, "test_project")) + .existsSync(), + true); + }); + + test("Project name with bad characters fails immediately", () { + var res = runWith(["-n", "!@"]); + expect(res.exitCode, 1); + + expect(testTemplateDirectory.listSync().isEmpty, true); + }); + + test("Project name with uppercase characters fails immediately", () { + var res = runWith(["-n", "ANeatApp"]); + expect(res.exitCode, 1); + + expect(testTemplateDirectory.listSync().isEmpty, true); + }); + + test("Project name with dashes fails immediately", () { + var res = runWith(["-n", "a-neat-app"]); + expect(res.exitCode, 1); + + expect(testTemplateDirectory.listSync().isEmpty, true); + }); + + test("Not providing name returns error", () { + var res = runWith([]); + expect(res.exitCode, 1); + + expect(testTemplateDirectory.listSync().isEmpty, true); + }); + + test("Providing empty name returns error", () { + var res = runWith(["-n"]); + expect(res.exitCode, 255); + + expect(testTemplateDirectory.listSync().isEmpty, true); + }); + }); + + group("Templates from path", () { + test("Template gets generated from local path, project points to it", () { + var res = runWith(["-n", "test_project"]); + expect(res.exitCode, 0); + + var aqueductLocationString = + new File(projectPath("test_project", file: ".packages")) + .readAsStringSync() + .split("\n") + .firstWhere((p) => p.startsWith("aqueduct:")) + .split("aqueduct:") + .last; + + var path = path_lib.normalize(path_lib.fromUri(aqueductLocationString)); + expect(path, path_lib.join(Directory.current.path, "lib")); + }); + + test("Tests run on template generated from local path", () { + var res = runWith(["-n", "test_project"]); + expect(res.exitCode, 0); + + res = Process.runSync("pub", ["run", "test", "-j", "1"], + runInShell: true, + workingDirectory: + path_lib.join(testTemplateDirectory.path, "test_project")); + print("${res.stdout} ${res.stderr}"); + expect(res.exitCode, 0); + expect(res.stdout, contains("All tests passed")); + }); + }); +} + +ProcessResult runWith(List args) { + var aqueductDirectory = Directory.current.path; + var result = Process.runSync( + "pub", ["global", "activate", "-spath", "$aqueductDirectory"], + runInShell: true); + expect(result.exitCode, 0); + + var allArgs = ["create", "--path-source", "$aqueductDirectory"]; + allArgs.addAll(args); + + return Process.runSync("aqueduct", allArgs, + runInShell: true, workingDirectory: testTemplateDirectory.path); +} + +String projectPath(String projectName, {String file}) { + return path_lib.join(testTemplateDirectory.path, projectName, file); +} diff --git a/test/db/model_controller_test.dart b/test/db/model_controller_test.dart index d95356886..4ae34d6e3 100644 --- a/test/db/model_controller_test.dart +++ b/test/db/model_controller_test.dart @@ -24,6 +24,7 @@ main() { }); tearDownAll(() async { + await context.persistentStore.close(); await server?.close(force: true); }); diff --git a/test/db/model_test.dart b/test/db/model_test.dart index 695459e4f..be17a48c5 100644 --- a/test/db/model_test.dart +++ b/test/db/model_test.dart @@ -4,10 +4,12 @@ import 'dart:mirrors'; import '../helpers.dart'; void main() { - var ps = new DefaultPersistentStore(); - ManagedDataModel dm = - new ManagedDataModel([TransientTest, TransientTypeTest, User, Post]); - ManagedContext _ = new ManagedContext(dm, ps); + setUpAll(() { + var ps = new DefaultPersistentStore(); + ManagedDataModel dm = + new ManagedDataModel([TransientTest, TransientTypeTest, User, Post]); + var _ = new ManagedContext(dm, ps); + }); test("NoSuchMethod still throws", () { var user = new User(); diff --git a/test/db/postgresql/adapter_test.dart b/test/db/postgresql/adapter_test.dart index bc9413174..c969a88f7 100644 --- a/test/db/postgresql/adapter_test.dart +++ b/test/db/postgresql/adapter_test.dart @@ -4,16 +4,14 @@ import 'package:postgres/postgres.dart'; import 'dart:async'; void main() { - PostgreSQLPersistentStore persistentStore = null; - - setUp(() { - persistentStore = new PostgreSQLPersistentStore(() async { - var connection = new PostgreSQLConnection("localhost", 5432, "dart_test", - username: "dart", password: "dart"); - await connection.open(); - return connection; - }); + PostgreSQLPersistentStore persistentStore = + new PostgreSQLPersistentStore(() async { + var connection = new PostgreSQLConnection("localhost", 5432, "dart_test", + username: "dart", password: "dart"); + await connection.open(); + return connection; }); + ; tearDown(() async { await persistentStore.close(); @@ -62,7 +60,11 @@ void main() { var connection = new PostgreSQLConnection( "localhost", 5432, "xyzxyznotadb", username: "dart", password: "dart"); - await connection.open(); + try { + await connection.open(); + } catch (e) { + await connection.close(); + } return connection; }); var expectedValues = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; @@ -76,14 +78,19 @@ void main() { () async { var counter = 0; persistentStore = new PostgreSQLPersistentStore(() async { - var conn = (counter == 0 + var connection = (counter == 0 ? new PostgreSQLConnection("localhost", 5432, "xyzxyznotadb", username: "dart", password: "dart") : new PostgreSQLConnection("localhost", 5432, "dart_test", username: "dart", password: "dart")); counter++; - await conn.open(); - return conn; + try { + await connection.open(); + } catch (e) { + await connection.close(); + } + + return connection; }); var expectedValues = [1, 2, 3, 4, 5]; var values = await Future.wait(expectedValues diff --git a/test/db/query_test.dart b/test/db/query_test.dart index ec4c78a0d..0a4df9951 100644 --- a/test/db/query_test.dart +++ b/test/db/query_test.dart @@ -3,9 +3,11 @@ import 'package:test/test.dart'; import '../helpers.dart'; main() { - var ps = new DefaultPersistentStore(); - ManagedDataModel dm = new ManagedDataModel([TestModel]); - ManagedContext _ = new ManagedContext(dm, ps); + setUpAll(() { + var ps = new DefaultPersistentStore(); + ManagedDataModel dm = new ManagedDataModel([TestModel]); + ManagedContext _ = new ManagedContext(dm, ps); + }); test("Accessing valueObject of Query automatically creates an instance", () { var q = new Query()..values.id = 1; diff --git a/test/helpers.dart b/test/helpers.dart index dd94308d8..83062a6d4 100644 --- a/test/helpers.dart +++ b/test/helpers.dart @@ -121,6 +121,7 @@ class AuthDelegate implements AuthServerDelegate { tokenQ.predicate = new QueryPredicate( "refreshToken = @refreshToken", {"refreshToken": t.refreshToken}); tokenQ.values = t; + return tokenQ.updateOne(); } @@ -146,6 +147,7 @@ class AuthDelegate implements AuthServerDelegate { Future deleteAuthCode(AuthServer server, AuthCode code) async { var authCodeQ = new Query(); authCodeQ.predicate = new QueryPredicate("id = @id", {"id": code.id}); + return authCodeQ.delete(); } diff --git a/test/test_project/lib/src/controller/identity_controller.dart b/test/test_project/lib/src/controller/identity_controller.dart index b5359a27f..1eb068315 100644 --- a/test/test_project/lib/src/controller/identity_controller.dart +++ b/test/test_project/lib/src/controller/identity_controller.dart @@ -1,4 +1,4 @@ -part of wildfire; +import '../../wildfire.dart'; class IdentityController extends HTTPController { @httpGet diff --git a/test/test_project/lib/src/controller/register_controller.dart b/test/test_project/lib/src/controller/register_controller.dart index ae3b9dab4..7956e2437 100644 --- a/test/test_project/lib/src/controller/register_controller.dart +++ b/test/test_project/lib/src/controller/register_controller.dart @@ -1,4 +1,4 @@ -part of wildfire; +import '../../wildfire.dart'; class RegisterController extends QueryController { @httpPost diff --git a/test/test_project/lib/src/controller/user_controller.dart b/test/test_project/lib/src/controller/user_controller.dart index a89156571..5336dec31 100644 --- a/test/test_project/lib/src/controller/user_controller.dart +++ b/test/test_project/lib/src/controller/user_controller.dart @@ -1,4 +1,4 @@ -part of wildfire; +import '../../wildfire.dart'; class UserController extends QueryController { @httpGet diff --git a/test/test_project/lib/src/model/token.dart b/test/test_project/lib/src/model/token.dart index ebf746f91..56f174df4 100644 --- a/test/test_project/lib/src/model/token.dart +++ b/test/test_project/lib/src/model/token.dart @@ -1,4 +1,4 @@ -part of wildfire; +import '../../wildfire.dart'; class AuthCode extends ManagedObject<_AuthCode> implements _AuthCode {} diff --git a/test/test_project/lib/src/model/user.dart b/test/test_project/lib/src/model/user.dart index 04cce11c1..9700edc00 100644 --- a/test/test_project/lib/src/model/user.dart +++ b/test/test_project/lib/src/model/user.dart @@ -1,4 +1,4 @@ -part of wildfire; +import '../../wildfire.dart'; class User extends ManagedObject<_User> implements _User, Authenticatable { @managedTransientInputAttribute diff --git a/test/test_project/lib/src/utilities/auth_delegate.dart b/test/test_project/lib/src/utilities/auth_delegate.dart index 35b455b47..0e409deab 100644 --- a/test/test_project/lib/src/utilities/auth_delegate.dart +++ b/test/test_project/lib/src/utilities/auth_delegate.dart @@ -1,4 +1,4 @@ -part of wildfire; +import '../../wildfire.dart'; class WildfireAuthenticationDelegate implements AuthServerDelegate { diff --git a/test/test_project/lib/src/wildfire_sink.dart b/test/test_project/lib/src/wildfire_sink.dart index e5ab77f57..e801e067b 100644 --- a/test/test_project/lib/src/wildfire_sink.dart +++ b/test/test_project/lib/src/wildfire_sink.dart @@ -1,4 +1,4 @@ -part of wildfire; +import '../wildfire.dart'; class WildfireConfiguration extends ConfigurationItem { WildfireConfiguration(String fileName) : super.fromFile(fileName); @@ -58,8 +58,7 @@ class WildfireSink extends RequestSink { ManagedContext contextWithConnectionInfo( DatabaseConnectionConfiguration database) { var connectionInfo = configuration.database; - var dataModel = - new ManagedDataModel.fromPackageContainingType(this.runtimeType); + var dataModel = new ManagedDataModel.fromCurrentMirrorSystem(); var psc = new PostgreSQLPersistentStore.fromConnectionInfo( connectionInfo.username, connectionInfo.password, diff --git a/test/test_project/lib/wildfire.dart b/test/test_project/lib/wildfire.dart index b7fddd986..4c6d6d127 100644 --- a/test/test_project/lib/wildfire.dart +++ b/test/test_project/lib/wildfire.dart @@ -6,16 +6,14 @@ /// A web server. library wildfire; -import 'dart:io'; -import 'dart:async'; -import 'package:aqueduct/aqueduct.dart'; - +export 'dart:io'; +export 'dart:async'; export 'package:aqueduct/aqueduct.dart'; -part 'src/model/token.dart'; -part 'src/model/user.dart'; -part 'src/wildfire_sink.dart'; -part 'src/controller/user_controller.dart'; -part 'src/controller/identity_controller.dart'; -part 'src/controller/register_controller.dart'; -part 'src/utilities/auth_delegate.dart'; +export 'src/model/token.dart'; +export 'src/model/user.dart'; +export 'src/wildfire_sink.dart'; +export 'src/controller/user_controller.dart'; +export 'src/controller/identity_controller.dart'; +export 'src/controller/register_controller.dart'; +export 'src/utilities/auth_delegate.dart'; diff --git a/test/utilities/test_client_test.dart b/test/utilities/test_client_test.dart index 020c55c72..d2d649342 100644 --- a/test/utilities/test_client_test.dart +++ b/test/utilities/test_client_test.dart @@ -355,13 +355,15 @@ void main() { test("Can match text object", () async { var defaultTestClient = new TestClient.onPort(4000); + server.queueResponse( - new Response.ok("text", headers: {"Content-Type": "text/plain"})); + new Response.ok("text")..contentType = ContentType.TEXT); var response = await defaultTestClient.request("/foo").get(); expect(response, hasBody("text")); server.queueResponse( - new Response.ok("text", headers: {"Content-Type": "text/plain"})); + new Response.ok("text")..contentType = ContentType.TEXT); + response = await defaultTestClient.request("/foo").get(); try { expect(response, hasBody("foobar")); @@ -375,13 +377,13 @@ void main() { test("Can match JSON Object", () async { var defaultTestClient = new TestClient.onPort(4000); - server.queueResponse(new Response.ok({"foo": "bar"}, - headers: {"Content-Type": "application/json"})); + server.queueResponse( + new Response.ok({"foo": "bar"})..contentType = ContentType.JSON); var response = await defaultTestClient.request("/foo").get(); expect(response, hasBody(isNotNull)); - server.queueResponse(new Response.ok({"foo": "bar"}, - headers: {"Content-Type": "application/json"})); + server.queueResponse( + new Response.ok({"foo": "bar"})..contentType = ContentType.JSON); response = await defaultTestClient.request("/foo").get(); try { expect(response, hasBody({"foo": "notbar"})); @@ -391,9 +393,10 @@ void main() { expect(e.toString(), contains('Body: {"foo":"bar"}')); } - server.queueResponse(new Response.ok( - {"nocontenttype": "thatsaysthisisjson"}, - headers: {"Content-Type": "text/plain"})); + server.queueResponse( + new Response.ok({"nocontenttype": "thatsaysthisisjson"}) + ..contentType = ContentType.TEXT); + response = await defaultTestClient.request("/foo").get(); try { expect(response, @@ -509,6 +512,36 @@ void main() { expect(e.toString(), contains('Body: {"foo":"bar","x":5}')); } }); + + test("Partial match, null and not present", () async { + var defaultTestClient = new TestClient.onPort(4000); + server.queueResponse(new Response.ok({"foo": null, "bar": "boo"})); + var response = await defaultTestClient.request("/foo").get(); + expect(response, hasBody(partial({"bar": "boo"}))); + expect(response, hasBody(partial({"foo": isNull}))); + expect(response, hasBody(partial({"baz": isNotPresent}))); + + try { + expect(response, hasBody(partial({"foo": isNotPresent}))); + expect(true, false); + } on TestFailure catch (e) { + expect( + e.toString(), + contains( + "Expected: Body: Partially matches: {foo: ,}")); + expect(e.toString(), contains('Body: {"foo":null,"bar":"boo"}')); + } + try { + expect(response, hasBody(partial({"bar": isNotPresent}))); + expect(true, false); + } on TestFailure catch (e) { + expect( + e.toString(), + contains( + "Expected: Body: Partially matches: {bar: ,}")); + expect(e.toString(), contains('Body: {"foo":null,"bar":"boo"}')); + } + }); }); group("Total matcher", () { @@ -524,8 +557,9 @@ void main() { test("Omit status code ignores it", () async { var defaultTestClient = new TestClient.onPort(4000); - server.queueResponse(new Response.ok({"foo": "bar"}, - headers: {"content-type": "application/json"})); + server.queueResponse( + new Response.ok({"foo": "bar"})..contentType = ContentType.JSON); + var response = await defaultTestClient.request("/foo").get(); expect( response,